Android | 分析greenDAO 3.2實現原理

將項目從greenDAO從2.x版本升級到最新的3.2版本,最大變化是可以用注解代替以前的java生成器。實現這點,需要引入相應的gradle插件,具體配置參考官網。


圖1

圖1是從官網盜來的主結構圖,注解Entity后,只需要build工程,DaoMaster、DaoSession和對應的Dao文件就會自動生成。分析greenDAO的實現原理,將會依照這幅圖的路線入手,分析各個部分的作用,最重要是研究清楚greenDAO是怎樣調用數據庫的CRUD。

DaoMaster

DaoMaster是greenDAO的入口,它的父類AbstractDaoMaster維護了數據庫重要的參數,分別是實例、版本和Dao的信息。

//AbstractDaoMaster的參數
protected final Database db;
protected final int schemaVersion;
protected final Map<Class<? extends AbstractDao<?, ?>>, DaoConfig> daoConfigMap;

創建DaoMaster需要傳入Android原生數據庫SQLiteDatabase的實例,接著傳遞給StandardDatabase:

//DaoMaster的構造函數
public DaoMaster(SQLiteDatabase db) {
    this(new StandardDatabase(db));
}

public DaoMaster(Database db) {
    super(db, SCHEMA_VERSION);
    registerDaoClass(UserDao.class);
}
public class StandardDatabase implements Database {
    private final SQLiteDatabase delegate;

    public StandardDatabase(SQLiteDatabase delegate) {
        this.delegate = delegate;
    }

    @Override
    public void execSQL(String sql) throws SQLException {
        delegate.execSQL(sql);
    }

    //其余省略
}

StandardDatabase實現了Database接口,方法都是SQLiteDatabase提供的,所以SQLite的操作都委托給AbstractDaoMaster的參數db去調用。

protected void registerDaoClass(Class<? extends AbstractDao<?, ?>> daoClass) {
    DaoConfig daoConfig = new DaoConfig(db, daoClass);
    daoConfigMap.put(daoClass, daoConfig);
}

所有Dao都需要創建DaoConfig,通過AbstractDaoMaster的registerDaoClass注冊進daoConfigMap,供后續使用。

數據庫升級

DbUpgradeHelper helper = new DbUpgradeHelper(context, dbName, null);
DaoMaster daoMaster = new DaoMaster(helper.getReadableDatabase());

生成數據庫可以使用類似上面的語句,通過getReadableDatabase獲取數據庫實例傳遞給DaoMaster。DbUpgradeHelper是自定義對象,向上查找父類,可以找到熟悉SQLiteOpenHelper。

DbUpgradeHelper --> DaoMaster.OpenHelper --> DatabaseOpenHelper --> SQLiteOpenHelper

SQLiteOpenHelper提供了onCreate、onUpgrade、onOpen等空方法。繼承SQLiteOpenHelper,各層添加了不同的功能:

  • DatabaseOpenHelper:使用EncryptedHelper加密數據庫;
  • DaoMaster.OpenHelper:onCreate時調用createAllTables,繼而調用各Dao的createTable;
  • DbUpgradeHelper:自定義,一般用來處理數據庫升級。

DatabaseOpenHelper和DaoMaster.OpenHelper的代碼簡單,就不貼了。數據庫升級涉及到表結構和表數據的變更,需要判斷版本號處理各版本的差異,處理方法可以參考下面的DbUpgradeHelper:

public class DbUpgradeHelper extends DaoMaster.OpenHelper {
    public DbUpgradeHelper(Context context, String name, SQLiteDatabase.CursorFactory factory) {
        super(context, name, factory);
    }

    @Override
    public void onUpgrade(Database db, int oldVersion, int newVersion) {
        if (oldVersion == newVersion) {
            LogUtils.d("數據庫是最新版本" + oldVersion + ",不需要升級");
            return;
        }
        LogUtils.d("數據庫從版本" + oldVersion + "升級到版本" + newVersion);
        switch (oldVersion) {
            case 1:
                String sql = "";
                db.execSQL(sql);
            case 2:
            default:
                break;
        }
    }
}

