美文网首页
从使用到源码—GreenDao(理解核心类篇)

从使用到源码—GreenDao(理解核心类篇)

作者: horseLai | 来源:发表于2018-10-03 23:19 被阅读0次

    引言

    • 在上一篇 “从使用到源码—GreenDao(基本使用篇) ”中,已经从环境配置开始介绍了GreenDao的基本使用,包括基本的增删改查,以及相对复杂点的建立关联数据库模型操作等。似乎已经可以满足了我们绝大部分的使用需求了。不过事实真是这样吗?

    • 虽说我们已经知道该怎么使用GreenDao了,但是出于对其内部实现的好奇与想要揭露真相的不甘,从本篇文章开始,将结合源码分析GreenDao的实现原理。

    核心类介绍


    • 酝酿酝酿
      • 回顾一下greenDao的注册过程,我们会在Application中创建一个DaoMaster.DevOpenHelper,然后通过这个类对象拿到数据库对象,接着给这个数据库建立Session。而而在使用过程中,我们是通过这个DaoSession拿到Dao,然后进行数据操作的。

        public class App extends Application { 
            private static DaoSession sDaoSession; 
            
            @Override
            public void onCreate() {
                super.onCreate();
        
                DaoMaster.DevOpenHelper devOpenHelper = new DaoMaster.DevOpenHelper(this, "database-normal");
                sDaoSession = new DaoMaster(devOpenHelper.getWritableDb()).newSession();
            }
            // ...
        }
        
      • 如果不满足于使用,那么这里肯定会留有疑问,比方说:

        • DaoMaster.DevOpenHelper跟系统官方的SQLiteOpenHelper有啥关系?
        • Session又是什么鬼?跟服务器的那个Session有啥联系?
        • DaoMaster看起来很强的样子,它都干了什么事?
        • 纵观全局,Dao,比如说UserDao是怎么实现的?怎么与其他部分建立联系的?
        • ...
      • 好吧,真的是疑问越多,想要扒它外衣以看清真相的心情就越迫切,那咱开始吧。

    • Entity

      • 这不用多说吧,可以看成是一个普通的Java Bean实体类,不同的是这个类的类名会被映射成数据表的表名,而字段名会被映射成数据表的列名,也就是说在GreenDao中,这家伙就代表数据表中的一条记录,而每个字段的注解就代表这一个约束条件,比方说下面示例:
        @Entity  // 指定为 GreenDao 的 Entity
        public class User { 
            @Id(autoincrement = true)  // 约束条件 
            private Long id;
            
            @Unique  // 约束条件
            @NotNull  // 约束条件
            private String phoneNum; 
            // ...
        }
        
    • DaoMaster

      • 官方描述: greenDao的入口类,用于持有SQLiteDatabase对象,然后管理与数据表相对应的Dao类,比如说UserDao.class,同时它还包含一些用于创建或删除数据表的静态方法。

      • 内部类DevOpenHelper: 实现自系统官方的SQLiteOpenHelper,用于创建表。

        DevOpenHelper 继承关系图

        源码中有两个我们熟悉的方法,可以看到,这跟我们自己在SQLiteOpenHelper的操作逻辑也是一样的。

            @Override
            public void onCreate(Database db) {
                Log.i("greenDAO", "Creating tables for schema version " + SCHEMA_VERSION);
                createAllTables(db, false); // 建表
            } 
            @Override
            public void onUpgrade(Database db, int oldVersion, int newVersion) {
                Log.i("greenDAO", "Upgrading schema from version " + oldVersion + " to " + newVersion + " by dropping all tables");
                dropAllTables(db, true); // 删表
                onCreate(db); // 重建
            } 
        

        上面的createAllTablesdropAllTables都来自DaoMaster,下面是DaoMaster的相关源码,可见在其构造函数中会一次性注册我们的所有EntityDao.class而注册Dao实际目的是缓存DaoConfig,将DaoConfigEntityDao.class本身形成映射关系,降低后期使用时因转换而产生的损耗,说白了就是用空间换时间

        public class DaoMaster extends AbstractDaoMaster {
            public static final int SCHEMA_VERSION = 1;
            public DaoMaster(Database db) {
                super(db, SCHEMA_VERSION);
                registerDaoClass(UserDao.class);
                registerDaoClass(BlogDao.class); 
                // ...
            } 
            public static void createAllTables(Database db, boolean ifNotExists) {
                UserDao.createTable(db, ifNotExists);
                BlogDao.createTable(db, ifNotExists);
                // ...
            } 
            public static void dropAllTables(Database db, boolean ifExists) {
                UserDao.dropTable(db, ifExists);
                BlogDao.dropTable(db, ifExists);
                // ...
            }   
            // ...
        }  
        public abstract class AbstractDaoMaster { 
            protected final Map<Class<? extends AbstractDao<?, ?>>, DaoConfig> daoConfigMap;
            // 注册Entity类
            protected void registerDaoClass(Class<? extends AbstractDao<?, ?>> daoClass) {
                    DaoConfig daoConfig = new DaoConfig(db, daoClass);
                    daoConfigMap.put(daoClass, daoConfig);
            }
            // ...
        }
        

        至于DaoConfig,实际是通过反射手段从XxxDao.Properties中提取出所有字段,并对这些字段进行分类,然后缓存起来。

        public final class DaoConfig implements Cloneable { 
            public final Database db;
            public final String tablename;
            public final Property[] properties; // 所有属性
        
            public final String[] allColumns; // 所有列名
            public final String[] pkColumns;  // 主键名
            public final String[] nonPkColumns; // 非主键名
            public DaoConfig(Database db, Class<? extends AbstractDao<?, ?>> daoClass) { 
                this.tablename = (String) daoClass.getField("TABLENAME").get(null);
                Property[] properties = reflectProperties(daoClass);
                // ...   
            }
            private static Property[] reflectProperties(Class<? extends AbstractDao<?, ?>> daoClass){
                // 提取`XxxDao.Properties`中的所有字段
                Class<?> propertiesClass = Class.forName(daoClass.getName() + "$Properties");
                Field[] fields = propertiesClass.getDeclaredFields();
                // ... 
            }
            // ...
        }
        
      • 综述: DaoMaster会持有数据库对象的引用,并且在创建时注册所有XxxDao.class,而注册的目的就是将XxxDao.Properties.class中的所有Property字段分类、保存到DaoConfig中,然后将XxxDao.classDaoConfig映射、缓存起来,这样就可以节省我们在使用时因转换而产生的性能损耗。

    • DaoSession
      • 官方描述: DaoSession用于管理所有与数据表相对应的DAO对象,而这个DAO对象可以通过getXxxDao()方法获取;同时,DaoSession还提供了一些类似于增删改查、刷新的泛型方法;最后,DaoSession对象会保持对IdentityScope的跟踪。

      • 注意: 数据库连接归属于DaoMaster,所以如果有多个Sessions,那它们是共用一个数据库连接的。 通过daoMaster.newSession()可以快速创建DaoSession,但是每个DaoSession都会分配内存,因而就会产生Entity的缓存。

        • DaoSession缓存与IdentityScope
          • 如果有两个检索操作返回同一个数据库对象,那么你实际返回的是一个Java对象还是两个呢? 这完全取决于IdentityScope。默认情况下(实际行为可配置),greenDao是多个检索返回相同的Java对象,例如:从USER表中加载ID41User对象,多个检索操作返回的实际是同一个Java对象。这种行为的副作用就是会导致产生Entity缓存(垃圾数据),如果Entity对象仍然存在与内存中(即使GreenDao使用的是弱引用),那么相应的对象就不会再次被创建,而greenDao也并不会自己检索来更新这些数据,最终导致检索时直接从缓存中得到数据,而不是最新的数据。
          • 缓存问题的解决办法:
          // 1. 清空整个session的IdentityScope
          daoSession.clear();
          
          // 2. 清空某个DAO的IdentityScope
          UserDao userDao = daoSession.getUserDao();
          userDao.detechAll();
          
      • 以上是来自官方的描述,我们接下来结合源码看看,对于DaoSession,实际我们只需要关注以下两个方法:

        • 这里刚好也验证了一下上面我们对DaoMasterDaoConfig分析的结论对吧,而这里的registerDao是不是看着很眼熟,跟DaoMaster#registerDaoClass简直不要太像了.
        public DaoSession(Database db, IdentityScopeType type, Map<Class<? extends AbstractDao<?, ?>>, DaoConfig>
                daoConfigMap) {
            super(db);
            // 创建DAO
            userDaoConfig = daoConfigMap.get(UserDao.class).clone();
            userDaoConfig.initIdentityScope(type); 
            userDao = new UserDao(userDaoConfig, this); 
            // ...
            // 注册DAO
            registerDao(User.class, userDao); 
            // ...
        }
        
        public void clear() {
            userDaoConfig.clearIdentityScope();
            // ... 
        }
        

        于是我们看看源码,好吧,确实性质是一样的,都是映射、缓存以提升性能、提供方便。

        private final Map<Class<?>, AbstractDao<?, ?>> entityToDao;
        protected <T> void registerDao(Class<T> entityClass, AbstractDao<T, ?> dao) {
            entityToDao.put(entityClass, dao);
        }
        
      • 综述: 相对来说DaoSession的功能还是相当明显的,无论是从官方描述也好,还是源码,它都坦荡地宣称着,自己就是在创建、使用DAO,就是在管理DAO,你从我这里能很轻松的得到与你的Enyity相对应的DAO,并且我还给你提供了若干个泛型的操作方法,你只需要让我知道Entity.class和相关数据,我就能为你服务。

    • Daos

      • 关于Daos,其实通过前面的分析,已经可以很清楚它的作用了,无非是提供数据库的增删该查等操作方法。下面我们以User数据的插入为例追踪一下它的实现过程。

        public long insert(T entity) {
            return executeInsert(entity, statements.getInsertStatement(), true);
        }
        
        // 1. 建立 DatabaseStatement
        public DatabaseStatement getInsertStatement() {
            if (insertStatement == null) {
                String sql = SqlUtils.createSqlInsert("INSERT INTO ", tablename, allColumns);
                DatabaseStatement newInsertStatement = db.compileStatement(sql);
                // ... 
            }
            return insertStatement;
        }
        
        // 2. 拼接 Sql语句
        public static String createSqlInsert(String insertInto, String tablename, String[] columns) {
            StringBuilder builder = new StringBuilder(insertInto);
            builder.append('"').append(tablename).append('"').append(" (");
            appendColumns(builder, columns);
            builder.append(") VALUES (");
            // 拼接 ? 占位符
            appendPlaceholders(builder, columns.length);
            builder.append(')');
            return builder.toString();
        }
        
        // 3. 执行executeInsert,实际通过事务插入数据
        private long executeInsert(T entity, DatabaseStatement stmt, boolean setKeyAndAttach) {
            long rowId;
            if (db.isDbLockedByCurrentThread()) {
                rowId = insertInsideTx(entity, stmt); // 实际使用事务执行插入
            } else { 
                db.beginTransaction();
                try {
                    rowId = insertInsideTx(entity, stmt);
                    db.setTransactionSuccessful();
                } finally {
                    db.endTransaction();
                }
            }
            if (setKeyAndAttach) {
                updateKeyAfterInsertAndAttach(entity, rowId, true);
            }
            return rowId;
        }
        
        // 4. 以事务形式执行插入
        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();
                }
            }
        }
        
        // 数据填充过程
        @Override
        protected final void bindValues(DatabaseStatement stmt, User entity) {
            stmt.clearBindings();
        
            Long id = entity.getId();
            if (id != null) {
                stmt.bindLong(1, id);
            }
            stmt.bindString(2, entity.getPhoneNum());
        
            String firstName = entity.getFirstName();
            if (firstName != null) {
                stmt.bindString(3, firstName);
            }
            // ...
        }
        
        

      以上便是一个数据插入操作的执行过程,其实跟我们平时手写实现时差别不大,只不过它是自动生成的,而我们是手写。知道了数据插入过程,那么其他操作就类比一下就好了,

      • 综述: Dao为数据表提供了包括增删该查等各种操作方法;实际上这些操作方法都是在执行拼接的Sql语句;

    总结

    • 本篇文章介绍了greenDao中的核心类各自的作用以及实现过程,总的来说就是以下这张图。

      核心类的间的关系
    • 然后我们再来回答一下前面提出的问题:

      • DaoMaster.DevOpenHelper跟系统官方的SQLiteOpenHelper有啥关系?

        • DaoMaster.DevOpenHelper间接继承于SQLiteOpenHelper,和我们自己实现时逻辑一致。
      • Session又是什么鬼?跟服务器的那个Session有啥联系?

        • DaoSession会创建Dao对象,然后将其于Dao.class本身形成映射关系并缓存起来,方便快速获取。服务器的Session用于保存服务端的一些信息,与这里的Session并没有啥联系。
      • DaoMaster看起来很强的样子,它都干了什么事?

        • 作为入口,在其对象创建时会创建、缓存DaoConfig,而DaoConfig中又会缓存对应于Entity的表列字段分类、列字段名称等信息,这样既可以方便获取,又能节省获取时的性能损耗。除此之外,它持有数据库对象,包含创建和删除所有数据表的操作方法,以及创建和管理DaoSession
      • 纵观全局,Dao,比如说UserDao是怎么实现的?怎么与其他部分建立联系的?

        • Dao包含增删改查等对数据表的操作方法,实现过程说白了就是执行拼接的Sql语句进行对应的数据操作。要说联系的话,可查看上面这张关系图。

    相关文章

      网友评论

          本文标题:从使用到源码—GreenDao(理解核心类篇)

          本文链接:https://www.haomeiwen.com/subject/pspzoftx.html