Android xUtils3源码解析之数据库模块

千金一刻莫空度,老大无成空自伤。这篇文章主要讲述Android xUtils3源码解析之数据库模块相关的知识,希望能为你提供帮助。
本文已授权微信公众号《非著名程序员》原创首发, 转载请务必注明出处。
xUtils3源码解析系列 一. Android xUtils3源码解析之网络模块
二. Android xUtils3源码解析之图片模块
三. Android xUtils3源码解析之注解模块
四. Android xUtils3源码解析之数据库模块
配置数据库

DbManager.DaoConfig daoConfig = new DbManager.DaoConfig() .setDbName(" test.db" ) .setDbVersion(1) .setDbOpenListener(new DbManager.DbOpenListener() { @ Override public void onDbOpened(DbManager db) { // 开启WAL, 对写入加速提升巨大 db.getDatabase().enableWriteAheadLogging(); } }) .setDbUpgradeListener(new DbManager.DbUpgradeListener() { @ Override public void onUpgrade(DbManager db, int oldVersion, int newVersion) { ... } });

xUtil3支持数据库多库的配置, 使用不同的DaoConfig, 可以创建多个.db文件, 每个.db文件彼此独立。
数据库操作 初始化 由于xUtils3设计的是在需要使用数据库的时候, 才创建数据表。所以下文以save操作为例, 跟进初始化数据表的过程。示例代码:
DbManager db = x.getDb(daoConfig); Parent parent = new Parent(); parent.setName(" CSDN 一口仨馍" ); db.save(parent);

数据库的操作比较耗时, 真实应该异步执行。可以看到, xUtils3提供的数据库操作是非常简单的, 首先getDb, 之后调用save()方法即可。其中save方法接受List
创建数据库文件
x.getDb(daoConfig)
public final class x { public static DbManager getDb(DbManager.DaoConfig daoConfig) { return DbManagerImpl.getInstance(daoConfig); } }

这里只是简单的返回了一个DbManagerImpl实例, 看样子真正的初始化操作都在DbManagerImpl里。跟进。
public final class DbManagerImpl extends DbBase { private DbManagerImpl(DaoConfig config) { if (config = = null) { throw new IllegalArgumentException(" daoConfig may not be null" ); } this.daoConfig = config; this.allowTransaction = config.isAllowTransaction(); this.database = openOrCreateDatabase(config); DbOpenListener dbOpenListener = config.getDbOpenListener(); if (dbOpenListener != null) { dbOpenListener.onDbOpened(this); } }public synchronized static DbManager getInstance(DaoConfig daoConfig) {if (daoConfig = = null) {//使用默认配置 daoConfig = new DaoConfig(); }DbManagerImpl dao = DAO_MAP.get(daoConfig); if (dao = = null) { dao = new DbManagerImpl(daoConfig); DAO_MAP.put(daoConfig, dao); } else { dao.daoConfig = daoConfig; }// update the database if needed SQLiteDatabase database = dao.database; int oldVersion = database.getVersion(); int newVersion = daoConfig.getDbVersion(); if (oldVersion != newVersion) { if (oldVersion != 0) { DbUpgradeListener upgradeListener = daoConfig.getDbUpgradeListener(); if (upgradeListener != null) { upgradeListener.onUpgrade(dao, oldVersion, newVersion); } else { try { dao.dropDb(); } catch (DbException e) { LogUtil.e(e.getMessage(), e); } } } database.setVersion(newVersion); } return dao; } }

乍一看代码有些长, 其实也没做太多操作, 绝大部分是些缓存赋值相关的操作。这里注意两个地方
  1. 在数据库版本更新时, 如果没有设置DbUpgradeListener, 那么在更新的时候会直接删除旧表。
  2. 在获取DbManagerImpl实例的时候, 创建了数据库, 例如: “test.db”。如果指定了数据库的位置( 通过DaoConfig#setDbDir()) , 则在指定位置创建, 默认在data/data/package name/database/下创建。
由于返回的是DbManagerImpl实例, 所以实际调用的是DbManagerImpl.save()。
DbManagerImpl.save()
public final class DbManagerImpl extends DbBase { public void save(Object entity) throws DbException { try { // 开启事务 beginTransaction(); // 判断将要保存的是对象还是对象的集合 if (entity instanceof List) { // 向上转型为List List< ?> entities = (List< ?> ) entity; if (entities.isEmpty()) return; // 依据被注解的类获取数据表对应的包装类 TableEntity< ?> table = this.getTable(entities.get(0).getClass()); // 如果没有表则创建 createTableIfNotExist(table); // 遍历插入数据库 for (Object item : entities) { // 拼接sql语句, 执行数据库插入操作 execNonQuery(SqlInfoBuilder.buildInsertSqlInfo(table, item)); } } else { TableEntity< ?> table = this.getTable(entity.getClass()); createTableIfNotExist(table); execNonQuery(SqlInfoBuilder.buildInsertSqlInfo(table, entity)); } // 设置事务成功 setTransactionSuccessful(); } finally { // 结束事务 endTransaction(); } } }

接下来每行都有注释, 这些是我在看的过程中写下的。我只说贴代码的逻辑吧。先看下创建TableEntity
javaBean到TableEntity的转化
创建表的包装类
public final class TableEntity< T> { /*package*/ TableEntity(DbManager db, Class< T> entityType) throws Throwable { this.db = db; this.entityType = entityType; this.constructor = entityType.getConstructor(); this.constructor.setAccessible(true); // 被保存的类没有没Table注解, 这里会抛出NullPointerException。 // ps: 作者这里应该验证下为null的问题 Table table = entityType.getAnnotation(Table.class); // 获取表名 this.name = table.name(); // 获取创建表之后执行的SQL语句 this.onCreated = table.onCreated(); // 获取列Map,Map< 列的类型, 列的包装类> this.columnMap = TableUtils.findColumnMap(entityType); // 遍历查找列的包装类, 直到找到id列 for (ColumnEntity column : columnMap.values()) { if (column.isId()) { this.id = column; break; } } } }

这里涉及到Table注解, 从Table注解中获取表名。之后封装了一个Map, key为列名, value为列的包装类, 例如: Map
/* package */ final class TableUtils { static synchronized LinkedHashMap< String, ColumnEntity> findColumnMap(Class< ?> entityType) { LinkedHashMap< String, ColumnEntity> columnMap = new LinkedHashMap< String, ColumnEntity> (); addColumns2Map(entityType, columnMap); return columnMap; }private static void addColumns2Map(Class< ?> entityType, HashMap< String, ColumnEntity> columnMap) { // 递归出口 if (Object.class.equals(entityType)) return; try { // 获取表实体类的所有属性 Field[] fields = entityType.getDeclaredFields(); for (Field field : fields) { // 获取属性的修饰符 int modify = field.getModifiers(); // 修饰符不能是static或者transient if (Modifier.isStatic(modify) || Modifier.isTransient(modify)) { continue; } // 为下面判断属性有没有被Column注解修饰做准备 Column columnAnn = field.getAnnotation(Column.class); if (columnAnn != null) { // 判断属性是否支持转换 if (ColumnConverterFactory.isSupportColumnConverter(field.getType())) { // 新建列( 属性) 的包装类 ColumnEntity column = new ColumnEntity(entityType, field, columnAnn); if (!columnMap.containsKey(column.getName())) { columnMap.put(column.getName(), column); } } } } // 递归解析属性 addColumns2Map(entityType.getSuperclass(), columnMap); } catch (Throwable e) { LogUtil.e(e.getMessage(), e); } } }

创建列的包装类
/** * @ param entityType 实体类 * @ param field 属性 * @ param column 注解 */ /* 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(); // 是否自增, int、Integer、long、Long类型的主键, 默认自增 this.isAutoId = this.isId & & column.autoGen() & & ColumnUtils.isAutoIdType(fieldType); // String为例, 返回的是StringColumnConverter this.columnConverter = ColumnConverterFactory.getColumnConverter(fieldType); // 查找get方法。例如: 对于age属性, 查找getAge()方法 this.getMethod = ColumnUtils.findGetMethod(entityType, field); if (this.getMethod != null & & !this.getMethod.isAccessible()) { // 设置可反射访问 this.getMethod.setAccessible(true); } // 查找set方法 this.setMethod = ColumnUtils.findSetMethod(entityType, field); if (this.setMethod != null & & !this.setMethod.isAccessible()) { this.setMethod.setAccessible(true); } }

数据操作的时候不用每次都这么繁琐, 因为表格有tableMap缓存, 下次直接就能取出相应的表包装类TableEntity。下面跟进下创建表的过程。
创建数据表
createTableIfNotExist()
// 创建数据表 protected void createTableIfNotExist(TableEntity< ?> table) throws DbException { // 根据系统表SQLITE_MASTER判断指定表格是否存在 if (!table.tableIsExist()) { synchronized (table.getClass()) { // 表不存在 if (!table.tableIsExist()) { // 获取创建表格语句 SqlInfo sqlInfo = SqlInfoBuilder.buildCreateTableSqlInfo(table); // 执行创建表格语句 execNonQuery(sqlInfo); // 获取创建表格之后的语句, 例如: 可用于创建索引。PS:Table注解中的属性 String execAfterTableCreated = table.getOnCreated(); if (!TextUtils.isEmpty(execAfterTableCreated)) { // 执行创建表之后的语句 execNonQuery(execAfterTableCreated); } // 再次设置" 表已创建" 标志位 table.setCheckedDatabase(true); // 获取监听 TableCreateListener listener = this.getDaoConfig().getTableCreateListener(); if (listener != null) { // 调用创建表之后的监听 listener.onTableCreated(this, table); } } } } }

创建表的语句如下
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()); }

就是拼接了一条创建数据表的语句, 而且使用的是CREATE TABLE IF NOT EXISTS。最后执行下创建表的语句。
public void execNonQuery(SqlInfo sqlInfo) throws DbException { SQLiteStatement statement = null; try { statement = sqlInfo.buildStatement(database); statement.execute(); } catch (Throwable e) { throw new DbException(e); } finally { if (statement != null) { try { statement.releaseReference(); } catch (Throwable ex) { LogUtil.e(ex.getMessage(), ex); } } } }// 绑定SQL语句中" ?" 对应的值 public SQLiteStatement buildStatement(SQLiteDatabase database) { SQLiteStatement result = database.compileStatement(sql); if (bindArgs != null) { for (int i = 1; i < bindArgs.size() + 1; i+ + ) { KeyValue kv = bindArgs.get(i - 1); // 将属性的类型转换为数据库类型, 例如String 转换成 TEXT Object value = ColumnUtils.convert2DbValueIfNeeded(kv.value); if (value = = null) { result.bindNull(i); } else { ColumnConverter converter = ColumnConverterFactory.getColumnConverter(value.getClass()); ColumnDbType type = converter.getColumnDbType(); switch (type) { case INTEGER: result.bindLong(i, ((Number) value).longValue()); break; case REAL: result.bindDouble(i, ((Number) value).doubleValue()); break; case TEXT: result.bindString(i, value.toString()); break; case BLOB: result.bindBlob(i, (byte[]) value); break; default: result.bindNull(i); break; } // end switch } } } return result; }

増 save在上述初始化的基础上操作, 真正执行save操作的地方在于execNonQuery(SqlInfoBuilder.buildInsertSqlInfo(table, item))。和创建表的过程类似, 使用SqlInfoBuilder.buildInsertSqlInfo() 构建一条SQL插入语句, 之后执行。跟进看下。
public static SqlInfo buildInsertSqlInfo(TableEntity< ?> table, Object entity) throws DbException {List< KeyValue> keyValueList = entity2KeyValueList(table, entity); if (keyValueList.size() = = 0) return null; SqlInfo result = new SqlInfo(); String sql = INSERT_SQL_CACHE.get(table); if (sql = = null) { StringBuilder builder = new StringBuilder(); builder.append(" INSERT INTO " ); builder.append(" \\" " ).append(table.getName()).append(" \\" " ); builder.append(" (" ); for (KeyValue kv : keyValueList) { builder.append(" \\" " ).append(kv.key).append(" \\" " ).append(' ,' ); } builder.deleteCharAt(builder.length() - 1); builder.append(" ) VALUES (" ); int length = keyValueList.size(); for (int i = 0; i < length; i+ + ) { builder.append(" ?," ); } builder.deleteCharAt(builder.length() - 1); builder.append(" )" ); sql = builder.toString(); result.setSql(sql); result.addBindArgs(keyValueList); INSERT_SQL_CACHE.put(table, sql); } else { result.setSql(sql); result.addBindArgs(keyValueList); }return result; }

这个方法的作用就是拼接SQL语句: INSERT INTO “tableName”( “key1”,”key2”) VALUES (?,?), 之后存入缓存, 下次直接从缓存中取出上面拼接的SQL语句。执行的过程和创建表是同一个方法, 不再赘述。
删 示例代码:
DbManager db = x.getDb(daoConfig); db.delete(Parent.class);

@ Override public void delete(Class< ?> entityType) throws DbException { delete(entityType, null); }@ Override public int delete(Class< ?> entityType, WhereBuilder whereBuilder) throws DbException { TableEntity< ?> table = this.getTable(entityType); if (!table.tableIsExist()) return 0; int result = 0; try { beginTransaction(); result = executeUpdateDelete(SqlInfoBuilder.buildDeleteSqlInfo(table, whereBuilder)); setTransactionSuccessful(); } finally { endTransaction(); } return result; }

因为使用WhereBuilder涉及到查找, 而查找的源码还没看, 所以这里以删除表中所有数据为例。
创建删除语句
public static SqlInfo buildDeleteSqlInfo(TableEntity< ?> table, WhereBuilder whereBuilder) throws DbException { StringBuilder builder = new StringBuilder(" DELETE FROM " ); builder.append(" \\" " ).append(table.getName()).append(" \\" " ); if (whereBuilder != null & & whereBuilder.getWhereItemSize() > 0) { builder.append(" WHERE " ).append(whereBuilder.toString()); }return new SqlInfo(builder.toString()); }

因为这里的WhereBuilder为null, 所以返回的是DELETE FROM " tableName" , 即删除表中所有数据。
改 示例代码:
DbManager db = x.getDb(daoConfig); Parent parent = new Parent(); parent.setName(" CSDN 一口仨馍" ); db.update(parent, " name" );

update后面照样支持WhereBuilder甚至指定列名, 为了方便分析主要流程, 这里就简单点来。update方法就不贴了, 和前面save过程几乎一样, 区别主要在执行的SQL语句不同, 下面主要看下更新语句的构建。
public static SqlInfo buildUpdateSqlInfo(TableEntity< ?> table, Object entity, String... updateColumnNames) throws DbException {List< KeyValue> keyValueList = entity2KeyValueList(table, entity); if (keyValueList.size() = = 0) return null; HashSet< String> updateColumnNameSet = null; if (updateColumnNames != null & & updateColumnNames.length > 0) { updateColumnNameSet = new HashSet< String> (updateColumnNames.length); Collections.addAll(updateColumnNameSet, updateColumnNames); }ColumnEntity id = table.getId(); Object idValue = id.getColumnValue(entity); if (idValue = = null) { throw new DbException(" this entity[" + table.getEntityType() + " ]' s id value is null" ); }SqlInfo result = new SqlInfo(); StringBuilder builder = new StringBuilder(" UPDATE " ); builder.append(" \\" " ).append(table.getName()).append(" \\" " ); builder.append(" SET " ); for (KeyValue kv : keyValueList) { if (updateColumnNameSet = = null || updateColumnNameSet.contains(kv.key)) { builder.append(" \\" " ).append(kv.key).append(" \\" " ).append(" = ?," ); result.addBindArg(kv); } } builder.deleteCharAt(builder.length() - 1); builder.append(" WHERE " ).append(WhereBuilder.b(id.getName(), " = " , idValue)); result.setSql(builder.toString()); return result; }

倒数第五行表明是依据对象主键的值来查找数据表中对应的行, 使用更新语句, 数据库实体类( JavaBean) 被Column修饰的属性中必须要有isId修饰, 而且还必须有值, 否则会抛出DbException。拼接出的SQL语句类似于UPDATE " tableName" SET " name" = ?," age" = ? WHERE " ID" = ' 1' 。其中的? 表示占位符, 在执行前被替换成具体的值。
查 示例代码:
DbManager db = x.getDb(daoConfig); WhereBuilder whereBuilder = WhereBuilder.b(" name" ," = " ," 一口仨馍" ).and(" age" ," = " ," 18" ); db.selector(Parent.class).where(whereBuilder).findAll();

WhereBuilder的作用是构建查找的SQL语句后半段。例如在select * from parent where " name" = ' 一口仨馍' and " age" = ' 18' 中, WhereBuilder返回的字符串是”name” = ‘一口仨馍’ and “age” = ‘18’。
db.selector()
@ Override public < T> Selector< T> selector(Class< T> entityType) throws DbException { return Selector.from(this.getTable(entityType)); }static < T> Selector< T> from(TableEntity< T> table) { return new Selector< T> (table); }private Selector(TableEntity< T> table) { this.table = table; }

new了个Selector对象, 除了赋值, 啥也木干。
Selector.findAll()
public List< T> findAll() throws DbException { if (!table.tableIsExist()) return null; List< T> result = null; Cursor cursor = table.getDb().execQuery(this.toString()); if (cursor != null) { try { result = new ArrayList< T> (); while (cursor.moveToNext()) { T entity = CursorUtils.getEntity(table, cursor); result.add(entity); } } catch (Throwable e) { throw new DbException(e); } finally { IOUtil.closeQuietly(cursor); } } return result; }

乍一看execQuery里的参数吓我一跳, 传个this.toString()是什么鬼啊! !
Selector.findAll()
public String toString() { StringBuilder result = new StringBuilder(); result.append(" SELECT " ); result.append(" *" ); result.append(" FROM " ).append(" \\" " ).append(table.getName()).append(" \\" " ); if (whereBuilder != null & & whereBuilder.getWhereItemSize() > 0) { result.append(" WHERE " ).append(whereBuilder.toString()); } if (orderByList != null & & orderByList.size() > 0) { result.append(" ORDER BY " ); for (OrderBy orderBy : orderByList) { result.append(orderBy.toString()).append(' ,' ); } result.deleteCharAt(result.length() - 1); } if (limit > 0) { result.append(" LIMIT " ).append(limit); result.append(" OFFSET " ).append(offset); } return result.toString(); }

在这里拼接的SQL语句( 手动冷漠脸) 。可以看到查找也支持ORDER BY、LIMIT和OFFSET关键字。
总结 xUtils3的数据库模块, 采用Table和Column注解修饰JavaBean, 初始化的时候(实际是调用具体操作才会检查是否已经初始化, 没有初始化才会执行初始化操作)会依据注解实例化相应的TableEntity和ColumnEntity并添加进缓存, 执行增删改查时依据TableEntity和ColumnEntity拼接相应的SQL语句并执行。
原来没有看过ORM框架的源码, 外加上自己数据库也渣的一匹, 以为ORM框架多难了, 以至于最后才分析xUtils3中的数据库模块。愿意看源码, 实际稍微花点时间也能看出个大概。没经历会觉得似乎难以逾越, 实际上也没有想象的那么难~
【Android xUtils3源码解析之数据库模块】xUtils3四大模块到此就全部解析结束了。加上写作, 前后大概花了一周工作时间, 基本上把类翻了几遍, 得益于框架功能比较全面, 所以收获还是蛮多的。不敢说自己完全掌握了xUtils3的精髓, 至少弄清了xUtils3的许多设计思想, 而且从具体的编码中get到不少小技能。总体来说还是比较满意的。如果您看完四篇博客之后, 仍有很多疑惑, 建议对着博文思路同步阅读源码, 实在有不好解决的问题, 可以在下面留言, 我尽量解答。感谢悉心阅读到最后~

    推荐阅读