Skip to content

Java|Kotlin 模型绑定

qiuwenchen edited this page Mar 8, 2024 · 2 revisions

模型绑定(Object-relational Mapping,简称 ORM),通过对 Java/Kotlin 类进行绑定,形成类或结构 - 表模型类或结构对象 - 表的映射关系,从而达到通过对象直接操作数据库的目的。

WCDB Java/Kotlin 的模型绑定分为五个部分:

  • 字段映射
  • 字段约束
  • 索引
  • 表约束
  • 虚拟表映射

这其中大部分是格式化的模版代码,我们在最后介绍文件模版和代码提示模版,以简化模型绑定的操作。

字段映射

WCDB Java/Kotlin 的字段映射都是使用注解来配置的。以下是一个字段映射的示例代码:

//Java
@WCDBTableCoding
public class Sample {
 @WCDBField(columnName = "identifier")
 public int id;//只支持绑定 public 的属性
 @WCDBField
 public String content;
 @WCDBField(columnName = "db_offset")
 public int offset;
 private String debugContent;
}
//Kotlin
@WCDBTableCoding
class Sample {
 @WCDBField(columnName = "identifier")
 var id: Int = 0//只支持绑定 public 的属性
 @WCDBField
 var content: String? = null
 @WCDBField(columnName = "db_offset")
 var offset: Int = 0
 var debugContent: String? = null
}

将一个Java/Kotlin类进行ORM绑定的过程如下:

  • 使用WCDBTableCoding注解标记Sample类,声明它实现了模型绑定。
  • 使用WCDBField注解配置需要绑定到数据库表的字段,这样数据库中的列名和字段名是一样的。
  • 对于字段名与表的列名不一样的情况,可以使用别名进行映射,如 @WCDBField(columnName = "identifier")
  • 对于字段名与 SQLite 的保留关键字冲突的字段,同样可以使用别名进行映射,如 offset 是 SQLite 的关键字,就需要@WCDBField(columnName = "db_offset")
  • 对于不需要写入数据库的字段,则不需要用WCDBField注解标记,比如debugContent字段。

字段映射定义完成后,先编译一下让注解生效,再调用 createTable(String, TableBinding<T>) 接口即可根据这个定义创建表。下面示例中所用到的DBSample相关内容,都是apt或ksp根据字段映射配置生成的内容。

// 以下代码等效于 SQL:CREATE TABLE IF NOT EXISTS sampleTable(identifier INTEGER, description TEXT)
//Java
database.createTable("sampleTable", DBSample.INSTANCE);
//Kotlin
database.createTable("sampleTable", DBSample)

字段映射的类型

并非所有类型的变量都支持被绑定为字段。WCDB Java/Kotlin 内建了常用类型的支持,包括:

数据库中的类型 类型
整型 boolean, byte, short, int, long以及它们对应的封装类
浮点型 float, double以及它们对应的封装类
字符串类型 String
二进制类型 Java的byte[]和Kotlin的ByteArray

字段约束

字段约束是针对单个字段的约束,如主键约束、非空约束、唯一约束、默认值等。

以下是一个字段约束的示例代码:

//Java
@WCDBTableCoding
public class Sample {
 @WCDBField(isPrimary = true)
 public int id;
 @WCDBField(isNotNull = true)
 @WCDBDefault(textValue = "defaultContent")
 public String content;
}
//Kotlin
@WCDBTableCoding
class Sample {
 @WCDBField(isPrimary = true)
 var id: Int = 0
 @WCDBField(isNotNull = true)
 @WCDBDefault(textValue = "defaultContent")
 var content: String? = null
}

字段约束主要是通过WCDBField注解来配置,可配置的内容如下:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface WCDBField {
    String columnName() default "";//字段名,不配置则使用属性名
    boolean isPrimary() default false;//该字段是否为主键。字段约束中只能同时存在一个主键
    boolean isAutoIncrement() default false;// 当该字段是主键时,其是否支持自增。只有整型数据可以定义为自增
    boolean enableAutoIncrementForExistingTable() default false;//如果以前没有配置主键自增,已经建好表,配置这个还可以将表改为主键自增
    boolean isUnique() default false;// 该字段是否可以具有唯一性
    boolean isNotNull() default false;// 该字段是否可以为空
    boolean isNotIndexed() default false;//fts表的配置,配置之后当前字段就不参与建索引
}

以上约束按需进行定义或者不定义即可。

字段默认值的配置则是需要额外使用WCDBDefault来配置,可以配置整型、浮点型、字符串三种默认值:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface WCDBDefault {
    long intValue() default 0;
    double doubleValue() default 0;
    String textValue() default "";
}

以上约束按需进行定义或者不定义即可。 定义完成后,同样调用 createTable(String, TableBinding<T>) 接口即可根据这个定义创建表。

// 以下代码等效于 SQL:CREATE TABLE IF NOT EXISTS sampleTable(id INTEGER PRIMARY KEY, content TEXT NOT NULL DEFAULT 'defaultContent')
//Java
database.createTable("sampleTable", DBSample.INSTANCE);
//Kotlin
database.createTable("sampleTable", DBSample)

