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

code小生,一个专注于 Android 领域的技术分享平台

作者:rangaofei
地址:https://rangaofei.github.io/
声明:本文是 rangaofei 原创,转发等请联系原作者授权。

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

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

非空约束

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

这篇文章主要是研究如何设置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文件中最少的情况下只需要设置两个库就可以:

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类:

@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类:

@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是这个实现类的父类的父类,这是一个抽象类,共有三个抽象方法:

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

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

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

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

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,主要看一下这个方法:

@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,这个是入口类。

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)类里边定义了生成代码的规则.

//主要方法
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()方法就是这个父类的方法.

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)中实现中有这个方法

addMethod(createCreateOpenHelper())

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

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类实现了编写该方法

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库)类来执行某些功能,也就是ListbuildCreateQueries()集合中的元素

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中生成的,看一下构造方法

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类中一个方法:

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的实例方法调用

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而来:

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

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

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

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

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

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语句:

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

技术推荐

Android Transition Framework 详解---超炫的动画框架

阿里巴巴推出超强针对布局方案和布局复用的开源框架(vlayout)

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值