从使用到源码—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,用于创建表。

      从使用到源码—GreenDao(理解核心类篇)_第1张图片
      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>, DaoConfig> daoConfigMap;
          // 注册Entity类
          protected void registerDaoClass(Class> 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> daoClass) { 
              this.tablename = (String) daoClass.getField("TABLENAME").get(null);
              Property[] properties = reflectProperties(daoClass);
              // ...   
          }
          private static Property[] reflectProperties(Class> 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>, 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, AbstractDao> entityToDao;
      protected  void registerDao(Class entityClass, AbstractDao 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中的核心类各自的作用以及实现过程,总的来说就是以下这张图。

    从使用到源码—GreenDao(理解核心类篇)_第2张图片
    核心类的间的关系

  • 然后我们再来回答一下前面提出的问题:

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

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

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

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

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

你可能感兴趣的:(从使用到源码—GreenDao(理解核心类篇))