數據庫變更語句的執行,可以利用switch-case沒有break時連續執行的特性,實現數據庫從任意舊版本升級到新版本。

DaoSession

public DaoSession newSession() {    
    return new DaoSession(db, IdentityScopeType.Session, daoConfigMap);
}
public DaoSession newSession(IdentityScopeType type) {    
    return new DaoSession(db, type, daoConfigMap);
}

DaoSession通過調用DaoMaster的newSession創建。對同一個數據庫,可以根據需要創建多個Session分別操作。參數IdentityScopeType涉及到是否啟用greenDAO的緩存機制,后文會進一步分析。

 public DaoSession(Database db, IdentityScopeType type, Map<Class<? extends AbstractDao<?, ?>>, DaoConfig> daoConfigMap) {
    super(db);
    userDaoConfig = daoConfigMap.get(UserDao.class).clone();
    userDaoConfig.initIdentityScope(type);
    
    userDao = new UserDao(userDaoConfig, this);
    
    registerDao(User.class, userDao);
}

創建DaoSession時,將會獲取每個Dao的DaoConfig,這是從之前的daoConfigMap中直接clone出來。并且Dao還需要在DaoSession注冊,registerDao在父類AbstractDaoSession中的實現:

public class AbstractDaoSession {
    private final Database db;
    private final Map<Class<?>, AbstractDao<?, ?>> entityToDao;

    public AbstractDaoSession(Database db) {
        this.db = db;
        this.entityToDao = new HashMap<Class<?>, AbstractDao<?, ?>>();
    }

    protected <T> void registerDao(Class<T> entityClass, AbstractDao<T, ?> dao) {
        entityToDao.put(entityClass, dao);
    }

    /** Convenient call for {@link AbstractDao#insert(Object)}. */
    public <T> long insert(T entity) {
        @SuppressWarnings("unchecked")
        AbstractDao<T, ?> dao = (AbstractDao<T, ?>) getDao(entity.getClass());
        return dao.insert(entity);
    }

    public AbstractDao<?, ?> getDao(Class<? extends Object> entityClass) {
        AbstractDao<?, ?> dao = entityToDao.get(entityClass);
        if (dao == null) {
            throw new DaoException("No DAO registered for " + entityClass);
        }
        return dao;
    }
    
    //其余略
}

registerDao將使用Map維持Class->Dao的關系。AbstractDaoSession提供了insert、update、delete等泛型方法,支持對數據庫表的CURD。原理就是從Map獲取對應的Dao,再調用Dao對應的操作方法。

Dao

每個Dao都有一個對應的DaoConfig,創建時通過反射機制,為Dao準備好TableName、Property、Pk等一系列具體的參數。所有Dao都繼承自AbstractDao,表的通用操作方法就定義在這里。

表的新增和刪除
public static void createTable(Database db, boolean ifNotExists) {
    String constraint = ifNotExists? "IF NOT EXISTS ": "";
    db.execSQL("CREATE TABLE " + constraint + "\"USER\" (" + //
            "\"ID\" INTEGER PRIMARY KEY ," + // 0: id
            "\"USER_NAME\" TEXT NOT NULL ," + // 1: user_name
            "\"REAL_NAME\" TEXT NOT NULL ," + // 2: real_name
            "\"EMAIL\" TEXT," + // 3: email
            "\"MOBILE\" TEXT," + // 4: mobile
            "\"UPDATE_AT\" INTEGER," + // 5: update_at
            "\"DELETE_AT\" INTEGER);"); // 6: delete_at
}

public static void dropTable(Database db, boolean ifExists) {
    String sql = "DROP TABLE " + (ifExists ? "IF EXISTS " : "") + "\"USER\"";
    db.execSQL(sql);
}

