Android:xUtils3浅析(二)——数据库模块

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/DrkCore/article/details/51866495

【转载请注明出处】
作者:DrkCore (http://blog.csdn.net/DrkCore)
原文链接:(http://blog.csdn.net/drkcore/article/details/51866495)

xUtils3的数据库模块是我们经常使用的一个模块。如果你对SQLiteOpenHelper的各种方法十分的熟悉的话你就会发现该模块其实就是对其的一种封装:通过注解、建造者模式等方式替代徒手撕鬼子式手写SQL语句来规范化对数据库的操作,毫无疑问的是——这确实帮了我们很大的忙。

一、 Column注解与Converter

大多数注解本身只是起到一个标记的作用,Column注解也不例外,其源码如下:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {

    String name();

    String property() default "";

    boolean isId() default false;

    boolean autoGen() default true;
}

用过xUtils3数据库模块的人都知道每个方法代表意义,不再赘述。

在运行过程中Column所标注的成员变量会被“翻译”成ColumnEntity

    /* package */ ColumnEntity(Class<?> entityType, Field field, Column column) {
        field.setAccessible(true);

        this.columnField = field;
        this.name = column.name();
        this.property = column.property();
        this.isId = column.isId();

        Class<?> fieldType = field.getType();
        this.isAutoId = this.isId && column.autoGen() && ColumnUtils.isAutoIdType(fieldType);
        this.columnConverter = ColumnConverterFactory.getColumnConverter(fieldType);

        this.getMethod = ColumnUtils.findGetMethod(entityType, field);
        if (this.getMethod != null && !this.getMethod.isAccessible()) {
            this.getMethod.setAccessible(true);
        }
        this.setMethod = ColumnUtils.findSetMethod(entityType, field);
        if (this.setMethod != null && !this.setMethod.isAccessible()) {
            this.setMethod.setAccessible(true);
        }

就像我们知道的那样,注解要和反射混着用才能发挥神奇的疗效,这里也不例外。需要注意的是这里的一行代码:

this.columnConverter = ColumnConverterFactory.getColumnConverter(fieldType);

我们知道在SQLite中除了整形的主键列之外其他列存储的类型都是未定的,所以和JAVA的基础数据类型自然不可能一一对应。

在JAVA中整数的类型就有int、short、long三种类型而在SQLite中只有一个INTEGER,另一个例子是SQLite没有BOOLEAN类型。这些矛盾在xUtils3中是通过策略工厂模式来化解的,就拿BooleanColumnConverter做例子:

public class BooleanColumnConverter implements ColumnConverter<Boolean> {
    @Override
    public Boolean getFieldValue(final Cursor cursor, int index) {
        return cursor.isNull(index) ? null : cursor.getInt(index) == 1;
    }

    @Override
    public Object fieldValue2DbValue(Boolean fieldValue) {
        if (fieldValue == null) return null;
        return fieldValue ? 1 : 0;
    }

    @Override
    public ColumnDbType getColumnDbType() {
        return ColumnDbType.INTEGER;
    }
}

你会一点都不惊讶地发现JAVA中的true和false是被SQLite中的INTEGER的1和0来表示的。而boolean的列和BooleanColumnConverter之间的对应关系以及其他的类型映射关系自然而然地被写进了工厂类ColumnConverterFactory之中:

 private static final ConcurrentHashMap<String, ColumnConverter> columnType_columnConverter_map;

    static {
        columnType_columnConverter_map = new ConcurrentHashMap<String, ColumnConverter>();

        BooleanColumnConverter booleanColumnConverter = new BooleanColumnConverter();
        columnType_columnConverter_map.put(boolean.class.getName(), booleanColumnConverter);
        columnType_columnConverter_map.put(Boolean.class.getName(), booleanColumnConverter);

        ByteArrayColumnConverter byteArrayColumnConverter = new ByteArrayColumnConverter();
        columnType_columnConverter_map.put(byte[].class.getName(), byteArrayColumnConverter);
        //……省略一些重复的内容
    }

就如其名字一样,Converter起到JAVA数据类型和SQLite数据类型相互转化的作用,在构建建表语句和获取数据的过程中都起到了重要作用。

二、 Table注解与建表语句的构建

该注解的源代码如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {

    String name();

    String onCreated() default "";
}

如你所见真的是十分地简短,其生成TableEntity的逻辑如下:

    /*package*/ TableEntity(DbManager db, Class<T> entityType) throws Throwable {
        this.db = db;
        this.entityType = entityType;
        this.constructor = entityType.getConstructor();
        this.constructor.setAccessible(true);
        Table table = entityType.getAnnotation(Table.class);
        this.name = table.name();
        this.onCreated = table.onCreated();
        this.columnMap = TableUtils.findColumnMap(entityType);

        for (ColumnEntity column : columnMap.values()) {
            if (column.isId()) {
                this.id = column;
                break;
            }
        }
    }

依旧是注解加反射,可以看到上面通过工具获取了所有的Column并找出了主键。

最后构建SQL语法的逻辑作者将之写进了构造者SqlInfoBuilder的buildCreateTableSqlInfo方法中:

    public static SqlInfo buildCreateTableSqlInfo(TableEntity<?> table) throws DbException {
        ColumnEntity id = table.getId();

        StringBuilder builder = new StringBuilder();
        builder.append("CREATE TABLE IF NOT EXISTS ");
        builder.append("\"").append(table.getName()).append("\"");
        builder.append(" ( ");

        if (id.isAutoId()) {
            builder.append("\"").append(id.getName()).append("\"").append(" INTEGER PRIMARY KEY AUTOINCREMENT, ");
        } else {
            builder.append("\"").append(id.getName()).append("\"").append(id.getColumnDbType()).append(" PRIMARY KEY, ");
        }

        Collection<ColumnEntity> columns = table.getColumnMap().values();
        for (ColumnEntity column : columns) {
            if (column.isId()) continue;
            builder.append("\"").append(column.getName()).append("\"");
            builder.append(' ').append(column.getColumnDbType());
            builder.append(' ').append(column.getProperty());
            builder.append(',');
        }

        builder.deleteCharAt(builder.length() - 1);
        builder.append(" )");
        return new SqlInfo(builder.toString());
    }

通过StringBuilder将建表语法拼接起来之后扔到DbManager去执行,就完成建表。

不过大家都知道反射操作向来是比较花时间的,对于这种问题的优化方式通常是用空间来换取时间,用人话说就是缓存。所以你可以在DbManagerImpl的抽象父类DbBase中看到这样的代码:

public abstract class DbBase implements DbManager {

    private final HashMap<Class<?>, TableEntity<?>> tableMap = new HashMap<>();

    @Override
    @SuppressWarnings("unchecked")
    public <T> TableEntity<T> getTable (Class<T> entityType) throws DbException {
        synchronized (tableMap) {
            TableEntity<T> table = (TableEntity<T>) tableMap.get(entityType);
            if (table == null) {
                try {
                    table = new TableEntity<T>(this, entityType);
                } catch (Throwable ex) {
                    throw new DbException(ex);
                }
                tableMap.put(entityType, table);
            }

            return table;
        }
    }
}

这里用一个HashMap实现了缓存,干得漂亮。

三、 findAll 和 findDbModelAll

说到SQLite的查找方法估计大家第一时间想到的就是Cursor,而之所以影响这么深刻想必都是在第一次用的时候踩过不少坑。

findAll方法的查找的语句按照惯例无非是给出一个TableEntity然后拼接出一个Select语句,不再赘述。比较核心的部分则是在如何将Cursor每行的数据转化成对象,这部分的代码作者将之写进了工具类CursorUtils之中:

    public static <T> T getEntity(TableEntity<T> table, final Cursor cursor) throws Throwable {
        T entity = table.createEntity();
        HashMap<String, ColumnEntity> columnMap = table.getColumnMap();
        int columnCount = cursor.getColumnCount();
        for (int i = 0; i < columnCount; i++) {
            String columnName = cursor.getColumnName(i);
            ColumnEntity column = columnMap.get(columnName);
            if (column != null) {
                column.setValueFromCursor(entity, cursor, i);
            }
        }
        return entity;
    }

这一块的逻辑只是遍历Cursor而已,数据的转化和赋值则全部都交给了我们前文说过的ColumnConverter之中:

    public void setValueFromCursor(Object entity, Cursor cursor, int index) {
        Object value = columnConverter.getFieldValue(cursor, index);
        if (value == null) return;

        if (setMethod != null) {
            try {
                setMethod.invoke(entity, value);
            } catch (Throwable e) {
                LogUtil.e(e.getMessage(), e);
            }
        } else {
            try {
                this.columnField.set(entity, value);
            } catch (Throwable e) {
                LogUtil.e(e.getMessage(), e);
            }
        }
    }

如果set方法存在则反射调用set方法,否则直接反射赋值。通常前者的速度会更快一些。

xUtils3的数据库模块还提供了用SQL语法直接查询的逻辑所以在代码中作者提供了findDbModleAll的方法。与findAll的差别在于从Cursor中提取数据的逻辑不同:

    public static DbModel getDbModel(final Cursor cursor) {
        DbModel result = new DbModel();
        int columnCount = cursor.getColumnCount();
        for (int i = 0; i < columnCount; i++) {
            result.add(cursor.getColumnName(i), cursor.getString(i));
        }
        return result;
    }

将所有的内容转成String保存起来,简直粗暴。到这里你就会发现,其实DbModel本质上有点像是HashMap的装饰者。当你真正要使用到数据时候它会将字符串转换成你想要的数据类型给你,比如:

public final class DbModel {

    /**
     * key: columnName
     * value: valueStr
     */
    private HashMap<String, String> dataMap = new HashMap<String, String>();

    public String getString(String columnName) {
        return dataMap.get(columnName);
    }

    public int getInt(String columnName) {
        return Integer.valueOf(dataMap.get(columnName));
    }

    public boolean getBoolean(String columnName) {
        String value = dataMap.get(columnName);
        if (value != null) {
            return value.length() == 1 ? "1".equals(value) : Boolean.valueOf(value);
        }
        return false;
    }

    //省略部分代码
}

看,简单易懂啊。

不过说道这里我很好奇为什么作者要用Integer.valueOf()实现这个方法,按理说AS应该会提示这里使用Integer.parseInt()会更合适一点。笔者在后面的自定义过程中就直接用后者来代替了。

剩下的删除、更新之类的操作大都是通过TableEntity拼接出SQL语法来实现的,不再赘述。

以上就是xUtils3数据库模块的粗略讲解。

结语

所有自定义后的源代码可以参见我的GitHub:

https://github.com/DrkCore/xUtils3

猜你喜欢

转载自blog.csdn.net/DrkCore/article/details/51866495