1. 程式人生 > >Android xUtils3原始碼解析之資料庫模組

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;
    }
}

乍一看程式碼有些長,其實也沒做太多操作,絕大部分是些快取賦值相關的操作。這裡注意兩個地方

  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中的資料庫模組。願意看原始碼,實際稍微花點時間也能看出個大概。沒經歷會覺得似乎難以逾越,實際上也沒有想象的那麼難~

xUtils3四大模組到此就全部解析結束了。加上寫作,前後大概花了一週工作時間,基本上把類翻了幾遍,得益於框架功能比較全面,所以收穫還是蠻多的。不敢說自己完全掌握了xUtils3的精髓,至少弄清了xUtils3的許多設計思想,而且從具體的編碼中get到不少小技能。總體來說還是比較滿意的。如果您看完四篇部落格之後,仍有很多疑惑,建議對著博文思路同步閱讀原始碼,實在有不好解決的問題,可以在下面留言,我儘量解答。感謝悉心閱讀到最後~