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

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

非空约束

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

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

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

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

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
4

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
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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类中一个方法:

```java
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代码:

@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语句中。