SAKA'S BLOG

Room使用之如何为字段设置非空约束

Room是安卓推出的一个官方框架,极大的简化了安卓开发者中间层的编写,仅仅需要编写三个主要的注解模块即可实现增删改查功能,前一篇文章简单翻译了一下Room支持的使用,拓展了一些SQLite的知识。

其实在使用中我们会发现Room仍然有很多不尽如人意的地方,这篇文章就一个简单的非空约束设置来探索一下。

非空约束

用过SQL的人都知道用在表上的约束是一种强制规则,可以限制出入到表中的数据类型,为数据提供准确性和可靠性。SQLite中的约束主要有以下几种:

name intruduction RoomAnnotation
NOT NULL 确保列中没有NULL值 暂无
DEFAULT 没有指定时提供默认值 暂无
UNIQUEUE 列中所有的值不同 index
PRIMARY KEY 主键 PrimaryKey
CHECK 确保列中的值满足一定条件 暂无

这篇文章主要是研究如何设置NOT NULL约束。

在SQLite语句中可以直接设置非空约束:

1
2
3
4
5
6
CREATE TABLE IF NOT EXISTS `user` 
(`uid` INTEGER NOT NULL,
`user_name` TEXT NOT NULL,
`password` TEXT,
`age` INTEGER NOT NULL,
PRIMARY KEY(`uid`))

这样我们就设置了uid、username和age字段不为空。

Room原理

Room算是一个庞大的库,但我们在gradle文件中最少的情况下只需要设置两个库就可以:

1
2
3

implementation "android.arch.persistence.room:runtime:$project.ext.room_version"
annotationProcessor "android.arch.persistence.room:compiler:$project.ext.room_version"

打开mvn我们可以看到他们两个所依赖的库到底有多少:

runtime

compiler

关于非空约束的设置在这里要将的主要是Compiler库,它是apt解析注解生成文件的主要库。

Room中大量使用了注解来标识数据存储信息和查询信息,这些注解的元注解全部使用了@Retention(RetentionPolicy.CLASS),也就是这些注解只保存到编译期,运行期就会消除(这里简单说一下其实在Room运行的过程中还是用到了反射,在获取DAO和Database实现类的时候)。它通过使用apt来获取注解信息并通过javapoet来生成实现类的代码,然后由Runtime来调用这些实现类。

这里我做一个简单的例子来说明:

首先实现一个Entity类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Entity(tableName = "user", indices = {@Index(name = "name", value = {"user_name"}, unique = true)})
public class User {

@PrimaryKey(autoGenerate = false)
private int uid;

@ColumnInfo(name = "user_name")
private String userName;

@ColumnInfo(name = "password")
private String password = "123456";

private Integer age;
//省略getter和setter方法
}

这个非常简单,只有四个字段,uid是int类型,设置为了主键,username是String类型,重命名为user_name,password是String类型,age是Integet类型。

然后继续实现一个Database类:

1
2
3
4
@Database(entities = {User.class})
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();//一个简单的接口,读者可以自行实现,与本文无关
}

这里将User实体类加入到了APPDatabase数据库中了,UserDao是的一个Dao层接口,需要在这里引入为抽象域。这样我们就完成了我们自己的编码工作,
这个类在编译期间将会生成一个名称为AppDatabase_Impl的实现类(位置在./app/build/generated/source/apt/debug/debug/package/AppDatabase_Impl),该文件完成了数据库的创建,打开连接,删除,增删改查的实现类的初始化等工作。RoomDatabase是这个实现类的父类的父类,这是一个抽象类,共有三个抽象方法:

1
2
3
4
5
6
7
8
9

//创建数据库,打开连接
protected abstract SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration config);

//同步内存中的数据和数据库的数据
protected abstract InvalidationTracker createInvalidationTracker();

//清空所有数据
public abstract void clearAllTables();

同时加上AppDatabase中的UserDao抽象方法,共有四个方法需要在AppDatabase_Impl中实现,关于表的常见主要是第一个方法,该方法返回的示意SupportSQLiteOpenHelper类型,该类型是由SupportSQLiteOpenHelper.Configuration中的工厂方法创建,configuration本身是一个构造者模式,需要配置一个SupportSQLiteOpenHelper.Callback,通过代理需要实现四个主要方法:

1
2
3
4
5
6
7
protected abstract void dropAllTables(SupportSQLiteDatabase database);

protected abstract void createAllTables(SupportSQLiteDatabase database);

protected abstract void onOpen(SupportSQLiteDatabase database);

protected abstract void onCreate(SupportSQLiteDatabase database);

创建表的方法就在createAllTables,主要看一下这个方法:

1
2
3
4
5
6
7
@Override
public void createAllTables(SupportSQLiteDatabase _db) {
_db.execSQL("CREATE TABLE IF NOT EXISTS `user` (`uid` INTEGER NOT NULL, `user_name` TEXT, `password` TEXT, `age` INTEGER, PRIMARY KEY(`uid`))");
_db.execSQL("CREATE UNIQUE INDEX `name` ON `user` (`user_name`)");
_db.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)");
_db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"1099dac99d3db917b94721c51358fa94\")");
}

