Skip to content

Objc 模型绑定

qiuwenchen edited this page Mar 7, 2024 · 3 revisions

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

WCDB使用内置的宏来连接类、属性与表、字段。共有三类宏,分别对应数据库的字段、索引和约束。所有宏都定义在WCTMacro.h中。

关于字段、索引、约束的具体描述及用法,请参考SQLite的相关文档:Create TableCreate Index

WCDB Objc 的模型绑定分为五个部分:

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

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

字段映射

字段映射主要使用WCDB_PROPERTY宏来声明,用WCDB_SYNTHESIZE系列宏来实现。以下是一个字段映射的示例代码:

@interface Sample : NSObject<WCTTableCoding>

@property(nonatomic, assign) int identifier;
@property(nonatomic, strong) NSString* content;
@property(nonatomic, assign) int offset;
@property(nonatomic, strong) NSString* debugContent;

WCDB_PROPERTY(identifier)
WCDB_PROPERTY(content);
WCDB_PROPERTY(offset)

@end

@implementation Sample

WCDB_IMPLEMENTATION(Sample)

WCDB_SYNTHESIZE_COLUMN(identifier, "id")
WCDB_SYNTHESIZE(content)
WCDB_SYNTHESIZE_COLUMN(offset, "db_offset")

@end

将一个ObjC类进行ORM绑定的过程如下:

  • 定义该类遵循WCTTableCoding协议。可以在类声明上定义,也可以通过[文件模版][WCTTableCoding-file-template]在category内定义。
  • 使用WCDB_PROPERTY宏在头文件声明需要绑定到数据库表的字段。对于不需要写入数据库的字段,则不需要声明,比如debugContent字段。
  • 使用WCDB_IMPLEMENTATION宏在类文件定义绑定到数据库表的类。
  • 使用WCDB_SYNTHESIZE宏在类文件定义需要绑定到数据库表的字段,这样数据库中的列名和字段名是一样的。
  • 对于字段名与表的列名不一样的情况,可以使用别名进行映射,如 WCDB_SYNTHESIZE_COLUMN(identifier, "id")
  • 对于字段名与 SQLite 的保留关键字冲突的字段,同样可以使用别名进行映射,如 offset 是 SQLite 的关键字,就需要WCDB_SYNTHESIZE_COLUMN(offset, "db_offset")

字段映射定义完成后,调用 createTable:withClass: 接口即可根据这个定义创建表。

// 以下代码等效于 SQL:CREATE TABLE IF NOT EXISTS sampleTable(id INTEGER, content TEXT, db_offset INTEGER)
BOOL ret = [database createTable:@"sampleTable" withClass:Sample.class];

字段映射的类型

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

C类型 数据库类型
整型(包括但不限于intunsignedlongunsigned longlong longunsigned long long等所有基于整型的C基本类型) 整型(INTEGER)
枚举型(enum及所有基于枚举型的C基本类型) 整型(INTEGER)
浮点数(包括但不限于floatdoublelong double等所有基于浮点型的C基本类型) 浮点型( REAL)
字符串(const char *的C字符串类型) 字符串( TEXT)
Objective-C类型 数据库类型
NSDate 浮点型(REAL)
NSNumber 浮点型(REAL)
NSStringNSMutableString 字符串(TEXT)
其他所有符合NSCoding协议的NSObject子类 二进制(BLOB)

对于没有内建支持的类型,开发者可以手动为其添加支持。我们将在自定义字段映射类型一章进一步介绍。

字段约束

字段约束是针对单个字段的约束,如主键约束、非空约束、唯一约束、默认值等。字段约束有下面这些宏实现,主要是写在实现文件中:

  • 主键约束以WCDB_PRIMARY开头,定义了数据库的主键,支持自定义主键的排序方式、是否自增。

    • WCDB_PRIMARY(propertyName)是最基本的用法,它直接使用propertyName作为数据库主键。

    • WCDB_PRIMARY_ASC(propertyName)定义主键升序。

    • WCDB_PRIMARY_DESC(propertyName)定义主键降序。

    • WCDB_PRIMARY_AUTO_INCREMENT(propertyName)定义主键自增,下面会有详细介绍。

    • WCDB_PRIMARY_ASC_AUTO_INCREMENT(propertyName)是主键自增和升序的组合。

  • 非空约束为WCDB_NOT_NULL(propertyName),当该字段插入数据为空时,数据库会返回错误。

  • 默认值约束为WCDB_DEFAULT(propertyName, defaultValue),默认值可以是任意的C类型或NSStringNSDataNSNumberNSNull

  • 唯一约束为WCDB_UNIQUE(propertyName),当该字段插入数据与其他列冲突时,数据库会返回错误。

  • 使用表达式建立约束WCDB_CHECK(propertyName, condition)

以下是对上面给出的Sample类添加字段约束的示例代码:

@implementation Sample

WCDB_IMPLEMENTATION(Sample)