簡單的先講,每個Dao里都有表的新增和刪除方法,很直接地拼Sql執行,注意傳參可以支持判斷表是否存在。

SQLiteStatement

下面開始研究greenDAO如何調用SQLite的CRUD,首先要理解什么是ORM。簡單來說,SQLite是一個關系數據庫,Java用的是對象,對象和關系之間的數據交互需要一個東西去轉換,這就是greenDAO的作用。轉換過程也不復雜,數據庫的列對應Java對象里的參數就行。

SQLiteStatement是封裝了對數據庫操作和相關數據的對象

SQLiteStatement由Android提供,它的父類SQLiteProgram有兩個重要的參數,是執行數據庫操作前要提供的:

private final String mSql;   //操作數據庫用的Sql
private final Object[] mBindArgs;  //列和數據值的關系

參數mBindArgs描述了數據庫列和數據的關系,SQLiteStatement為不同數據類型提供bind方法,結果保存在mBindArgs,最終交給SQLite處理。

和StandardDatabase一樣,SQLiteStatement的方法委托給DatabaseStatement調用,所以greenDAO操作數據庫前需要先獲取DatabaseStatement。

生成Sql

sql的獲取需要用到TableStatements,它的對象維護在DaoConfig里,由它負責創建和緩存DatabaseStatement,下面是insert的DatabaseStatement獲取過程:

public DatabaseStatement getInsertStatement() {
    if (insertStatement == null) {
        String sql = SqlUtils.createSqlInsert("INSERT INTO ", tablename, allColumns);
        DatabaseStatement newInsertStatement = db.compileStatement(sql);
        synchronized (this) {
            if (insertStatement == null) {
                insertStatement = newInsertStatement;
            }
        }
        if (insertStatement != newInsertStatement) {
            newInsertStatement.close();
        }
    }
    return insertStatement;
}

sql語句通過SqlUtils工具拼接,由Database調用compileStatement將sql存入DatabaseStatement。可知,DatabaseStatement的實現類是StandardDatabaseStatement:

 @Override
public DatabaseStatement compileStatement(String sql) {
    return new StandardDatabaseStatement(delegate.compileStatement(sql));
}

拼接出來的sql是包括表名和字段名的通用插入語句,生成的DatabaseStatement是可以復用的,所以第一次獲取的DatabaseStatement會緩存在insertStatement參數,下次直接使用。

其他例如count、update、delete等操作獲取DatabaseStatement原理是一樣的,就不介紹了。

執行insert

insert和insertOrReplace都調用了executeInsert,區別之處是入參DatabaseStatement的獲取方法不同。

private long executeInsert(T entity, DatabaseStatement stmt, boolean setKeyAndAttach) {
    long rowId;
    if (db.isDbLockedByCurrentThread()) {
        rowId = insertInsideTx(entity, stmt);
    } else {
        // Do TX to acquire a connection before locking the stmt to avoid deadlocks
        db.beginTransaction();
        try {
            rowId = insertInsideTx(entity, stmt);
            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
        }
    }
    if (setKeyAndAttach) {
        updateKeyAfterInsertAndAttach(entity, rowId, true);
    }
    return rowId;
}

 private long insertInsideTx(T entity, DatabaseStatement stmt) {
    synchronized (stmt) {
        if (isStandardSQLite) {
            SQLiteStatement rawStmt = (SQLiteStatement) stmt.getRawStatement();
            bindValues(rawStmt, entity);
            return rawStmt.executeInsert();
        } else {
            bindValues(stmt, entity);
            return stmt.executeInsert();
        }
    }
}

當前線程獲取數據庫鎖的情況下,直接執行insert操作即可,否則需要使用事務保證操作的原子性和一致性。insertInsideTx方法里,isStandardSQLite判斷當前是不是SQLite數據庫(留下擴展的伏筆?)。關鍵來了,獲取原始的SQLiteStatement,調用了bindValues。