自增属性

同时配置了 isPrimaryisAutoIncrement的字段,支持以自增的方式进行插入数据。但仍可以通过非自增的方式插入数据。

当需要进行自增插入时,对象需设置主键属性为0,则数据库会使用已有数据中最大的值+1 作为主键的值,同时插入之后会把主键的插入值赋值到对应的属性

//Java
Sample sample = new Sample();
// 插入自增数据
database.insertObject(sample,  DBSample.allFields(), "sampleTable");
System.out.print(sample.id);//输出1
// 再次插入自增数据
database.insertObject(sample,  DBSample.allFields(), "sampleTable");
System.out.print(sample.id);//输出2
// 插入非自增的指定数据
Sample specificSample = new Sample();
specificSample.id = 10;
database.insertObject(specificSample,  DBSample.allFields(), "sampleTable");//插入的主键将会是指定的10
// 再次插入自增数据
database.insertObject(sample,  DBSample.allFields(), "sampleTable");
System.out.print(sample.id);//输出11
//Kotlin
val sample = Sample()
// 插入自增数据
database.insertObject(sample, DBSample.allFields(),"sampleTable")
print(sample.id)//输出1
// 再次插入自增数据
database.insertObject(sample, DBSample.allFields(),"sampleTable")
print(sample.id)//输出2
// 插入非自增的指定数据
val specificSample = Sample()
specificSample.id = 10
database.insertObject(specificSample, DBSample.allFields(),"sampleTable")//插入的主键将会是指定的10
// 再次插入自增数据
database.insertObject(sample, DBSample.allFields(),"sampleTable")
print(sample.id)//输出12

索引

单字段的索引可以使用WCDBIndex注解配置,配置索引后的数据在能有更高的查询效率。多字段的索引配置请看下一节。

以下是一个定义索引的示例代码:

//Java
@WCDBTableCoding
public class Sample {
 @WCDBField(isPrimary = true)
 public int id;
 @WCDBField
 @WCDBIndex(isUnique = true)
 public int subId;
 @WCDBField
 @WCDBIndex(name = "length_index")
 public double length;
}
//Kotlin
@WCDBTableCoding
class Sample {
 @WCDBField(isPrimary = true)
 var id: Int = 0
 @WCDBField
 @WCDBIndex(isUnique = true)
 var subId: Int = 0
 @WCDBField
 @WCDBIndex(name = "length_index")
 var length: Double = 0.0
}

其中可以在WCDBIndex中配置isUnique来配置唯一性索引。索引名在不指定时,会用表名和字段名的组合来作为索引名,假如表名是"sampleTable",subId字段的索引名就是"sampleTable_subId_index"。

索引定义完成后,同样调用 createTable(String, TableBinding<T>)接口即可根据这个定义创建表和相关的索引。

// 以下代码等效于 SQL:
// CREATE TABLE IF NOT EXISTS sampleTable(id INTEGER PRIMARY KEY, subId INTEGER, length REAL)
// CREATE UNIQUE INDEX IF NOT EXISTS sampleTable_subId_index on sampleTable(subId)
// CREATE INDEX IF NOT EXISTS length_index on sampleTable(length)
//Java
database.createTable("sampleTable", DBSample.INSTANCE);
//Kotlin
database.createTable("sampleTable", DBSample)

表约束

表约束的配置都是在WCDBTableCoding中,它的可配置内容如下:

@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface WCDBTableCoding {
    MultiIndexes[] multiIndexes() default {};  //多字段索引
    MultiPrimary[] multiPrimaries() default {};//联合主键
    MultiUnique[] multiUnique() default {};    //多字段唯一约束
    boolean isWithoutRowId() default false;    //是否有rowid
    FTSModule ftsModule() default @FTSModule();//配置FTS虚表
}

以下是一个表约束的示例代码:

//Java
@WCDBTableCoding(
 multiPrimaries = @MultiPrimary(columns = {"multiPrimaryKeyPart1", "multiPrimaryKeyPart2"}),
 multiUnique = @MultiUnique(columns = {"multiUniquePart1", "multiUniquePart1"}),
 multiIndexes = @MultiIndexes(columns = {"multiIndexPart1", "multiIndexPart2"})
)
public class Sample {
 @WCDBField(isPrimary = true) public int id;
 @WCDBField public int multiPrimaryKeyPart1;
 @WCDBField public int multiPrimaryKeyPart2;
 @WCDBField public int multiUniquePart1;
 @WCDBField public int multiUniquePart2;
 @WCDBField public int multiIndexPart1;
 @WCDBField public int multiIndexPart2;
}
//Kotlin
@WCDBTableCoding(
 multiPrimaries = [MultiPrimary(columns = ["multiPrimaryKeyPart1", "multiPrimaryKeyPart2"])],
 multiUnique = [MultiUnique(columns = ["multiUniquePart1", "multiUniquePart1"])],
 multiIndexes = [MultiIndexes(columns = ["multiIndexPart1", "multiIndexPart2"])]
)
class Sample {
 @WCDBField(isPrimary = true) var id = 0
 @WCDBField var multiPrimaryKeyPart1 = 0
 @WCDBField var multiPrimaryKeyPart2 = 0
 @WCDBField var multiUniquePart1 = 0
 @WCDBField var multiUniquePart2 = 0
 @WCDBField var multiIndexPart1 = 0
 @WCDBField var multiIndexPart2 = 0
}

