引言
在上一篇 “从使用到源码—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
>, 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
>, 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
中的核心类各自的作用以及实现过程,总的来说就是以下这张图。
-
然后我们再来回答一下前面提出的问题:
-
DaoMaster.DevOpenHelper
跟系统官方的SQLiteOpenHelper
有啥关系?-
DaoMaster.DevOpenHelper
间接继承于SQLiteOpenHelper
,和我们自己实现时逻辑一致。
-
-
Session
又是什么鬼?跟服务器的那个Session
有啥联系?-
DaoSession
会创建Dao
对象,然后将其于Dao.class
本身形成映射关系并缓存起来,方便快速获取。服务器的Session
用于保存服务端的一些信息,与这里的Session
并没有啥联系。
-
-
DaoMaster
看起来很强的样子,它都干了什么事?- 作为入口,在其对象创建时会创建、缓存
DaoConfig
,而DaoConfig
中又会缓存对应于Entity
的表列字段分类、列字段名称等信息,这样既可以方便获取,又能节省获取时的性能损耗。除此之外,它持有数据库对象,包含创建和删除所有数据表的操作方法,以及创建和管理DaoSession
。
- 作为入口,在其对象创建时会创建、缓存
-
纵观全局,
Dao
,比如说UserDao
是怎么实现的?怎么与其他部分建立联系的?-
Dao
包含增删改查等对数据表的操作方法,实现过程说白了就是执行拼接的Sql
语句进行对应的数据操作。要说联系的话,可查看上面这张关系图。
-
-