Android | 分析greenDAO 3.2实现原理
MaxEllingto
8年前
<p>将项目从greenDAO从2.x版本升级到最新的3.2版本,最大变化是可以用注解代替以前的java生成器。实现这点,需要引入相应的gradle插件,具体配置参考官网。</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/a31a136ab6f109933bdf94167f5610d7.png"></p> <p style="text-align:center">图1</p> <p>图1是从官网盗来的主结构图,注解Entity后,只需要build工程,DaoMaster、DaoSession和对应的Dao文件就会自动生成。分析greenDAO的实现原理,将会依照这幅图的路线入手,分析各个部分的作用,最重要是研究清楚greenDAO是怎样调用数据库的CRUD。</p> <h3><strong>DaoMaster</strong></h3> <p>DaoMaster是greenDAO的入口,它的父类AbstractDaoMaster维护了数据库重要的参数,分别是实例、版本和Dao的信息。</p> <pre> <code class="language-java">//AbstractDaoMaster的参数 protected final Database db; protected final int schemaVersion; protected final Map<Class<? extends AbstractDao<?, ?>>, DaoConfig> daoConfigMap;</code></pre> <p>创建DaoMaster需要传入Android原生数据库SQLiteDatabase的实例,接着传递给StandardDatabase:</p> <pre> <code class="language-java">//DaoMaster的构造函数 public DaoMaster(SQLiteDatabase db) { this(new StandardDatabase(db)); } public DaoMaster(Database db) { super(db, SCHEMA_VERSION); registerDaoClass(UserDao.class); }</code></pre> <pre> <code class="language-java">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); } //其余省略 }</code></pre> <p>StandardDatabase实现了Database接口,方法都是SQLiteDatabase提供的,所以SQLite的操作都委托给AbstractDaoMaster的参数db去调用。</p> <pre> <code class="language-java">protected void registerDaoClass(Class<? extends AbstractDao<?, ?>> daoClass) { DaoConfig daoConfig = new DaoConfig(db, daoClass); daoConfigMap.put(daoClass, daoConfig); }</code></pre> <p>所有Dao都需要创建DaoConfig,通过AbstractDaoMaster的registerDaoClass注册进daoConfigMap,供后续使用。</p> <h3><strong>数据库升级</strong></h3> <pre> <code class="language-java">DbUpgradeHelper helper = new DbUpgradeHelper(context, dbName, null); DaoMaster daoMaster = new DaoMaster(helper.getReadableDatabase());</code></pre> <p>生成数据库可以使用类似上面的语句,通过getReadableDatabase获取数据库实例传递给DaoMaster。DbUpgradeHelper是自定义对象,向上查找父类,可以找到熟悉SQLiteOpenHelper。</p> <p>DbUpgradeHelper --> DaoMaster.OpenHelper --> DatabaseOpenHelper --> SQLiteOpenHelper</p> <p>SQLiteOpenHelper提供了onCreate、onUpgrade、onOpen等空方法。继承SQLiteOpenHelper,各层添加了不同的功能:</p> <ul> <li>DatabaseOpenHelper:使用EncryptedHelper加密数据库;</li> <li>DaoMaster.OpenHelper:onCreate时调用createAllTables,继而调用各Dao的createTable;</li> <li>DbUpgradeHelper:自定义,一般用来处理数据库升级。</li> </ul> <p>DatabaseOpenHelper和DaoMaster.OpenHelper的代码简单,就不贴了。数据库升级涉及到表结构和表数据的变更,需要判断版本号处理各版本的差异,处理方法可以参考下面的DbUpgradeHelper:</p> <pre> <code class="language-java">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; } } }</code></pre> <p>数据库变更语句的执行,可以利用switch-case没有break时连续执行的特性,实现数据库从任意旧版本升级到新版本。</p> <h3><strong>DaoSession</strong></h3> <pre> <code class="language-java">public DaoSession newSession() { return new DaoSession(db, IdentityScopeType.Session, daoConfigMap); } public DaoSession newSession(IdentityScopeType type) { return new DaoSession(db, type, daoConfigMap); }</code></pre> <p>DaoSession通过调用DaoMaster的newSession创建。对同一个数据库,可以根据需要创建多个Session分别操作。参数IdentityScopeType涉及到是否启用greenDAO的缓存机制,后文会进一步分析。</p> <pre> <code class="language-java">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); }</code></pre> <p>创建DaoSession时,将会获取每个Dao的DaoConfig,这是从之前的daoConfigMap中直接clone出来。并且Dao还需要在DaoSession注册,registerDao在父类AbstractDaoSession中的实现:</p> <pre> <code class="language-java">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; } //其余略 }</code></pre> <p>registerDao将使用Map维持Class->Dao的关系。AbstractDaoSession提供了insert、update、delete等泛型方法,支持对数据库表的CURD。原理就是从Map获取对应的Dao,再调用Dao对应的操作方法。</p> <h3><strong>Dao</strong></h3> <p>每个Dao都有一个对应的DaoConfig,创建时通过反射机制,为Dao准备好TableName、Property、Pk等一系列具体的参数。所有Dao都继承自AbstractDao,表的通用操作方法就定义在这里。</p> <p><strong>表的新增和删除</strong></p> <pre> <code class="language-java">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); }</code></pre> <p>简单的先讲,每个Dao里都有表的新增和删除方法,很直接地拼Sql执行,注意传参可以支持判断表是否存在。</p> <p><strong>SQLiteStatement</strong></p> <p>下面开始研究greenDAO如何调用SQLite的CRUD,首先要理解什么是ORM。简单来说,SQLite是一个关系数据库,Java用的是对象,对象和关系之间的数据交互需要一个东西去转换,这就是greenDAO的作用。转换过程也不复杂,数据库的列对应Java对象里的参数就行。</p> <p>SQLiteStatement是封装了对数据库操作和相关数据的对象</p> <p>SQLiteStatement由Android提供,它的父类SQLiteProgram有两个重要的参数,是执行数据库操作前要提供的:</p> <pre> <code class="language-java">private final String mSql; //操作数据库用的Sql private final Object[] mBindArgs; //列和数据值的关系</code></pre> <p>参数mBindArgs描述了数据库列和数据的关系,SQLiteStatement为不同数据类型提供bind方法,结果保存在mBindArgs,最终交给SQLite处理。</p> <p>和StandardDatabase一样,SQLiteStatement的方法委托给DatabaseStatement调用,所以greenDAO操作数据库前需要先获取DatabaseStatement。</p> <p><strong>生成Sql</strong></p> <p>sql的获取需要用到TableStatements,它的对象维护在DaoConfig里,由它负责创建和缓存DatabaseStatement,下面是insert的DatabaseStatement获取过程:</p> <pre> <code class="language-java">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; }</code></pre> <p>sql语句通过SqlUtils工具拼接,由Database调用compileStatement将sql存入DatabaseStatement。可知,DatabaseStatement的实现类是StandardDatabaseStatement:</p> <pre> <code class="language-java">@Override public DatabaseStatement compileStatement(String sql) { return new StandardDatabaseStatement(delegate.compileStatement(sql)); }</code></pre> <p>拼接出来的sql是包括表名和字段名的通用插入语句,生成的DatabaseStatement是可以复用的,所以第一次获取的DatabaseStatement会缓存在insertStatement参数,下次直接使用。</p> <p>其他例如count、update、delete等操作获取DatabaseStatement原理是一样的,就不介绍了。</p> <p><strong>执行insert</strong></p> <p>insert和insertOrReplace都调用了executeInsert,区别之处是入参DatabaseStatement的获取方法不同。</p> <pre> <code class="language-java">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(); } } }</code></pre> <p>当前线程获取数据库锁的情况下,直接执行insert操作即可,否则需要使用事务保证操作的原子性和一致性。insertInsideTx方法里,isStandardSQLite判断当前是不是SQLite数据库(留下扩展的伏笔?)。关键来了,获取原始的SQLiteStatement,调用了bindValues。</p> <pre> <code class="language-java">@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()); }</code></pre> <p>bindValues由各自的Dao实现,描述index和数据的关系,最终保存进mBindArgs。到这里,应该就能明白greenDao的核心作用。greenDao将我们熟悉的对象,转换成sql语句和执行参数,再提交SQLite执行。</p> <p>update和delete的操作和insert大同小异,推荐自行分析。</p> <h3><strong>数据Load与缓存机制</strong></h3> <pre> <code class="language-java">userDaoConfig = daoConfigMap.get(UserDao.class).clone(); userDaoConfig.initIdentityScope(type);</code></pre> <p>创建DaoSession并获取DaoConfig时,调用了initIdentityScope,这里是greenDAO缓存的入口。</p> <pre> <code class="language-java">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); } }</code></pre> <p>DaoSession的入参IdentityScopeType现在可以解释了,None时不启用缓存,Session时启用缓存。缓存接口IdentityScope根据主键是不是数字,分为两个实现类IdentityScopeLong和IdentityScopeObject。两者的实现类似,选IdentityScopeObject来研究。</p> <pre> <code class="language-java">private final HashMap<K, Reference<T>> map;</code></pre> <p>缓存机制很简单,一个保存pk和entity关系的Map,再加上get、put、detach、remove、clear等操作方法。其中get、put方法分无锁版本和加锁版本,对应当前线程是否获得锁的情况。</p> <pre> <code class="language-java">map.put(key, new WeakReference<T>(entity));</code></pre> <p>注意,将entity加入Map时使用了弱引用,资源不足时GC会主动回收对象。</p> <p>下面是load方法,看缓存扮演了什么角色。</p> <pre> <code class="language-java">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); }</code></pre> <p>在执行真正的数据加载前,标记1处先查找缓存,如果有就直接返回,无就去查数据库。标记2处准备sql语句和参数,交给rawQuery查询,得到Cursor。</p> <p>用主键查询,只可能有一个结果,调用loadUnique,最终调用loadCurrent。loadCurrent会先尝试从缓存里获取数据,代码很长,分析identityScopeLong != null这段就可以体现原理:</p> <pre> <code class="language-java">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; } }</code></pre> <pre> <code class="language-java">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); } } }</code></pre> <p>AbstractDao同时维护identityScope和identityScopeLong对象,entity会同时put进它们两者。如果主键是数字,优先从identityScopeLong获取缓存,速度更快;如果主键不是数字,就尝试从IdentityScopeObject获取;如果没有缓存,只能通过游标读取数据库。</p> <h3><strong>数据Query</strong></h3> <p>QueryBuilder使用链式结构构建Query,灵活地支持where、or、join等约束的添加。具体代码是简单的数据操作,没必要细说,数据最终会拼接成sql。Query的unique操作和上面的load一样,而list操作在调用rawQuery获取Cursor后,最终调用AbstractDao的loadAllFromCursor:</p> <pre> <code class="language-java">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; }</code></pre> <p>标记1处尝试使用Android提供的CursorWindow以获取一个更快的Cursor。SQLiteDatabase将查询结果保存在CursorWindow所指向的共享内存中,然后通过Binder把这片共享内存传递到查询端。Cursor不是本文要讨论的内容,详情可以参考其他资料。</p> <p>标记2处通过移动Cursor,利用loadCurrent进行批量操作,结果保存在List中返回。</p> <h3><strong>一对一和一对多</strong></h3> <p>greenDAO支持一对一和一对多,但并不支持多对多。</p> <pre> <code class="language-java">@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; }</code></pre> <p>一对一,使用@ToOne标记,greenDAO会自动生成get方法,并标记为@Generated,代表是自动生成的,不要动代码。get方法利用主键load出对应的entity即可。</p> <pre> <code class="language-java">@ToMany(joinProperties = { @JoinProperty(name = "key", referencedName = "father_key") }) private List<CheckItem> children;</code></pre> <p>一对多的形式和一对一类似,使用@ToMany标记,get方法是利用QueryBuild查询目标List,代码简单就不贴了。</p> <h3><strong>后记</strong></h3> <p>到此,过了一遍greenDAO主要功能,还有些高级特性用到再研究吧。纵观下来,greenDAO还是挺简单的,但也很实用,简化了数据库调用的复杂度,具体的执行就交给原生的Android数据库管理类。</p> <p> </p> <p> </p> <p>来自:http://www.jianshu.com/p/0d3cbe6278fb</p> <p> </p>