注意,这里表约束中的columns中用的是DB字段的名字。如果属性名和字段名不一致时,要区分使用DB字段名。

约束的定义方式与索引类似。定义完成后,同样调用 createTable(String, TableBinding<T>) 接口即可根据这个定义创建表。

// 以下代码等效于 SQL:
//  CREATE TABLE IF NOT EXISTS sampleTable(
//      identifier INTEGER PRIMARY KEY, 
//      multiPrimaryKeyPart1 INTEGER, 
//      multiPrimaryKeyPart2 INTEGER, 
//      multiUniquePart1 INTEGER, 
//      multiUniquePart1 INTEGER,
//      CONSTRAINT PRIMARY KEY(multiPrimaryKeyPart1, multiPrimaryKeyPart2),
//      CONSTRAINT UNIQUE(multiUniquePart1, multiUniquePart2)
//  )
//  CREATE INDEX IF NOT EXISTS sampleTable_multiIndexPart1_multiIndexPart2_index on sampleTable(multiIndexPart1, multiIndexPart2)
//Java
database.createTable("sampleTable", DBSample.INSTANCE);
//Kotlin
database.createTable("sampleTable", DBSample)

虚拟表映射

虚拟表映射主要是通过WCDBTableCoding中的ftsModule来配置,它用于定于虚拟表以进行全文搜索等特性。

普通表不需要用到虚拟表映射,因此这里暂且按下不表,我们会在全文搜索一章中进行介绍。

数据库升级

在开发过程中,经过多个版本的迭代后,经常会出现数据库字段升级的情况,如增加新字段、删除或重命名旧字段、新增索引等等。 对于 SQLite 本身,其并不支持对字段的删除和重命名。新增加字段则需要考虑不同版本升级等情况。而这个问题通过模型绑定可以很好的解决。

纵观上述字段映射、字段约束、索引和表约束等四个部分,都是通过调用 createTable(String, TableBinding<T>) 接口使其生效的。 实际上,该接口会将 模型绑定的定义 与 表本身的结构 联系起来,并进行更新。

对于字段映射:

  1. 表已存在但模型绑定中未定义的字段,会被忽略。这可以用于删除字段。
  2. 表不存在但模型绑定中有定义的字段,会被新增到表中。这可以用于新增字段。
  3. 对于需要重命名的字段,可以通过别名的方式重新映射。

忽略字段并不会删除字段。对于该字段旧内容,会持续存在在表中,因此文件不会因此变小。实际上,数据库作为持续增长的二进制文件,只有将其数据导出生成另一个新的数据库,才有可能回收这个字段占用的空间。对于新插入的数据,该字段内容为空,不会对性能产生可见的影响。

对于索引,不存在的索引会被新增到数据库中。

对于数据库已存在但模型绑定中未定义的索引,createTable(String, TableBinding<T>) 接口不会自动将其删除。如果需要删除,开发者需要调用 dropIndex(String) 接口。

以下是数据库升级的一个例子:

在第一个版本中,Sample 的模型绑定定义如下,并在数据库创建了以之对应的表 sampleTable。

//Java
@WCDBTableCoding
public class Sample {
 @WCDBField public int id;
 @WCDBField public String content;
 @WCDBField public int createDate;
}

database.createTable("sampleTable", DBSample.INSTANCE);
//Kotlin
@WCDBTableCoding
class Sample {
 @WCDBField var id = 0
 @WCDBField var content: String? = null
 @WCDBField var createDate: Int = 0
}

database.createTable("sampleTable", DBSample)

到了第二个版本,sampleTable 表进行了升级。

//Java
@WCDBTableCoding
public class Sample {
 @WCDBField public int id;
 @WCDBField(columnName = "content") public String description;
 @WCDBField @WCDBIndex public String title;
}

database.createTable("sampleTable", DBSample.INSTANCE);
//Kotlin
@WCDBTableCoding
class Sample {
 @WCDBField var id = 0
 @WCDBField(columnName = "content") var description: String? = null
 @WCDBField @WCDBIndex var title: String? = null
}

database.createTable("sampleTable", DBSample)

可以看到,通过修改模型绑定,并再次调用 createTable(String, TableBinding<T>)

  1. content 字段通过别名的特性,被重命名为了 description
  2. 已删除的 createDate 字段会被忽略。
  3. 对于新增的 title 会被添加到表中。
  4. 新增的索引sampleTable_title_index会被添加到表中。
Clone this wiki locally