Room是安卓推出的一个官方框架,极大的简化了安卓开发者中间层的编写,仅仅需要编写三个主要的注解模块即可实现增删改查功能,前一篇文章简单翻译了一下Room支持的使用,拓展了一些SQLite的知识。
其实在使用中我们会发现Room仍然有很多不尽如人意的地方,这篇文章就一个简单的非空约束设置来探索一下。
非空约束
用过SQL的人都知道用在表上的约束是一种强制规则,可以限制出入到表中的数据类型,为数据提供准确性和可靠性。SQLite中的约束主要有以下几种:
name | intruduction | RoomAnnotation |
---|---|---|
NOT NULL | 确保列中没有NULL值 | 暂无 |
DEFAULT | 没有指定时提供默认值 | 暂无 |
UNIQUEUE | 列中所有的值不同 | index |
PRIMARY KEY | 主键 | PrimaryKey |
CHECK | 确保列中的值满足一定条件 | 暂无 |
这篇文章主要是研究如何设置NOT NULL约束。
在SQLite语句中可以直接设置非空约束:
1 | CREATE TABLE IF NOT EXISTS `user` |
这样我们就设置了uid、username和age字段不为空。
Room原理
Room算是一个庞大的库,但我们在gradle文件中最少的情况下只需要设置两个库就可以:
1 |
|
打开mvn我们可以看到他们两个所依赖的库到底有多少:
关于非空约束的设置在这里要将的主要是Compiler库,它是apt解析注解生成文件的主要库。
Room中大量使用了注解来标识数据存储信息和查询信息,这些注解的元注解全部使用了@Retention(RetentionPolicy.CLASS)
,也就是这些注解只保存到编译期,运行期就会消除(这里简单说一下其实在Room运行的过程中还是用到了反射,在获取DAO和Database实现类的时候)。它通过使用apt来获取注解信息并通过javapoet来生成实现类的代码,然后由Runtime来调用这些实现类。
这里我做一个简单的例子来说明:
首先实现一个Entity类:
1 | "user", indices = { (name = "name", value = {"user_name"}, unique = true)}) (tableName = |
这个非常简单,只有四个字段,uid是int类型,设置为了主键,username是String类型,重命名为user_name,password是String类型,age是Integet类型。
然后继续实现一个Database类:
1 | (entities = {User.class}) |
这里将User实体类加入到了APPDatabase数据库中了,UserDao是的一个Dao层接口,需要在这里引入为抽象域。这样我们就完成了我们自己的编码工作,
这个类在编译期间将会生成一个名称为AppDatabase_Impl
的实现类(位置在./app/build/generated/source/apt/debug/debug/package/AppDatabase_Impl
),该文件完成了数据库的创建,打开连接,删除,增删改查的实现类的初始化等工作。RoomDatabase是这个实现类的父类的父类,这是一个抽象类,共有三个抽象方法:
1 |
|
同时加上AppDatabase中的UserDao抽象方法,共有四个方法需要在AppDatabase_Impl
中实现,关于表的常见主要是第一个方法,该方法返回的示意SupportSQLiteOpenHelper
类型,该类型是由SupportSQLiteOpenHelper.Configuration
中的工厂方法创建,configuration本身是一个构造者模式,需要配置一个SupportSQLiteOpenHelper.Callback
,通过代理需要实现四个主要方法:
1 | protected abstract void dropAllTables(SupportSQLiteDatabase database); |
创建表的方法就在createAllTables
,主要看一下这个方法:
1 |
|
只需要关注第一个执行语句,可以看到,只有uid字段被设置为了NOT NULL,其他字段都没有默认这个属性,假如多写几个变量可以很轻松的知道,所有的基本类型都会设置默认非空,除此之外都不会有这个约束,同样为非基本类型设置了PrimaryKey属性,也不会生成这个约束。
源码探索
Room本身是一个庞大的库,这里只会分析用到的一些东西,同时代码生成库COmpiler官方用的是kotlin语言,鉴于我的kotlin停留在不入门级别,有错误希望读者指正。
代码生成库用到的是compiler和common库(源码位置:asop/framewirks/support/room
)
compiler库是生成代码的主要库,所有的实现类都是在这个库中由系统自动生成,找到RoomDatabase
,这个是入口类。
1 | override fun initSteps(): MutableIterable<ProcessingStep>? { |
这个方法是apt的主要方法,compiler提供了一个contex(非activity的context),context是运行apt时的上下文,提供了许多有用的工具类和方法,包括日志输出,控制镇检查,注解缓存等.class DatabaseProcessingStep(context: Context) : ContextBoundProcessingStep(context)
类里边定义了生成代码的规则.
1 | //主要方法 |
根据上边代码的注解,看到了dataBases是由Element经过处理生成为Database
类的集合,该类的所有元素经过DatabaseWriter(db).write(context.processingEnv)
方法写入文件.而DatabaseWriter
继承自ClassWriter
,write()方法就是这个父类的方法.
1 | abstract fun createTypeSpecBuilder(): TypeSpec.Builder |
让子类实现abstract fun createTypeSpecBuilder(): TypeSpec.Builder
,通过builder模式将信息引入进来,在子类(DatabseWrite)中实现中有这个方法
1 | addMethod(createCreateOpenHelper()) |
这个方法是加入OpenHelper方法的语句
1 | private fun createCreateOpenHelper() : MethodSpec { |
找到生成方法的语句SQLiteOpenHelperWriter(database).write(openHelperVar, configParam, openHelperCode)
,SQLiteOpenHelperWriter类实现了编写该方法
1 | private fun createCreateAllTables() : MethodSpec { |
这此我们基本算是找到根源了,addStatement("_db.execSQL($S)", it)
中的参数it就是我们需要的东西,它是Database类委托给DatabseBundle(migration库)类来执行某些功能,也就是List<String> buildCreateQueries()
集合中的元素
1 | public List<String> buildCreateQueries() { |
Entity同时也是委托来给了EntityBudle来执行某些功能,我们要找的约束也时再EntityBundle中生成的,看一下构造方法
1 | public EntityBundle(String tableName, String createSql, |
其中的mCreateSql就是系统生成的创建表中变量的语句.而这个是经过一系列的Processor来生成的,包括EntityProcessor,PojoProcessor,FiledProcessor,而FiledProcessor就是用来生成Filed对象,Filed类中一个方法:
1 | fun databaseDefinition(autoIncrementPKey : Boolean) : String { |
假如noNull为真则会添加约束,这个方法最终会被Entity的实例方法调用
1 | fun createTableQuery(tableName : String) : String { |
看到这里应该都明白这个NOT NULL约束是如何生成的,它就是根据Filed
中变量noNull
而来:
1 | val nonNull = element.isNonNull() && (parent == null || parent.isNonNullRecursively()) |
后边的parent我们可以不用管,算是一个递归,但是最终都是判断element.isNonNull()
.这是room的扩展函数,扩展了Element的java方法
找到ext包下的element_ext文件,其中具体定义了该方法:
1 | fun Element.isNonNull() = |
基本找到真凶了,这里共有三个条件,判断TypeKind是否primitive,是否包含NonNull注解,是否包含kotlin中的NotNull注解.
而primitive方法:
1 | public boolean isPrimitive() { |
这样我们就知道了,所有的基本类型都是primitive的,必然会生成NOT NUll约束,而非空注解也会生成NOT NULL约束,所以我们只要给非基本类型加上这两个约束中的一种就可以了.
修改User中的age代码:
1 | @NonNull |
看一下AppDatabase_Impl的实现类中的sql语句:
1 | _db.execSQL( |
验证成功了。
其他方式
上面讲的方法是最简单的方法,在我们创建好表以后基本很难更改这些约束。
除了重命名表和在已有的表中添加列,ALTER TABLE 命令不支持其他操作。我们就可以利用migration来执行原生SQL语句生成表,这样约束就可以写在SQL语句中。