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阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容