WCDB_SYNTHESIZE_COLUMN(identifier, "id")
WCDB_SYNTHESIZE(content)
WCDB_SYNTHESIZE_COLUMN(offset, "db_offset")

WCDB_PRIMARY(identifier)
WCDB_NOT_NULL(content)
WCDB_DEFAULT(offset, 0)
WCDB_CHECK(identifier, Sample.identifier > 1000 && Sample.identifier < 10000)

@end

自增属性

定义了 WCDB_PRIMARY_AUTO_INCREMENT 的字段,支持以自增的方式进行插入数据。但仍可以通过非自增的方式插入数据。

当需要进行自增插入时,对象需设置 isAutoIncrement 属性为 YES,则数据库会使用 已有数据中最大的值+1 作为主键的值。

Sample* autoIncrementObject = [[Sample alloc] init];
autoIncrementObject.isAutoIncrement = YES;

// 插入自增数据
BOOL ret = [database insertObject:autoIncrementObject intoTable:@"sampleTable"];
if(ret) {
  NSLog(@"%lld", autoIncrementObject.lastInsertedRowID); // 输出 1
}

// 再次插入自增数据
ret = [database insertObject:autoIncrementObject intoTable:@"sampleTable"];
if(ret) {
  NSLog(@"%lld", autoIncrementObject.lastInsertedRowID); // 输出 2
}

// 插入非自增的指定数据
Sample* specificObject = [[Sample alloc] init];
specificObject.identifier = 10;
ret = [database insertObject:specificObject intoTable:@"sampleTable"];

WCDB没有默认实现lastInsertedRowID这个属性,要使用它就要在model中使用@synthesize lastInsertedRowID来将它实现一下,否则调用时会因为找不到属性导致闪退。

表约束

  • 多主键约束以WCDB_MULTI_PRIMARY开头,定义了数据库的多主键,支持自定义每个主键的排序方式。
    • WCDB_MULTI_PRIMARY(constraintName, propertyName)是最基本的用法,与索引类似,多个主键通过constraintName匹配。
    • WCDB_MULTI_PRIMARY_ASC(constraintName, propertyName)定义了多主键propertyName对应的主键升序。
    • WCDB_MULTI_PRIMARY_DESC(constraintName, propertyName)定义了多主键中propertyName对应的主键降序。
  • 多字段唯一约束以WCDB_MULTI_UNIQUE开头,定义了数据库的多字段组合唯一,支持自定义每个字段的排序方式。
    • WCDB_MULTI_UNIQUE(constraintName, propertyName)是最基本的用法,与索引类似,多个字段通过constraintName匹配。
    • WCDB_MULTI_UNIQUE_ASC(constraintName, propertyName)定义了多字段中propertyName对应的字段升序。
    • WCDB_MULTI_UNIQUE_DESC(constraintName, propertyName)定义了多字段中propertyName对应的字段降序。
  • 无Rowid约束 WCDB_WITHOUT_ROWID,这种适用于一些简单表,具体见SQLite-WITHOUT ROWID Optimization

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

@implementation Sample

WCDB_IMPLEMENTATION(Sample)

WCDB_SYNTHESIZE_COLUMN(identifier, "id")
WCDB_SYNTHESIZE(content)
WCDB_SYNTHESIZE_COLUMN(offset, "db_offset")

WCDB_MULTI_PRIMARY("primary_identifier_offset", identifier)
WCDB_MULTI_PRIMARY("primary_identifier_offset", offset)

WCDB_MULTI_UNIQUE("unique_identifier_offset", identifier)
WCDB_MULTI_UNIQUE("unique_identifier_offset", offset)

@end

索引

索引宏以WCDB_INDEX开头,定义了数据库的索引属性。支持定义索引的排序方式。

  • WCDB_INDEX(indexSubfixName, propertyName)是最简单的用法,它直接定义某个字段为索引。同时,WCDB会将tableName+indexSubfixName作为该索引的名称。
  • WCDB_INDEX_ASC(indexSubfixName, propertyName)定义索引为升序。
  • WCDB_INDEX_DESC(indexSubfixName, propertyName)定义索引为降序。
  • WCDB_UNIQUE_INDEX(indexSubfixName, propertyName)定义唯一索引。
  • WCDB_UNIQUE_INDEX_ASC(indexSubfixName, propertyName)定义唯一索引为升序。
  • WCDB_UNIQUE_INDEX_DESC(indexSubfixName, propertyName)定义唯一索引为降序。

WCDB通过indexSubfixName匹配多索引。相同的indexSubfixName会被组合为多字段索引,而且索引中的字段顺序按照宏的声明次序。下面是Sample类使用索引的示例:

@implementation Sample

WCDB_IMPLEMENTATION(Sample)

WCDB_SYNTHESIZE_COLUMN(identifier, "id")
WCDB_SYNTHESIZE(content)
WCDB_SYNTHESIZE_COLUMN(offset, "db_offset")