@Override
protected final void bindValues(SQLiteStatement stmt, User entity) {
    stmt.clearBindings();

    Long id = entity.getId();
    if (id != null) {
        stmt.bindLong(1, id);
    }
    stmt.bindString(2, entity.getUser_name());
    stmt.bindString(3, entity.getReal_name());
}

bindValues由各自的Dao實現,描述index和數據的關系,最終保存進mBindArgs。到這里,應該就能明白greenDao的核心作用。greenDao將我們熟悉的對象,轉換成sql語句和執行參數,再提交SQLite執行。

update和delete的操作和insert大同小異,推薦自行分析。

數據Load與緩存機制

userDaoConfig = daoConfigMap.get(UserDao.class).clone();
userDaoConfig.initIdentityScope(type);

創建DaoSession并獲取DaoConfig時,調用了initIdentityScope,這里是greenDAO緩存的入口。

 public void initIdentityScope(IdentityScopeType type) {
    if (type == IdentityScopeType.None) {
        identityScope = null;
    } else if (type == IdentityScopeType.Session) {
        if (keyIsNumeric) {
            identityScope = new IdentityScopeLong();
        } else {
            identityScope = new IdentityScopeObject();
        }
    } else {
        throw new IllegalArgumentException("Unsupported type: " + type);
    }
}

DaoSession的入參IdentityScopeType現在可以解釋了,None時不啟用緩存,Session時啟用緩存。緩存接口IdentityScope根據主鍵是不是數字,分為兩個實現類IdentityScopeLong和IdentityScopeObject。兩者的實現類似,選IdentityScopeObject來研究。

private final HashMap<K, Reference<T>> map;

緩存機制很簡單,一個保存pk和entity關系的Map,再加上get、put、detach、remove、clear等操作方法。其中get、put方法分無鎖版本和加鎖版本,對應當前線程是否獲得鎖的情況。

map.put(key, new WeakReference<T>(entity));

注意,將entity加入Map時使用了弱引用,資源不足時GC會主動回收對象。


下面是load方法,看緩存扮演了什么角色。

public T load(K key) {
    assertSinglePk();
    if (key == null) {
        return null;
    }
    //1
    if (identityScope != null) {
        T entity = identityScope.get(key);
        if (entity != null) {
            return entity;
        }
    }
   //2
    String sql = statements.getSelectByKey();
    String[] keyArray = new String[]{key.toString()};
    Cursor cursor = db.rawQuery(sql, keyArray);
    return loadUniqueAndCloseCursor(cursor);
}

在執行真正的數據加載前,標記1處先查找緩存,如果有就直接返回,無就去查數據庫。標記2處準備sql語句和參數,交給rawQuery查詢,得到Cursor。

用主鍵查詢,只可能有一個結果,調用loadUnique,最終調用loadCurrent。loadCurrent會先嘗試從緩存里獲取數據,代碼很長,分析identityScopeLong != null這段就可以體現原理:

    if (identityScopeLong != null) {
        if (offset != 0) {
            // Occurs with deep loads (left outer joins)
            if (cursor.isNull(pkOrdinal + offset)) {
                return null;
            }
        }

        long key = cursor.getLong(pkOrdinal + offset);
        T entity = lock ? identityScopeLong.get2(key) : identityScopeLong.get2NoLock(key);
        if (entity != null) {
            return entity;
        } else {
            entity = readEntity(cursor, offset);
            attachEntity(entity);
            if (lock) {
                identityScopeLong.put2(key, entity);
            } else {
                identityScopeLong.put2NoLock(key, entity);
            }
            return entity;
        }
    } 
protected final void attachEntity(K key, T entity, boolean lock) {
    attachEntity(entity);
    if (identityScope != null && key != null) {
        if (lock) {
            identityScope.put(key, entity);
        } else {
            identityScope.putNoLock(key, entity);
        }
    }
}

