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數據庫管理類。

 

 

來自:http://www.jianshu.com/p/0d3cbe6278fb

 

 本文由用戶 MaxEllingto 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!