WCDB_INDEX_ASC("_index", identifier)
WCDB_INDEX("_multiIndex", identifier)
WCDB_INDEX("_multiIndex", offset)

@end

使用这些宏定义的索引的名字都是表名拼接indexSubfixName。比如表名为sampleTable,上面两个索引的名字分别是sampleTable_indexsampleTable_multiIndex

虚拟表映射

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

数据库升级

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

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

对于字段映射:

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

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

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

对于数据库已存在但模型绑定中未定义的索引,-[WCTTableProtocol createTable:withClass:] 接口不会自动将其删除。如果需要删除,开发者需要调用 -[WCTTableProtocol dropIndex:] 接口,或者使用WCDB_INDEX_TO_BE_DROPPED显式声明需要删除索引。

因为建索引需要遍历原有数据,是个耗时操作,对旧表建索引要谨慎考虑性能问题。可以使用WCDB_INDEX_FOR_NEWLY_CREATED_TABLE_ONLY宏显式声明新增的索引只在新表中创建,而不在已有的表中添加。

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

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

@interface Sample : NSObject<WCTTableCoding>

@property(nonatomic, assign) int identifier;
@property(nonatomic, strong) NSString* discription;
@property(nonatomic, strong) NSDate* createDate;

WCDB_PROPERTY(identifier)
WCDB_PROPERTY(discription);
WCDB_PROPERTY(createDate)

@end

@implementation Sample

WCDB_IMPLEMENTATION(Sample)

WCDB_SYNTHESIZE(identifier)
WCDB_SYNTHESIZE(discription)
WCDB_SYNTHESIZE(createDate)

@end

[database createTable:@"sampleTable" withClass:Sample.class];

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

@interface Sample : NSObject<WCTTableCoding>

@property(nonatomic, assign) int identifier;
@property(nonatomic, strong) NSString* content;
@property(nonatomic, strong) NSString* title;

WCDB_PROPERTY(identifier)
WCDB_PROPERTY(content);
WCDB_PROPERTY(title)

@end

@implementation Sample

WCDB_IMPLEMENTATION(Sample)

WCDB_SYNTHESIZE(identifier)
WCDB_SYNTHESIZE_COLUMN(content, "discription")
WCDB_SYNTHESIZE(title)

WCDB_INDEX("_index", identifier)

@end

[database createTable:@"sampleTable" withClass:Sample.class];

可以看到,通过修改模型绑定,并再次调用 -[WCTTableProtocol createTable:withClass:]

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

文件与代码模版

模型绑定的大部分都是格式固定的代码,因此,WCDB Objc 提供了文件模版和代码模版两种方式,以简化模型绑定操作。 文件和代码模版都在源代码的 tools/templates 目录下

  • 未获取 WCDB 的 Github 仓库的开发者,可以在命令执行 curl https://raw.githubusercontent.com/Tencent/wcdb/master/tools/templates/install.sh -s | sh
  • 已获取 WCDB 的 Github 仓库的开发者,可以手动执行 cd path-to-your-wcdb-dir/tools/templates; sh install.sh;
  • 安装完成后重启Xcode,选择新建文件,滚到窗口底部,即可看到对应的文件模版。

文件模版

文件模版安装完成后,在 Xcode 的菜单 File -> New -> File... 中创建新文件,选择 TableCodable

在弹出的菜单中输入文件名,并选择 Language 为 Objective-C 即可。

这里以Sample类为例,Xcode会自动创建Sample+WCTTableCoding.h文件模版:

#import "Sample.h"
#import <WCDB/WCDB.h>

@interface Sample (WCTTableCoding) <WCTTableCoding>

WCDB_PROPERTY(<#property1 #>)
WCDB_PROPERTY(<#property2 #>)
WCDB_PROPERTY(<#property3 #>)
WCDB_PROPERTY(<#property4 #>)
WCDB_PROPERTY(<#... #>)

@end

加上类的ORM实现即可。

//Sample.h
#import <Foundation/Foundation.h>

@interface Sample : NSObject

@property(nonatomic, assign) int intValue;
@property(nonatomic, retain) NSString *stringValue;

@end
  
//WCTSampleAdvance.mm
@implementation Sample
  
WCDB_IMPLEMENTATION(Sample)
WCDB_SYNTHESIZE(intValue)
WCDB_SYNTHESIZE(stringValue)

WCDB_PRIMARY_ASC_AUTO_INCREMENT(intValue)

@end
  
//Sample+WCTTableCoding.h
#import "Sample.h"
#import <WCDB/WCDB.h>

@interface Sample (WCTTableCoding) <WCTTableCoding>

WCDB_PROPERTY(intValue)
WCDB_PROPERTY(stringValue)

@end

此时,原来的Sample.h中不包含任何C++的代码。因此,其他文件对其引用时,不需要修改文件名后缀。只有Model层需要使用WCDB接口的类,才需要包含Sample+WCTTableCoding.h,并修改文件名后缀为.mm

Clone this wiki locally