只需要关注第一个执行语句,可以看到,只有uid字段被设置为了NOT NULL,其他字段都没有默认这个属性,假如多写几个变量可以很轻松的知道,所有的基本类型都会设置默认非空,除此之外都不会有这个约束,同样为非基本类型设置了PrimaryKey属性,也不会生成这个约束。

源码探索

Room本身是一个庞大的库,这里只会分析用到的一些东西,同时代码生成库COmpiler官方用的是kotlin语言,鉴于我的kotlin停留在不入门级别,有错误希望读者指正。

代码生成库用到的是compiler和common库(源码位置:asop/framewirks/support/room)

compiler库是生成代码的主要库,所有的实现类都是在这个库中由系统自动生成,找到RoomDatabase,这个是入口类。

1
2
3
4
override fun initSteps(): MutableIterable<ProcessingStep>? {
val context = Context(processingEnv)
return arrayListOf(DatabaseProcessingStep(context))
}

这个方法是apt的主要方法,compiler提供了一个contex(非activity的context),context是运行apt时的上下文,提供了许多有用的工具类和方法,包括日志输出,控制镇检查,注解缓存等.class DatabaseProcessingStep(context: Context) : ContextBoundProcessingStep(context)类里边定义了生成代码的规则.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//主要方法
override fun process(elementsByAnnotation: SetMultimap<Class<out Annotation>, Element>)
: MutableSet<Element> {
//获取Database注解的所有信息
val databases = elementsByAnnotation[Database::class.java]
?.map {
DatabaseProcessor(context, MoreElements.asType(it)).process()
}
//获取Dao注解的所有信息
val allDaoMethods = databases?.flatMap { it.daoMethods }
allDaoMethods?.let {
prepareDaosForWriting(databases, it)
it.forEach {
DaoWriter(it.dao, context.processingEnv).write(context.processingEnv)
}
}
//将Database注解信息收集类转转为系统生成的实现类
databases?.forEach { db ->
DatabaseWriter(db).write(context.processingEnv)
//输出数据库信息
if (db.exportSchema) {
val schemaOutFolder = context.schemaOutFolder
if (schemaOutFolder == null) {
context.logger.w(Warning.MISSING_SCHEMA_LOCATION, db.element,
ProcessorErrors.MISSING_SCHEMA_EXPORT_DIRECTORY)
} else {
if (!schemaOutFolder.exists()) {
schemaOutFolder.mkdirs()
}
val qName = db.element.qualifiedName.toString()
val dbSchemaFolder = File(schemaOutFolder, qName)
if (!dbSchemaFolder.exists()) {
dbSchemaFolder.mkdirs()
}
db.exportSchema(File(dbSchemaFolder, "${db.version}.json"))
}
}
}
return mutableSetOf()

根据上边代码的注解,看到了dataBases是由Element经过处理生成为Database类的集合,该类的所有元素经过DatabaseWriter(db).write(context.processingEnv)方法写入文件.而DatabaseWriter继承自ClassWriter,write()方法就是这个父类的方法.

1
2
3
4
5
6
7
8
9
10
11
abstract fun createTypeSpecBuilder(): TypeSpec.Builder

fun write(processingEnv: ProcessingEnvironment) {
val builder = createTypeSpecBuilder()
sharedFieldSpecs.values.forEach { builder.addField(it) }
sharedMethodSpecs.values.forEach { builder.addMethod(it) }
addGeneratedAnnotationIfAvailable(builder, processingEnv)
JavaFile.builder(className.packageName(), builder.build())
.build()
.writeTo(processingEnv.filer)
}

让子类实现abstract fun createTypeSpecBuilder(): TypeSpec.Builder,通过builder模式将信息引入进来,在子类(DatabseWrite)中实现中有这个方法

1
addMethod(createCreateOpenHelper())

这个方法是加入OpenHelper方法的语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private fun createCreateOpenHelper() : MethodSpec {
val scope = CodeGenScope(this)
return MethodSpec.methodBuilder("createOpenHelper").apply {
addModifiers(Modifier.PROTECTED)
returns(SupportDbTypeNames.SQLITE_OPEN_HELPER)

val configParam = ParameterSpec.builder(RoomTypeNames.ROOM_DB_CONFIG,
"configuration").build()
addParameter(configParam)

val openHelperVar = scope.getTmpVar("_helper")
val openHelperCode = scope.fork()
SQLiteOpenHelperWriter(database)
.write(openHelperVar, configParam, openHelperCode)
addCode(openHelperCode.builder().build())
addStatement("return $L", openHelperVar)
}.build()
}

找到生成方法的语句SQLiteOpenHelperWriter(database).write(openHelperVar, configParam, openHelperCode),SQLiteOpenHelperWriter类实现了编写该方法

1
2
3
4
5
6
7
8
9
private fun createCreateAllTables() : MethodSpec {
return MethodSpec.methodBuilder("createAllTables").apply {
addModifiers(PUBLIC)
addParameter(SupportDbTypeNames.DB, "_db")
database.bundle.buildCreateQueries().forEach {
addStatement("_db.execSQL($S)", it)
}
}.build()
}

这此我们基本算是找到根源了,addStatement("_db.execSQL($S)", it)中的参数it就是我们需要的东西,它是Database类委托给DatabseBundle(migration库)类来执行某些功能,也就是List<String> buildCreateQueries()集合中的元素

1
2
3
4
5
6
7
8
public List<String> buildCreateQueries() {
List<String> result = new ArrayList<>();
for (EntityBundle entityBundle : mEntities) {
result.addAll(entityBundle.buildCreateQueries());
}
result.addAll(mSetupQueries);
return result;
}

Entity同时也是委托来给了EntityBudle来执行某些功能,我们要找的约束也时再EntityBundle中生成的,看一下构造方法

1
2
3
4
5
6
7
8
9
10
11
12
public EntityBundle(String tableName, String createSql,
List<FieldBundle> fields,
PrimaryKeyBundle primaryKey,
List<IndexBundle> indices,
List<ForeignKeyBundle> foreignKeys) {
mTableName = tableName;
mCreateSql = createSql;
mFields = fields;
mPrimaryKey = primaryKey;
mIndices = indices;
mForeignKeys = foreignKeys;
}

其中的mCreateSql就是系统生成的创建表中变量的语句.而这个是经过一系列的Processor来生成的,包括EntityProcessor,PojoProcessor,FiledProcessor,而FiledProcessor就是用来生成Filed对象,Filed类中一个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
fun databaseDefinition(autoIncrementPKey : Boolean) : String {
val columnSpec = StringBuilder("")
if (autoIncrementPKey) {
columnSpec.append(" PRIMARY KEY AUTOINCREMENT")
}
if (nonNull) {
columnSpec.append(" NOT NULL")
}
if (collate != null) {
columnSpec.append(" COLLATE ${collate.name}")
}
return "`$columnName` ${(affinity ?: SQLTypeAffinity.TEXT).name}$columnSpec"
}

假如noNull为真则会添加约束,这个方法最终会被Entity的实例方法调用

1
2
3
4
5
6
7
fun createTableQuery(tableName : String) : String {
val definitions = (fields.map {
val autoIncrement = primaryKey.autoGenerateId && primaryKey.fields.contains(it)
it.databaseDefinition(autoIncrement)
} + createPrimaryKeyDefinition() + createForeignKeyDefinitions()).filterNotNull()
return "CREATE TABLE IF NOT EXISTS `$tableName` (${definitions.joinToString(", ")})"
}

看到这里应该都明白这个NOT NULL约束是如何生成的,它就是根据Filed中变量noNull而来:

1
val nonNull = element.isNonNull() && (parent == null || parent.isNonNullRecursively())

后边的parent我们可以不用管,算是一个递归,但是最终都是判断element.isNonNull().这是room的扩展函数,扩展了Element的java方法

找到ext包下的element_ext文件,其中具体定义了该方法:

1
2
3
4
fun Element.isNonNull() =
asType().kind.isPrimitive
|| hasAnnotation(android.support.annotation.NonNull::class)
|| hasAnnotation(org.jetbrains.annotations.NotNull::class)

基本找到真凶了,这里共有三个条件,判断TypeKind是否primitive,是否包含NonNull注解,是否包含kotlin中的NotNull注解.
而primitive方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public boolean isPrimitive() {
switch(this) {
case BOOLEAN:
case BYTE:
case SHORT:
case INT:
case LONG:
case CHAR:
case FLOAT:
case DOUBLE:
return true;

default:
return false;
}
}

这样我们就知道了,所有的基本类型都是primitive的,必然会生成NOT NUll约束,而非空注解也会生成NOT NULL约束,所以我们只要给非基本类型加上这两个约束中的一种就可以了.

修改User中的age代码:

1
2
@NonNull
private Integer age ;

看一下AppDatabase_Impl的实现类中的sql语句:

1
2
3
4
5
6
7
8
_db.execSQL(
"CREATE TABLE IF NOT EXISTS `user` (
`uid` INTEGER NOT NULL,
`user_name` TEXT,
`password` TEXT,
`age` INTEGER NOT NULL,
PRIMARY KEY(`uid`))"
);

验证成功了。

其他方式

上面讲的方法是最简单的方法,在我们创建好表以后基本很难更改这些约束。
除了重命名表和在已有的表中添加列,ALTER TABLE 命令不支持其他操作。我们就可以利用migration来执行原生SQL语句生成表,这样约束就可以写在SQL语句中。