千金一刻莫空度,老大无成空自伤。这篇文章主要讲述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.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到不少小技能。总体来说还是比较满意的。如果您看完四篇博客之后, 仍有很多疑惑, 建议对着博文思路同步阅读源码, 实在有不好解决的问题, 可以在下面留言, 我尽量解答。感谢悉心阅读到最后~
推荐阅读
- Android xUtils3源码解析之注解模块
- 值得你关注的Android8.0(Android O)上的重要变化
- Google App Engine10年,支持更多你喜欢的编程语言
- Android studio 2.3安装遇到的问题
- PhoneGap 获得APP的VersionName
- 学习嵌入式开发板的Android平台体系结构和源码结构
- Android利用Fiddler进行网络数据抓包
- h5开发app之在线生成二维码
- Tornado 高并发源码分析之三--- Application 对象