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

引言

  • 在上一篇 “从使用到源码—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语句进行对应的数据操作。要说联系的话,可查看上面这张关系图。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,264评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,549评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,389评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,616评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,461评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,351评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,776评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,414评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,722评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,760评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,537评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,381评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,787评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,030评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,304评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,734评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,943评论 2 336

推荐阅读更多精彩内容