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

【转载请注明出处】
作者: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

你可能感兴趣的:(Android)