目录
源码解析目录
从Room源码看抽象与封装——SQLite的抽象
从Room源码看抽象与封装——数据库的创建
从Room源码看抽象与封装——数据库的升降级
从Room源码看抽象与封装——Dao
从Room源码看抽象与封装——数据流
前言
这一系列的文章的长度大大超出了我的预期,我本想着大概需要两篇文章来分析Room的源码,结果现在这都第四篇了,居然还没来到Dao,没关系,好消息来了,这篇文章我们终于要来看看Room对于“增删改查”的实现了。
1. Dao
Dao是Data Access Objects的缩写,翻译成中文就是“数据访问对象”,这是一个面向对象的数据库接口,再翻译成大白话就是,这是我们访问数据库的接口,能进行对象数据跟数据库数据的双向转换,方便我们使用数据库。
如上图所示,这是Room的架构图,一般而言App是通过Dao来操作数据库的,包括从数据库中获取数据和保存数据到数据库,显然,我们对于数据库的存和取使用的都是Entity,即Java对象,这就体现出了Dao的职责:对象数据跟数据库数据的双向转换,并且帮我们完成“增删改查”。
Long long ago,在第一篇文章SQLite的抽象中,讲了Room对“增删改查”的抽象,这种抽象奠定了Dao的基础,其实也体现出了Room实现“增删改查”的一些细节。这里我帮你复习一下。
“增删改”是通过SupportSQLiteStatement
完成的,查询是通过SupportSQLiteQuery
完成的。话虽这么说,但是从我们定义的Dao到最终“增删改查”的实现,这中间经历了漫漫长途,没错,这是辆长途汽车,把车门封死,我要飙车了。
2. RoomDatabase
在看“增删改查”之前我们再来重新认识一下RoomDatabase
。很“神奇”的一点是,RoomDatabase
它并不是个Database。之所以这么说,是因为RoomDatabase
并没有实现SupportSQLiteDatabase
这个接口。在Room体系当中,SupportSQLiteDatabase
接口的唯一实现类是FrameworkSQLiteDatabase
。RoomDatabase
虽然并不是个Database,但是它却包含了一个SupportSQLiteDatabase
,也就是说RoomDatabase
采用了组合的模式来代替了继承,之所以这么做是因为,严格来讲RoomDatabase
并不是一个Database,但是它也包括了部分Database的职责,除此之外,RoomDatabase
还有更多别的职责。RoomDatabase
是对SupportSQLiteDatabase
的收缩和扩展,使用组合的模式让实现变得更加灵活。
public abstract class RoomDatabase {
//包含有 SupportSQLiteDatabase
protected volatile SupportSQLiteDatabase mDatabase;
private SupportSQLiteOpenHelper mOpenHelper;
@CallSuper
public void init(@NonNull DatabaseConfiguration configuration) {
mOpenHelper = createOpenHelper(configuration);
//...
}
@NonNull
public SupportSQLiteOpenHelper getOpenHelper() {
return mOpenHelper;
}
@NonNull
protected abstract SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration config);
// used in generated code
public void assertNotMainThread() {
if (mAllowMainThreadQueries) {
return;
}
if (isMainThread()) {
throw new IllegalStateException("Cannot access database on the main thread since"
+ " it may potentially lock the UI for a long period of time.");
}
}
// 以下方法是对 SupportSQLiteDatabase 方法的包装,只保留了少量用得到的方法
// 同时也方便我们在单元测试时 mock RoomDatabase
public Cursor query(String query, @Nullable Object[] args) {
return mOpenHelper.getWritableDatabase().query(new SimpleSQLiteQuery(query, args));
}
public Cursor query(SupportSQLiteQuery query) {
//不能在主线程中
assertNotMainThread();
return mOpenHelper.getWritableDatabase().query(query);
}
//把SQL语句(String类型)编译成可以执行的SQL语句(SupportSQLiteStatement类型)
public SupportSQLiteStatement compileStatement(@NonNull String sql) {
//不能在主线程中
assertNotMainThread();
return mOpenHelper.getWritableDatabase().compileStatement(sql);
}
public void beginTransaction() {
//不能在主线程中
assertNotMainThread();
SupportSQLiteDatabase database = mOpenHelper.getWritableDatabase();
database.beginTransaction();
}
public void endTransaction() {
mOpenHelper.getWritableDatabase().endTransaction();
}
public void setTransactionSuccessful() {
mOpenHelper.getWritableDatabase().setTransactionSuccessful();
}
public boolean inTransaction() {
return mOpenHelper.getWritableDatabase().inTransaction();
}
}
从RoomDatabase
源码当中可以看出,虽然RoomDatabase
并不是Database,但是它包含了相当Database的职责,RoomDatabase
对SupportSQLiteDatabase
中的方法进行了大量的“收缩”,只保留了几个“用得着”的方法,此外,RoomDatabase
中还包含了除Database之外的其它职责,例如,Database的创建及打开等,这就是我说的,RoomDatabase
是对SupportSQLiteDatabase
的收缩和扩展。
注意,RoomDatabase
中的query
,compileStatement
及beginTransaction
方法中都加有判断,强制要求不能在主线程中调用,因为Room把“增删改”都放到了Transaction中,也就是说,“增删改查”都不能发生在主线程中,避免我们不小心在主线程操作数据库。
3. 增删改
先来回顾一下SupportSQLiteStatement
:
public interface SupportSQLiteStatement extends SupportSQLiteProgram {
/**
* 执行SQL, 非 SELECT / INSERT / DELETE / UPDATE
* 例如CREATE / DROP table, view, trigger, index 等
*/
void execute();
/**
* 例如 UPDATE / DELETE 这种
* 返回有多少行受了影响
*/
int executeUpdateDelete();
/**
* INSERT这种
* 返回 row ID,-1代表失败
*/
long executeInsert();
/**
* 执行SQL后返回一个数字
* 例如 SELECT COUNT(*) FROM table;
*/
long simpleQueryForLong();
/**
* 执行SQL后返回一个String
*/
String simpleQueryForString();
}
可以看出,通过SupportSQLiteStatement
我们可以方便地执行增(executeInsert
)删改(executeUpdateDelete
),这是因为SupportSQLiteStatement
本身就是对于SQLiteStatement
的映射,要知道在SQLite中,“增删改”原本就是通过SQLiteStatement
来执行的,只不过藏的比较深,我们不经常接触到。
到目前为止,即使我们不去查看Room的源码,也基本上可以推测出Dao中的“增删改”是如何被执行的。首先注解处理器帮我们生成出来了相应的SQL语句,然后这些SQL语句通过RoomDatabase
的compileStatement
方法被编译成了SupportSQLiteStatement
,最后调用SupportSQLiteStatement
上的executeInsert
或者executeUpdateDelete
方法来完成“增删改”。这种推测从流程上讲是对的,要是我们自己设计可能也就这么干了,但是,Google就是Google,总是在抽象层次上能更进一步。下面我就来看看Room是如何实现的。
3.1 SharedSQLiteStatement
我们方才的推测最大的问题就在于,没有考虑增删改SupportSQLiteStatement
的共性,而共性往往是抽象的基础或者说切入点。增删改SupportSQLiteStatement
的共性就在于:
- 一般情况下“增删改”SQL语句都会包含有若干占位符(?),而后在执行之前需要绑定具体的参数。
- “增删改”SQL语句是可以重用的,因为SQL语句使用了占位符(?),而不是直接把参数硬编码进SQL语句中,只需要在执行之前把最新的参数重新绑定到SQL语句中就可以重用了。
- “增删改”重用的几率是很大的,以UPDATE为例,向数据库多次插入数据再正常不过了,只是我们每次插入的数据不一样,如果使用带占位符的SQL语句,那么SQL语句是一模一样的。
- 当然,重用是有益的,降低内存占用,提升执行效率。
基于以上几点,Room抽象出了SharedSQLiteStatement
,用于解决增删改SupportSQLiteStatement
重用的问题。
public abstract class SharedSQLiteStatement {
//多线程重用的标记
private final AtomicBoolean mLock = new AtomicBoolean(false);
private final RoomDatabase mDatabase;
private volatile SupportSQLiteStatement mStmt;
/**
* Creates an SQLite prepared statement that can be re-used across threads. If it is in use,
* it automatically creates a new one.
* 如注释所说,statement会跨线程重用,如果statement正在使用就会再新建一个
*/
public SharedSQLiteStatement(RoomDatabase database) {
mDatabase = database;
}
/**
* SQL语句,注解处理器会帮我们生成
*/
protected abstract String createQuery();
protected void assertNotMainThread() {
mDatabase.assertNotMainThread();
}
private SupportSQLiteStatement createNewStatement() {
String query = createQuery();
return mDatabase.compileStatement(query);
}
//如果 mStmt存在并且没有在使用就重用,不然就新建一个
private SupportSQLiteStatement getStmt(boolean canUseCached) {
final SupportSQLiteStatement stmt;
if (canUseCached) {
if (mStmt == null) {
mStmt = createNewStatement();
}
stmt = mStmt;
} else {
// it is in use, create a one off statement
stmt = createNewStatement();
}
return stmt;
}
/**
* 通过这个方法获取 SupportSQLiteStatement,使用完之后必须调用 release
*/
public SupportSQLiteStatement acquire() {
assertNotMainThread();
return getStmt(mLock.compareAndSet(false, true));
}
/**
* 当 SupportSQLiteStatement不再使用时,必须调用这个方法
*/
public void release(SupportSQLiteStatement statement) {
if (statement == mStmt) {
mLock.set(false);
}
}
}
SharedSQLiteStatement
的基本使用方式是,通过acquire
方法获取一个SupportSQLiteStatement
对象,在使用完后通过release
方法释放。SharedSQLiteStatement
只保留了一个SupportSQLiteStatement
,在我们使用完毕调用release
后才会被重用。在多线程的情形下,如果保留的SupportSQLiteStatement
还未被release,就会新建一个SupportSQLiteStatement
(但是这个新建的Statement不会被保留重用),不会因为要重用而影响正常的执行。
由于增、删改需要调用SupportSQLiteStatement
不同的方法,所以增、删改分别各自实现了SharedSQLiteStatement
。
//插入Entity
public abstract class EntityInsertionAdapter extends SharedSQLiteStatement {
public EntityInsertionAdapter(RoomDatabase database) {
super(database);
}
//绑定参数
protected abstract void bind(SupportSQLiteStatement statement, T entity);
public final void insert(T entity) {
//获取,可能会重用之前的 SupportSQLiteStatement
final SupportSQLiteStatement stmt = acquire();
try {
//绑定
bind(stmt, entity);
//执行 executeInsert
stmt.executeInsert();
} finally {
//释放
release(stmt);
}
}
//其它方法都是类似的
}
//删除、更新Entity
public abstract class EntityDeletionOrUpdateAdapter extends SharedSQLiteStatement {
public EntityDeletionOrUpdateAdapter(RoomDatabase database) {
super(database);
}
//绑定参数
protected abstract void bind(SupportSQLiteStatement statement, T entity);
public final int handle(T entity) {
//获取,可能会重用之前的 SupportSQLiteStatement
final SupportSQLiteStatement stmt = acquire();
try {
//绑定
bind(stmt, entity);
//执行 executeUpdateDelete
return stmt.executeUpdateDelete();
} finally {
//释放
release(stmt);
}
}
//其它方法都是类似的
}
万事俱备,只欠东风。我们只需要在Dao中实现对应的EntityInsertionAdapter
或者EntityDeletionOrUpdateAdapter
,就可以执行“增删改”操作了。而这些是注解处理器帮我们实现的,我们留待最后再看。
4. 查询
依然是Long long ago,在第一篇文章SQLite的抽象中就介绍了,SupportSQLiteDatabase
收紧了查询方法,并且底层使用rawQueryWithFactory
方法来真正的执行查询。我们再来复习一下。
public interface SupportSQLiteQuery {
String getSql();
//重点在这个方法
void bindTo(SupportSQLiteProgram statement);
int getArgCount();
}
class FrameworkSQLiteDatabase implements SupportSQLiteDatabase {
private final SQLiteDatabase mDelegate;
@Override
public Cursor query(final SupportSQLiteQuery supportQuery) {
return mDelegate.rawQueryWithFactory(new SQLiteDatabase.CursorFactory() {
@Override
public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver masterQuery,
String editTable, SQLiteQuery query) {
supportQuery.bindTo(new FrameworkSQLiteProgram(query));
return new SQLiteCursor(masterQuery, editTable, query);
}
}, supportQuery.getSql(), EMPTY_STRING_ARRAY, null);
}
//...
}
查询SQL的执行看上去很简单,但是却暗含着很多并不简单的内容。
首先,SupportSQLiteQuery
的定义就很奇怪,getSql
表示SQL语句(包含占位符),getArgCount
表示SQL语句中有几个占位符,单凭这两个信息,这个查询SQL根本无法执行,SupportSQLiteQuery
中最重要的方法是bindTo
,调用这个方法进行参数绑定,在这之后SQL语句才变得完整,才能被执行。按道理来说,SQL语句和绑定参数是配套的,正常流程应该是我们向数据库提供SQL语句及其绑定参数,然后执行查询,但是Room并没有这么做。不这么做居然也能完成查询,真是神奇。
其次,再来看看真正执行查询的底层方法rawQueryWithFactory
,它的完整签名是
Cursor rawQueryWithFactory(
CursorFactory cursorFactory, String sql, String[] selectionArgs,
String editTable)
第一个参数是cursorFactory
,即Cursor工厂,这个方法本身我们就不经常使用,即使用第一个参数也往往传null,表示使用默认Cursor工厂;第二个参数是sql
,没啥好说的;第三个参数是selectionArgs
,即SQL语句的绑定参数;最后一个参数可以忽略。
Room对于rawQueryWithFactory
方法的使用也很奇怪,明明必须要提供SQL语句的绑定参数,但是Room却使用了一个空字符串数组EMPTY_STRING_ARRAY
敷衍了过去,这么说来真相就只有一个了,必然是传递的第一个参数cursorFactory
有啥魔法,那我们就来仔细看看。
class FrameworkSQLiteDatabase implements SupportSQLiteDatabase {
@Override
public Cursor query(final SupportSQLiteQuery supportQuery) {
return mDelegate.rawQueryWithFactory(new SQLiteDatabase.CursorFactory() {
@Override
public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver masterQuery,
String editTable, SQLiteQuery query) {
//调用了SupportSQLiteQuery上的bindTo方法,很可疑
supportQuery.bindTo(new FrameworkSQLiteProgram(query));
return new SQLiteCursor(masterQuery, editTable, query);
}
}, supportQuery.getSql(), EMPTY_STRING_ARRAY, null);
}
}
第一个参数是Cursor工厂,要求我们创建一个Cursor出来,按道理来说,如果不是为了创建自定义的Cursor,是没必要传递这个参数的。Room这里还真的没有创建什么特别的、自定义的Cursor出来,还是使用的原来的SQLiteCursor
,Room这么做的唯一目的就是为了完成SQL语句的参数绑定,因为我们提供的SQL语句(String类型)会被转化为SQLiteQuery
,然后作为参数传递到newCursor
方法当中,SQLiteQuery
是SQLiteProgram
的子类,可以被包装成FrameworkSQLiteProgram
,正好回调SupportSQLiteQuery
上的bindTo
方法,进而完成参数的绑定。在newCursor
被调用时,查询SQL还未真正执行,此时绑定参数还来得及。
总结一下,Room使用SQLiteDatabase
底层的rawQueryWithFactory
方法来执行查询操作,并且在需要提供selectionArgs
参数时使用空字符串数组敷衍过去,由于此时查询尚未执行,SQLiteDatabase
并不知道我们给的绑定参数是错误的,它只是会默默地帮我们把SQL语句转化为SQLiteQuery
,然后Room使用一个并没有实质作用的CursorFactory
(仅仅利用了它的调用时机),在查询要执行前的最后一刻,把缺失的绑定参数给补上。真是演了一出偷梁换柱的好戏,妙啊!
此时,我的内心是忐忑的,生怕有个还没被我绕晕的人,上来怼我一句,有个卵用?!好好地提供selectionArgs
参数不就完事了。
额。。。好,我们进行下一个话题。
其实,最根本的原因在于“类型安全”,Room是很强调这一点的。这里的类型安全指的是SQL语句在绑定参数时应该依据参数的类型绑定,例如bindString
,bindLong
等等,而不是什么类型都转换成String再绑定,这么做不安全,也不方便。SQLiteProgram
其实是包含有一个叫bindAllArgsAsStrings
的方法的,SupportSQLiteProgram
在定义时选择将其剔除,不暴露出来。非常遗憾的是,rawQueryWithFactory
方法的selectionArgs
参数就是一个String[]
,这并不符合Room的设计理念。
我们姑且把FrameworkSQLiteDatabase设计出的这套query的方式称之为“延迟绑定”。
扯了这么多,现在的问题就归结为了如何向SupportSQLiteQuery
提供绑定参数,SupportSQLiteQuery
只定义了三个方法,并没有定义一个提供绑定参数的方法。
4.1 SimpleSQLiteQuery和RoomSQLiteQuery
public interface SupportSQLiteQuery {
String getSql();
void bindTo(SupportSQLiteProgram statement);
int getArgCount();
}
如上所述,SupportSQLiteQuery
并没有定义一个提供绑定参数的方法。即使FrameworkSQLiteDatabase
费那么大劲设计出了“延迟绑定”的方式,在bindTo
方法被调用时,我们依然不知道哪里可以提供要绑定的参数。不把绑定参数放到SupportSQLiteQuery
中,这似乎非常不合理,毕竟,SQL语句和绑定参数是配套的。其实,Room当中的确包含有这种“正常思路”的SQLiteQuery,这就是SupportSQLiteQuery
的实现类SimpleSQLiteQuery
。
public final class SimpleSQLiteQuery implements SupportSQLiteQuery {
private final String mQuery;
//这里保存了要绑定的参数
@Nullable
private final Object[] mBindArgs;
public SimpleSQLiteQuery(String query, @Nullable Object[] bindArgs) {
mQuery = query;
mBindArgs = bindArgs;
}
public SimpleSQLiteQuery(String query) {
this(query, null);
}
@Override
public String getSql() {
return mQuery;
}
@Override
public void bindTo(SupportSQLiteProgram statement) {
bind(statement, mBindArgs);
}
@Override
public int getArgCount() {
return mBindArgs == null ? 0 : mBindArgs.length;
}
public static void bind(SupportSQLiteProgram statement, Object[] bindArgs) {
if (bindArgs == null) {
return;
}
final int limit = bindArgs.length;
for (int i = 0; i < limit; i++) {
final Object arg = bindArgs[i];
bind(statement, i + 1, arg);
}
}
private static void bind(SupportSQLiteProgram statement, int index, Object arg) {
// extracted from android.database.sqlite.SQLiteConnection
if (arg == null) {
statement.bindNull(index);
} else if (arg instanceof byte[]) {
statement.bindBlob(index, (byte[]) arg);
} else if (arg instanceof Float) {
statement.bindDouble(index, (Float) arg);
} else if (arg instanceof Double) {
statement.bindDouble(index, (Double) arg);
} else if (arg instanceof Long) {
statement.bindLong(index, (Long) arg);
} else if (arg instanceof Integer) {
statement.bindLong(index, (Integer) arg);
} else if (arg instanceof Short) {
statement.bindLong(index, (Short) arg);
} else if (arg instanceof Byte) {
statement.bindLong(index, (Byte) arg);
} else if (arg instanceof String) {
statement.bindString(index, (String) arg);
} else if (arg instanceof Boolean) {
statement.bindLong(index, ((Boolean) arg) ? 1 : 0);
} else {
throw new IllegalArgumentException("Cannot bind " + arg + " at index " + index
+ " Supported types: null, byte[], float, double, long, int, short, byte,"
+ " string");
}
}
}
可以看出,SimpleSQLiteQuery
的确是SupportSQLiteQuery
的简单实现类。SimpleSQLiteQuery
非常符合“正常思路”下SQLiteQuery该有的样子,有SQL语句,有绑定参数mBindArgs
,在bindTo
方法被调用时,根据mBindArgs
的参数类型绑定一下子就完事了。但是,事实上,Room只在非常特定的条件下使用了SimpleSQLiteQuery
,使用条件都是那种,第一,查询语句是固定的;第二,没有绑定参数的。
Room并没有把SimpleSQLiteQuery
作为执行查询操作的通用手段。为什么?主要原因有如下几个方面:
-
FrameworkSQLiteDatabase
费劲巴拉地设计出“延迟绑定”的机制,目的就是为了“类型安全”,如果按照SimpleSQLiteQuery
的做法,把所有绑定参数放入Object[]
中,那还不如把绑定参数全转换为String类型,然后直接提供给rawQueryWithFactory
方法。 - 把绑定参数全部放入
Object[]
中是有性能损耗的,因为需要装箱拆箱操作。 - 查询可以说是数据库最重要最常用的操作了,在一个应用中“增删改”操作加在一起可能也没有查询操作多,这么多的查询操作,如果每一个都要生成一个
SimpleSQLiteQuery
对象,每个SimpleSQLiteQuery
对象再包含若干个绑定参数,每个参数的存取又都需要装箱拆箱的话,这对性能的影响是不容忽视的。
综上所述,比较好的SupportSQLiteQuery
的实现应该是:
- 绑定参数按照其类型存储,避免不必要的装箱拆箱操作。
- 查询操作虽然比较多,但不见得都会用到,
SupportSQLiteQuery
的对象应该是“懒加载”的,最好SupportSQLiteQuery
对象可以复用。
如你所想,RoomSQLiteQuery
就是这么实现的。
public class RoomSQLiteQuery implements SupportSQLiteQuery, SupportSQLiteProgram {
private volatile String mQuery;
//绑定参数按照其类型存储,避免装箱拆箱
final long[] mLongBindings;
final double[] mDoubleBindings;
final String[] mStringBindings;
final byte[][] mBlobBindings;
//记录了对应位置的参数类型
@Binding
private final int[] mBindingTypes;
final int mCapacity;
// number of arguments in the query
int mArgCount;
//回收复用池,按照绑定参数的个数进行分类
static final TreeMap sQueryPool = new TreeMap<>();
/**
* 获取 RoomSQLiteQuery,按照绑定参数的个数来,可能会复用之前的 RoomSQLiteQuery
*/
public static RoomSQLiteQuery acquire(String query, int argumentCount) {
synchronized (sQueryPool) {
//关键操作,取sQueryPool的ceilingEntry
//ceiling是天花板的意思,ceilingEntry就是取不小于argumentCount的Entity
final Map.Entry entry =
sQueryPool.ceilingEntry(argumentCount);
if (entry != null) {
//如果存在会把它从sQueryPool,这样就不会被别的查询用到了
sQueryPool.remove(entry.getKey());
final RoomSQLiteQuery sqliteQuery = entry.getValue();
//对它进行初始化
sqliteQuery.init(query, argumentCount);
return sqliteQuery;
}
}
//没有可复用的,则新建一个
RoomSQLiteQuery sqLiteQuery = new RoomSQLiteQuery(argumentCount);
sqLiteQuery.init(query, argumentCount);
return sqLiteQuery;
}
private RoomSQLiteQuery(int capacity) {
mCapacity = capacity;
// SQL的参数绑定,位置是从1开始计数的,不是从0
int limit = capacity + 1;
//noinspection WrongConstant
mBindingTypes = new int[limit];
mLongBindings = new long[limit];
mDoubleBindings = new double[limit];
mStringBindings = new String[limit];
mBlobBindings = new byte[limit][];
}
void init(String query, int argCount) {
mQuery = query;
mArgCount = argCount;
}
/**
* 释放掉 query,让其回到sQueryPool,以便复用
*/
public void release() {
synchronized (sQueryPool) {
sQueryPool.put(mCapacity, this);
prunePoolLocked();
}
}
//sQueryPool如果超过15个,会被修剪到10个
private static void prunePoolLocked() {
if (sQueryPool.size() > POOL_LIMIT) {
int toBeRemoved = sQueryPool.size() - DESIRED_POOL_SIZE;
final Iterator iterator = sQueryPool.descendingKeySet().iterator();
while (toBeRemoved-- > 0) {
iterator.next();
iterator.remove();
}
}
}
@Override
public String getSql() {
return mQuery;
}
@Override
public int getArgCount() {
return mArgCount;
}
//根据 mBindingTypes中记录的类型,进行相应的绑定
@Override
public void bindTo(SupportSQLiteProgram program) {
for (int index = 1; index <= mArgCount; index++) {
switch (mBindingTypes[index]) {
case NULL:
program.bindNull(index);
break;
case LONG:
program.bindLong(index, mLongBindings[index]);
break;
case DOUBLE:
program.bindDouble(index, mDoubleBindings[index]);
break;
case STRING:
program.bindString(index, mStringBindings[index]);
break;
case BLOB:
program.bindBlob(index, mBlobBindings[index]);
break;
}
}
}
@Override
public void bindNull(int index) {
mBindingTypes[index] = NULL;
}
//记录对应位置的参数类型,并且保存相应参数
@Override
public void bindLong(int index, long value) {
mBindingTypes[index] = LONG;
mLongBindings[index] = value;
}
@Override
public void bindDouble(int index, double value) {
mBindingTypes[index] = DOUBLE;
mDoubleBindings[index] = value;
}
@Override
public void bindString(int index, String value) {
mBindingTypes[index] = STRING;
mStringBindings[index] = value;
}
@Override
public void bindBlob(int index, byte[] value) {
mBindingTypes[index] = BLOB;
mBlobBindings[index] = value;
}
private static final int NULL = 1;
private static final int LONG = 2;
private static final int DOUBLE = 3;
private static final int STRING = 4;
private static final int BLOB = 5;
//参数类型标记
@Retention(RetentionPolicy.SOURCE)
@IntDef({NULL, LONG, DOUBLE, STRING, BLOB})
@interface Binding {
}
}
RoomSQLiteQuery
是SupportSQLiteQuery
的实现类,是Room进行数据库查询真正用到的类。它的实现主要包含如下内容:
- 按照绑定参数的个数分配相应存储空间,以便按照参数的类型保存参数。
-
RoomSQLiteQuery
中包含有回收复用池,我们通过acquire
方法获取一个RoomSQLiteQuery
对象,再使用完毕后使用release
方法释放,该对象就会回到回收复用池。在这两个方法之间,RoomSQLiteQuery
对象已经脱离了回收复用池,不会被“打扰”。 - 回收复用池是按照
RoomSQLiteQuery
的绑定参数个数来分类的(TreeMap
),从回收复用池中拿对象,会通过ceilingEntity
方法去取,保证给我们一个不小于绑定参数个数argumentCount
的对象(如果有的话),这样既保证了RoomSQLiteQuery
对象的正常使用,又提高了复用的效率。 - 诸如
bindLong
等方法,会在获取到RoomSQLiteQuery
对象后被调用(生成的代码),这些方法会记录对应位置的参数类型并保存相应参数,在真正需要绑定参数(bindTo
方法被调用)时,根据这些记录的信息进行绑定。
查询SQL之所以可以被复用的原因在于,对于RoomSQLiteQuery
而言,SQL语句是什么并不重要,重要的是绑定参数要有足够的空间去存储(getArgCount
决定需要多大空间),getSql
返回的SQL语句和bindTo
时绑定参数是对应的,就可以了。
5. Dao实现
以上所说都是Dao实现“增删改查”的基础,有了这些基础,再来看Dao的实现是很简单的。假设我们的Dao定义如下:
@Entity
data class User(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?
)
@Dao
interface UserDao {
@Query("SELECT * FROM user WHERE uid = :userId")
fun getUserById(userId: Int): User?
@Insert
fun insert(user: User)
@Delete
fun delete(user: User)
@Update
fun update(user: User)
}
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
那么生成的代码如下:
public final class AppDatabase_Impl extends AppDatabase {
private volatile UserDao _userDao;
//...
@Override
public UserDao userDao() {
if (_userDao != null) {
return _userDao;
} else {
synchronized(this) {
if(_userDao == null) {
_userDao = new UserDao_Impl(this);
}
return _userDao;
}
}
}
}
public final class UserDao_Impl implements UserDao {
private final RoomDatabase __db;
private final EntityInsertionAdapter __insertionAdapterOfUser;
private final EntityDeletionOrUpdateAdapter __deletionAdapterOfUser;
private final EntityDeletionOrUpdateAdapter __updateAdapterOfUser;
public UserDao_Impl(RoomDatabase __db) {
this.__db = __db;
this.__insertionAdapterOfUser = new EntityInsertionAdapter(__db) {
@Override
public String createQuery() {
//SQL语句帮我们生成
return "INSERT OR ABORT INTO `User`(`uid`,`first_name`,`last_name`) VALUES (?,?,?)";
}
@Override
public void bind(SupportSQLiteStatement stmt, User value) {
//绑定帮我们做好
stmt.bindLong(1, value.getUid());
if (value.getFirstName() == null) {
stmt.bindNull(2);
} else {
stmt.bindString(2, value.getFirstName());
}
if (value.getLastName() == null) {
stmt.bindNull(3);
} else {
stmt.bindString(3, value.getLastName());
}
}
};
//__deletionAdapterOfUser, __updateAdapterOfUser是类似的
}
@Override
public void insert(final User user) {
__db.assertNotSuspendingTransaction();
__db.beginTransaction();
try {
//插入调用EntityInsertionAdapter上的insert方法
__insertionAdapterOfUser.insert(user);
__db.setTransactionSuccessful();
} finally {
__db.endTransaction();
}
}
@Override
public void delete(final User user) {
__db.assertNotSuspendingTransaction();
__db.beginTransaction();
try {
//删除和更新调用EntityDeletionOrUpdateAdapter上handle方法
__deletionAdapterOfUser.handle(user);
__db.setTransactionSuccessful();
} finally {
__db.endTransaction();
}
}
@Override
public void update(final User user) {
__db.assertNotSuspendingTransaction();
__db.beginTransaction();
try {
//删除和更新调用EntityDeletionOrUpdateAdapter上handle方法
__updateAdapterOfUser.handle(user);
__db.setTransactionSuccessful();
} finally {
__db.endTransaction();
}
}
@Override
public User getUserById(final int userId) {
final String _sql = "SELECT * FROM user WHERE uid = ?";
//获取RoomSQLiteQuery对象
final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);
int _argIndex = 1;
//绑定参数,这里不是最终的参数绑定,只是要记录对应位置的参数类型及参数本身,方便之后真正的参数绑定
_statement.bindLong(_argIndex, userId);
__db.assertNotSuspendingTransaction();
//执行查询拿到Cursor
final Cursor _cursor = DBUtil.query(__db, _statement, false);
//把Cursor数据转换成Entity对象
try {
final int _cursorIndexOfUid = CursorUtil.getColumnIndexOrThrow(_cursor, "uid");
final int _cursorIndexOfFirstName = CursorUtil.getColumnIndexOrThrow(_cursor, "first_name");
final int _cursorIndexOfLastName = CursorUtil.getColumnIndexOrThrow(_cursor, "last_name");
final User _result;
if(_cursor.moveToFirst()) {
final int _tmpUid;
_tmpUid = _cursor.getInt(_cursorIndexOfUid);
final String _tmpFirstName;
_tmpFirstName = _cursor.getString(_cursorIndexOfFirstName);
final String _tmpLastName;
_tmpLastName = _cursor.getString(_cursorIndexOfLastName);
_result = new User(_tmpUid,_tmpFirstName,_tmpLastName);
} else {
_result = null;
}
return _result;
} finally {
_cursor.close();
//释放RoomSQLiteQuery对象(回到回收复用池)
_statement.release();
}
}
}
虽然生成的代码很长,但是都是些套路,也说明这些代码确实适合用注解处理器生成。Dao的实现只是对之前分析内容的运用,没有什么实质性的内容。
6. 总结
Room对于“增删改查”实现的关键在于“效率”,针对增删改查不同的执行方式,使用了不同的复用机制。“增删改”复用的核心在于,针对特定的Entity(例如User),“增删改”各自的SQL语句是固定的,绑定参数是固定的,使用SharedSQLiteStatement
的方式,只保留一个Statement进行复用是很合理的;查询复用的核心在于,“延迟绑定”+绑定参数存储空间的复用,Room中所有的查询共享一个查询复用池,只在查询真正发生时才从复用池中取,因为查询操作是“最多”的,使用查询复用池大大提升了查询效率。