引言
-
在上一篇 “从使用到源码—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,用于创建表。源码中有两个我们熟悉的方法,可以看到,这跟我们自己在
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); // 重建 }上面的
createAllTables和dropAllTables都来自DaoMaster,下面是DaoMaster的相关源码,可见在其构造函数中会一次性注册我们的所有EntityDao.class,而注册Dao实际目的是缓存DaoConfig,将DaoConfig与EntityDao.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.class与DaoConfig映射、缓存起来,这样就可以节省我们在使用时因转换而产生的性能损耗。
-
-
DaoSession
-
官方描述:
DaoSession用于管理所有与数据表相对应的DAO对象,而这个DAO对象可以通过getXxxDao()方法获取;同时,DaoSession还提供了一些类似于增删改查、刷新的泛型方法;最后,DaoSession对象会保持对IdentityScope的跟踪。 -
注意: 数据库连接归属于
DaoMaster,所以如果有多个Sessions,那它们是共用一个数据库连接的。 通过daoMaster.newSession()可以快速创建DaoSession,但是每个DaoSession都会分配内存,因而就会产生Entity的缓存。DaoSession缓存与IdentityScope:- 如果有两个检索操作返回同一个数据库对象,那么你实际返回的是一个
Java对象还是两个呢? 这完全取决于IdentityScope。默认情况下(实际行为可配置),greenDao是多个检索返回相同的Java对象,例如:从USER表中加载ID为41的User对象,多个检索操作返回的实际是同一个Java对象。这种行为的副作用就是会导致产生Entity缓存(垃圾数据),如果Entity对象仍然存在与内存中(即使GreenDao使用的是弱引用),那么相应的对象就不会再次被创建,而greenDao也并不会自己检索来更新这些数据,最终导致检索时直接从缓存中得到数据,而不是最新的数据。 - 缓存问题的解决办法:
// 1. 清空整个session的IdentityScope daoSession.clear(); // 2. 清空某个DAO的IdentityScope UserDao userDao = daoSession.getUserDao(); userDao.detechAll();- 如果有两个检索操作返回同一个数据库对象,那么你实际返回的是一个
-
以上是来自官方的描述,我们接下来结合源码看看,对于
DaoSession,实际我们只需要关注以下两个方法:- 这里刚好也验证了一下上面我们对
DaoMaster,DaoConfig分析的结论对吧,而这里的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语句进行对应的数据操作。要说联系的话,可查看上面这张关系图。
-