Android xUtils3原始碼解析之資料庫模組
本文已授權微信公眾號《非著名程式設計師》原創首發,轉載請務必註明出處。
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到不少小技能。總體來說還是比較滿意的。如果您看完四篇部落格之後,仍有很多疑惑,建議對著博文思路同步閱讀原始碼,實在有不好解決的問題,可以在下面留言,我儘量解答。感謝悉心閱讀到最後~