Skip to content

C++ 模型绑定

qiuwenchen edited this page Mar 7, 2024 · 7 revisions

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

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

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

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

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

字段映射

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

// Sample.hpp
class Sample {
public:
    Sample();//必须要有默认构造函数
    Sample(int identifier, const std::string& content);//非必须实现的构造函数,只是为了演示方便
    // 只支持绑定 public 的成员变量
    int identifier;
    std::string content;
    int offset;
    WCDB_CPP_ORM_DECLARATION(Sample)
private:
    std::string debugContent;
};

// Sample.cpp
WCDB_CPP_ORM_IMPLEMENTATION_BEGIN(Sample)
WCDB_CPP_SYNTHESIZE_COLUMN(identifier, "id")
WCDB_CPP_SYNTHESIZE(content)
WCDB_CPP_SYNTHESIZE_COLUMN(offset, "db_offset")
WCDB_CPP_ORM_IMPLEMENTATION_END

将一个C++类或结构体进行ORM绑定的过程如下:

  • 使用WCDB_CPP_ORM_DECLARATION宏在头文件的类或结构体中声明它实现了模型绑定。
  • 使用WCDB_CPP_ORM_IMPLEMENTATION_BEGIN宏在类实现文件中定义绑定到数据库表的类。
  • 使用WCDB_CPP_SYNTHESIZE宏在类实现中配置需要绑定到数据库表的字段,这样数据库中的列名和字段名是一样的。
  • 对于字段名与表的列名不一样的情况,可以使用别名进行映射,如 WCDB_CPP_SYNTHESIZE_COLUMN(identifier, "id")
  • 对于字段名与 SQLite 的保留关键字冲突的字段,同样可以使用别名进行映射,如 offset 是 SQLite 的关键字,就需要WCDB_CPP_SYNTHESIZE_COLUMN(offset, "db_offset")
  • 对于不需要写入数据库的字段,则不需要用WCDB_CPP_SYNTHESIZE声明,比如debugContent字段。
  • WCDB_CPP_ORM_IMPLEMENTATION_END标记模型绑定部分内容结束。

模型绑定相关内容都需要写到WCDB_CPP_ORM_DECLARATIONWCDB_CPP_ORM_IMPLEMENTATION_END之间,且不能添加其他不是模型绑定宏的逻辑。

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

// 以下代码等效于 SQL:CREATE TABLE IF NOT EXISTS sampleTable(id INTEGER, content TEXT, db_offset INTEGER)
bool ret = database.createTable<Sample>("sampleTable");

字段映射的类型

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