AbstractDao同時維護identityScope和identityScopeLong對象,entity會同時put進它們兩者。如果主鍵是數字,優先從identityScopeLong獲取緩存,速度更快;如果主鍵不是數字,就嘗試從IdentityScopeObject獲取;如果沒有緩存,只能通過游標讀取數據庫。

數據Query

QueryBuilder使用鏈式結構構建Query,靈活地支持where、or、join等約束的添加。具體代碼是簡單的數據操作,沒必要細說,數據最終會拼接成sql。Query的unique操作和上面的load一樣,而list操作在調用rawQuery獲取Cursor后,最終調用AbstractDao的loadAllFromCursor:

protected List<T> loadAllFromCursor(Cursor cursor) {
    int count = cursor.getCount();
    if (count == 0) {
        return new ArrayList<T>();
    }
    List<T> list = new ArrayList<T>(count);
    //1
    CursorWindow window = null;
    boolean useFastCursor = false;
    if (cursor instanceof CrossProcessCursor) {
        window = ((CrossProcessCursor) cursor).getWindow();
        if (window != null) { // E.g. Robolectric has no Window at this point
            if (window.getNumRows() == count) {
                cursor = new FastCursor(window);
                useFastCursor = true;
            } else {
                DaoLog.d("Window vs. result size: " + window.getNumRows() + "/" + count);
            }
        }
    }
    //2
    if (cursor.moveToFirst()) {
        if (identityScope != null) {
            identityScope.lock();
            identityScope.reserveRoom(count);
        }
        try {
            if (!useFastCursor && window != null && identityScope != null) {
                loadAllUnlockOnWindowBounds(cursor, window, list);
            } else {
                do {
                    list.add(loadCurrent(cursor, 0, false));
                } while (cursor.moveToNext());
            }
        } finally {
            if (identityScope != null) {
                identityScope.unlock();
            }
        }
    }
    return list;
}

標記1處嘗試使用Android提供的CursorWindow以獲取一個更快的Cursor。SQLiteDatabase將查詢結果保存在CursorWindow所指向的共享內存中,然后通過Binder把這片共享內存傳遞到查詢端。Cursor不是本文要討論的內容,詳情可以參考其他資料。

標記2處通過移動Cursor,利用loadCurrent進行批量操作,結果保存在List中返回。

一對一和一對多

greenDAO支持一對一和一對多,但并不支持多對多。

@ToOne(joinProperty = "father_key")
private CheckItem father;

@Generated
public CheckItem getFather() {
    String __key = this.father_key;
    if (father__resolvedKey == null || father__resolvedKey != __key) {
        __throwIfDetached();
        CheckItemDao targetDao = daoSession.getCheckItemDao();
        CheckItem fatherNew = targetDao.load(__key);
        synchronized (this) {
            father = fatherNew;
            father__resolvedKey = __key;
        }
    }
    return father;
}

一對一,使用@ToOne標記,greenDAO會自動生成get方法,并標記為@Generated,代表是自動生成的,不要動代碼。get方法利用主鍵load出對應的entity即可。

@ToMany(joinProperties = {
    @JoinProperty(name = "key", referencedName = "father_key")
})
private List<CheckItem> children;

一對多的形式和一對一類似,使用@ToMany標記,get方法是利用QueryBuild查詢目標List,代碼簡單就不貼了。

后記

到此,過了一遍greenDAO主要功能,還有些高級特性用到再研究吧。縱觀下來,greenDAO還是挺簡單的,但也很實用,簡化了數據庫調用的復雜度,具體的執行就交給原生的Android數據庫管理類。

歡迎留言交流,如果有紕漏,請通知我,謝謝。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,362評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,577評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,486評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,852評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,600評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,944評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,108評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,652評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,385評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,616評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,798評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,205評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,537評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,334評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,570評論 2 379

推薦閱讀更多精彩內容