Android xUtils3源码解析之数据库模块
Posted 一口仨馍
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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;
乍一看代码有些长,其实也没做太多操作,绝大部分是些缓存赋值相关的操作。这里注意两个地方
- 在数据库版本更新时,如果没有设置DbUpgradeListener,那么在更新的时候会直接删除旧表。
- 在获取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中的数据库模块。愿意看源码,实际稍微花点时间也能看出个大概。没经历会觉得似乎难以逾越,实际上也没有想象的那么难~
xUtils3四大模块到此就全部解析结束了。加上写作,前后大概花了一周工作时间,基本上把类翻了几遍,得益于框架功能比较全面,所以收获还是蛮多的。不敢说自己完全掌握了xUtils3的精髓,至少弄清了xUtils3的许多设计思想,而且从具体的编码中get到不少小技能。总体来说还是比较满意的。如果您看完四篇博客之后,仍有很多疑惑,建议对着博文思路同步阅读源码,实在有不好解决的问题,可以在下面留言,我尽量解答。感谢悉心阅读到最后~
以上是关于Android xUtils3源码解析之数据库模块的主要内容,如果未能解决你的问题,请参考以下文章