C类型 数据库类型
整型(包括但不限于intunsignedlongunsigned longlong longunsigned long long等所有基于整型的 C 基本类型) 整型(INTEGER)
枚举型(enum及所有基于枚举型的C基本类型) 整型(INTEGER)
浮点数(包括但不限于floatdouble等所有基于浮点型的C基本类型) 浮点型( REAL)
字符串(C字符串类型char *,标准库字符串std::string,WCDB内部字符串类型WCDB::StringView 字符串( TEXT)
二进制数组(标准库二进制数组std::vector<unsigned char>,WCDB内部二进制类型WCDB::Data 二进制(BLOB)

上面提到的类型的std::optionalstd::shared_ptr类型也支持绑定为DB字段。

对于没有内建支持的类型,暂不支持模型绑定,后面我们会扩展更多类型。

字段约束

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

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

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

    • WCDB_CPP_PRIMARY_ASC(fieldName)定义主键升序。

    • WCDB_CPP_PRIMARY_DESC(fieldName)定义主键降序。

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

    • WCDB_CPP_PRIMARY_ASC_AUTO_INCREMENT(fieldName)是主键自增和升序的组合。

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

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

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

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

// Sample.cpp
WCDB_CPP_ORM_IMPLEMENTATION_BEGIN(Sample)
WCDB_CPP_SYNTHESIZE_COLUMN(identifier, "id")
WCDB_CPP_SYNTHESIZE(content)
WCDB_CPP_SYNTHESIZE_COLUMN(offset, "db_offset")

WCDB_CPP_PRIMARY(identifier)
WCDB_CPP_NOT_NULL(content)
WCDB_CPP_DEFAULT(offset, 0)
WCDB_CPP_ORM_IMPLEMENTATION_END

自增属性

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

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

Sample autoIncrementObject;
autoIncrementObject.isAutoIncrement = true;

// 插入自增数据
bool ret = database.insertObjects<Sample>(autoIncrementObject, "sampleTable");
if(ret) {
  printf("%lld", *autoIncrementObject.lastInsertedRowID); // 输出 1
}

// 再次插入自增数据
ret &= database.insertObjects<Sample>(autoIncrementObject, "sampleTable");
if(ret) {
    printf("%lld", *autoIncrementObject.lastInsertedRowID); // 输出 2
}

// 插入非自增的指定数据
Sample specificObject;
specificObject.identifier = 10;
ret &= database.insertObjects<Sample>(specificObject, "sampleTable");

表约束

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

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

// Sample.cpp
WCDB_CPP_ORM_IMPLEMENTATION_BEGIN(Sample)
WCDB_CPP_SYNTHESIZE_COLUMN(identifier, "id")
WCDB_CPP_SYNTHESIZE(content)
WCDB_CPP_SYNTHESIZE_COLUMN(offset, "db_offset")

WCDB_CPP_MULTI_PRIMARY("primary_identifier_offset", identifier)
WCDB_CPP_MULTI_PRIMARY("primary_identifier_offset", offset)

WCDB_CPP_MULTI_UNIQUE("unique_identifier_offset", identifier)
WCDB_CPP_MULTI_UNIQUE("unique_identifier_offset", offset)
WCDB_CPP_ORM_IMPLEMENTATION_END

索引

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

  • WCDB_CPP_INDEX(indexSubfixName, fieldName)是最简单的用法,它直接定义某个字段为索引。同时,WCDB会将tableName+indexSubfixName作为该索引的名称。
  • WCDB_CPP_INDEX_ASC(indexSubfixName, fieldName)定义索引为升序。
  • WCDB_CPP_INDEX_DESC(indexSubfixName, fieldName)定义索引为降序。
  • WCDB_CPP_UNIQUE_INDEX(indexSubfixName, fieldName)定义唯一索引。
  • WCDB_CPP_UNIQUE_INDEX_ASC(indexSubfixName, fieldName)定义唯一索引为升序。
  • WCDB_CPP_UNIQUE_INDEX_DESC(indexSubfixName, fieldName)定义唯一索引为降序。

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

// Sample.cpp
WCDB_CPP_ORM_IMPLEMENTATION_BEGIN(Sample)
WCDB_CPP_SYNTHESIZE_COLUMN(identifier, "id")
WCDB_CPP_SYNTHESIZE(content)
WCDB_CPP_SYNTHESIZE_COLUMN(offset, "db_offset")

WCDB_CPP_INDEX_ASC("_index", identifier)
WCDB_CPP_INDEX("_multiIndex", identifier)
WCDB_CPP_INDEX("_multiIndex", offset)
WCDB_CPP_ORM_IMPLEMENTATION_END

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

虚拟表映射

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

数据库升级

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

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

对于字段映射:

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

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

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

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

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

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

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

// Sample.hpp
class Sample {
public:
    int identifier;
    std::string discription;
    double createDate;
    WCDB_CPP_ORM_DECLARATION(Sample)
};

// Sample.cpp
WCDB_CPP_ORM_IMPLEMENTATION_BEGIN(Sample)
WCDB_CPP_SYNTHESIZE(identifier)
WCDB_CPP_SYNTHESIZE(discription)
WCDB_CPP_SYNTHESIZE(createDate)
WCDB_CPP_ORM_IMPLEMENTATION_END

database.createTable<Sample>("sampleTable");

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

// Sample.hpp
class Sample {
public:
    Sample();
    Sample(int identifier, const std::string& content);
    int identifier;
    std::string content;
    std::string title;
    WCDB_CPP_ORM_DECLARATION(Sample)
};

// Sample.cpp
WCDB_CPP_ORM_IMPLEMENTATION_BEGIN(Sample)
WCDB_CPP_SYNTHESIZE(identifier)
WCDB_CPP_SYNTHESIZE_COLUMN(content, "discription")
WCDB_CPP_SYNTHESIZE(title)

WCDB_CPP_INDEX("_index", identifier)
WCDB_CPP_ORM_IMPLEMENTATION_END

database.createTable<Sample>("sampleTable");

可以看到,通过修改模型绑定,并再次调用 createTable

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