WCDB整理
[TOC]
WCDB
WCDB基于WINQ,引入了Objective-C++代码,因此对于所有引入WCDB的源文件,都需要将其后缀.m改为.mm。为减少影响范围,可以通过Objective-C的category特性将其隔离,达到只在model层使用Objective-C++编译,而不影响controller和view。
易用
WCDB通过WINQ和ORM,使得从拼接SQL、获取数据、拼装Object的整个过程,只需要一行代码即可完成。
NSArray<Message *> *messages = [wcdb getAllObjectsOfClass:Message.class fromTable:@"message"];
创建数据库
WCTDatabase通过指定路径进行创建。同时,该接口会自动创建路径中未创建的目录。
WCTDatabase* wcdb = [[WCTDatabase alloc] initWithPath:path];
打开数据库
WCDB会在第一次访问数据库时,自动打开数据库,不需要开发者主动操作。
canOpen接口可用于测试数据库能否正常打开,isOpened接口可用于测试数据库是否已打开。
建表与ORM
WCDB_IMPLEMENTATION(className)用于定义进行绑定的类WCDB_PROPERTY(propertyName)和WCDB_SYNTHESIZE(className, propertyName)用于声明和定义字段。WCDB_SYNTHESIZE(className, propertyName)默认使用属性名作为数据库表的字段名。对于属性名与字段名不同的情况,可以使用WCDB_SYNTHESIZE_COLUMN(className, propertyName, columnName)进行映射。WCDB_PRIMARY_AUTO_INCREMENT(className, propertyName)用于定义主键且自增。WCDB_INDEX(className, indexNameSubfix, propertyName)用于定义索引。
createTableAndIndexesOfName:withClass:该接口使用的是IF NOT EXISTS的SQL,因此可以用重复调用。不需要在每次调用前判断表或索引是否已经存在。
当定义发生变化时,该接口也会对应的增加字段或索引。因此,该接口可用于数据库表的升级
查询
NSArray<Message *> *messages = [wcdb getAllObjectsOfClass:Message.class fromTable:@"message"];
插入
[wcdb insertObject:message into:@"message"];
修改
[wcdb updateAllRowsInTable:@"message" onProperties:Message.modifiedTime withObject:message];
删除
[wcdb deleteAllObjects];
条件语句
WCDB通过WINQ完成条件语句
查询示例
|
|
自增插入示例
|
|
数值更新示例
|
|
部分删除示例
|
|
更多关于增删查改接口的用法,可参考:CRUD教程
特殊语句和核心层接口
WCDB的ObjC层接口封装了绝大部分场景下适用的增删查改语句。但SQL千变万化,接口层不可能覆盖全部场景。对于这种情况,可以通过WINQ的核心层接口进行调用。
对于SQL:EXPLAIN QUERY PLAN CREATE TABLE message(localID INTEGER)。
找到其对应的sql-stmt,然后通过以
WCDB::Statement开头的类进行调用。如例子中,其对应的sql-stmt为WCDB::StatementExplain和WCDB::StatementCreateTable。获取字段映射。对于已经定义ORM的字段,可以通过
className.propertyName获取,如:Message.localID。对于未定义ORM的字段,可以通过WCDB::Column columnName("columnName")创建,如WCDB::Column localID("localID")。根据
Statement内的定义,按照与SQL同名的函数调用获得完整的WINQ语句。如例子中,其对应的WINQ语句为12WCDB::ColumnDefList columnDefList = {WCTSampleORM.identifier.def(WCTColumnTypeInteger32, true)};WCDB::StatementExplain statementExplain = WCDB::StatementExplain().explainQueryPlan(WCDB::StatementCreateTable().create("message", columnDefList));通过
getDescription()打印log,调试确保SQL正确1NSLog(@"SQL: %s", statementExplain.getDescription().c_str());
执行WINQ
通过exec:执行WINQ statement。
|
|
获取WINQ运行结果
通过prepare:运行WINQ statement,获得WCTStatement,并以此获取返回值。
|
|
该接口风格与FMDB类似。
更多关于示例代码,可以参考核心层接口
事务
|
|
便捷事务接口
runTransaction:接口会在commit失败时自动rollback事务。开发者也可以在BLOCK结束时返回YES或NO来决定commit或rollback事务,以此减少代码量。
|
|
配置
|
|
关闭数据库
WCDB会自动管理这个过程。对于某一路径的数据库,WCDB会在所有对其的引用释放时,自动关闭数据库,并回收资源。
WCDB提供了close:接口确保完全关闭数据库,并阻塞其他线程的访问。
|
|
将一个已有的ObjC类进行ORM绑定的过程如下:
- 定义该类遵循
WCTTableCoding协议。可以在类声明上定义,也可以通过文件模版在category内定义。 - 使用
WCDB_PROPERTY宏在头文件声明需要绑定到数据库表的字段。 - 使用
WCDB_IMPLEMENTATIO宏在类文件定义绑定到数据库表的类。 - 使用
WCDB_SYNTHESIZE宏在类文件定义需要绑定到数据库表的字段。
除此之外,WCDB还提供了许多可选的宏,用于定义数据库索引、约束等,如:
WCDB_PRIMARY用于定义主键WCDB_INDEX用于定义索引WCDB_UNIQUE用于定义唯一约束WCDB_NOT_NULL用于定义非空约束- …
基础类
WCDB提供了三个基础类进行数据库操作:WCTDatabase、WCTTable、WCTTransaction。它们的接口都是线程安全的。
WCTDatabase
WCTDatabase表示一个数据库,可以进行所有数据库操作,包括增删查改、表操作、事务、文件操作、损坏修复等。
WCDB提供了删除数据库、移动数据库、获取数据库占用空间和使用路径的文件操作接口。
文件操作的最佳实践是确保数据库已关闭。
可加密
WCTDatabase不支持跨线程事务。事务内的操作必须在同一个线程运行完。跨线程事务可以参考WCTTransaction。关于事务的例子,可以参考Sample-transaction。
WCTTable
WCTTable表示一个表。它等价于预设了class和tableName的WCTDatabase,仅可以进行数据的增删查改等。
WCTTransaction
WCTTransaction表示一个事务。
与WCTDatabase的事务不同,WCTTransaction可以在函数和对象之间传递,实现跨线程的事务。
关于事务的例子,可以参考Sample-transaction。
基础类共享
对于同一个路径的数据库,不同的WCTDatabase、WCTTable、WCTTransaction对象共享同一个WCDB核心。因此,你可以在代码的不同位置、不同线程任意创建不同的基础类对象,WCDB会自动管理它们的共享数据和线程并发。
|
|
CRUD
WCDB的增删改查分为表操作和数据操作两种。
表操作
表操作包括创建/删除 表/索引、判断表、索引是否存在等。WCTDatabase和WCTransaction都支持表操作的接口。
开发者可以根据ORM的定义创建表或索引:
|
|
也可以通过WINQ自定义表或索引:
|
|
关于表操作的例子,可以参考Sample-table
数据操作
数据操作分为便捷接口和链式接口两种。WCTDatabase、WCTTable、WCTTransaction均支持数据操作接口。
便捷接口
便捷接口的设计原则为,通过一行代码即可完成数据的操作。
插入
insertObject:into:和insertObjects:into:,插入单个或多个对象insertOrReplaceObject:into和insertOrReplaceObjects:into,插入单个或多个对象。当对象的主键在数据库内已经存在时,更新数据;否则插入对象。insertObject:onProperties:into:和insertObjects:onProperties:into:,插入单个或多个对象的部分属性insertOrReplaceObject:onProperties:into和insertOrReplaceObjects:onProperties:into,插入单个或多个对象的部分属性。当对象的主键在数据库内已经存在时,更新数据;否则插入对象。
删除
deleteAllObjectsFromTable:删除表内的所有数据deleteObjectsFromTable:后可组合接where、orderBy、limit、offset以删除部分数据
更新
updateAllRowsInTable:onProperties:withObject:,通过object更新数据库中所有指定列的数据updateRowsInTable:onProperties:withObject:后可组合接where、orderBy、limit、offset以通过object更新指定列的部分数据updateAllRowsInTable:onProperty:withObject:,通过object更新数据库某一列的数据updateRowsInTable:onProperty:withObject:后可组合接where、orderBy、limit、offset以通过object更新某一列的部分数据updateAllRowsInTable:onProperties:withRow:,通过数组更新数据库中的所有指定列的数据updateRowsInTable:onProperties:withRow:后可组合接where、orderBy、limit、offset以通过数组更新指定列的部分数据updateAllRowsInTable:onProperty:withRow:,通过数组更新数据库某一列的数据updateRowsInTable:onProperty:withRow:后可组合接where、orderBy、limit、offset以通过数组更新某一列的部分数
查找
getOneObjectOfClass:fromTable:后可接where、orderBy、limit、offset以从数据库中取出一行数据并组合成objectgetOneObjectOnResults:fromTable:后可接where、orderBy、limit、offset以从数据库中取出一行数据的部分列并组合成objectgetOneRowOnResults:fromTable:后可接where、orderBy、limit、offset以从数据库中取出一行数据的部分列并组合成数组getOneColumnOnResult:fromTable:后可接where、orderBy、limit、offset以从数据库中取出一列数据并组合成数组getOneDistinctColumnOnResult:fromTable:后可接where、orderBy、limit、offset以从数据库中取出一列数据,并取distinct后组合成数组。getOneValueOnResult:fromTable:后可接where、orderBy、limit、offset以从数据库中取出一行数据的某一列getAllObjectsOfClass:fromTable:,取出所有数据,并组合成objectgetObjectsOfClass:fromTable:后可接where、orderBy、limit、offset以从数据库中取出一部分数据,并组合成objectgetAllObjectsOnResults:fromTable:,取出所有数据的指定列,并组合成objectgetObjectsOnResults:fromTable:后可接where、orderBy、limit、offset以从数据库中取出一部分数据的指定列,并组合成objectgetAllRowsOnResults:fromTable:,取出所有数据的指定列,并组合成数组getRowsOnResults:fromTable:后可接where、orderBy、limit、offset以从数据库中取出一部分数据的指定列,并组合成数组
具体例子可直接参考Sample-convenience
链式接口
链式调用是指对象的接口返回一个对象,从而允许在单个语句中将调用链接在一起,而不需要变量来存储中间结果。
WCDB对于增删改查操作,都提供了对应的类以实现链式调用
WCTInsertWCTDeleteWCTUpdateWCTSelectWCTRowSelectWCTMultiSelect
|
|
where、orderBy、limit等接口的返回值均为self,因此可以通过链式调用,更自然更灵活的写出对应的查询。
开发者可以通过链式接口获取数据库操作的耗时、错误信息;也可以通过遍历逐个生成object。
|
|
关于链式接口的例子,请参考Sample-chaincall。
封装SQL
|
|
结合WINQ,开发者可以用核心层接口执行其他未封装的复杂SQL。
|
|
调试SQL
[WCTStatistics SetGlobalSQLTrace:]会监控所有执行的SQL,该接口可用于调试,确定SQL是否执行正确。
|
|
关于核心层接口的例子,请参考Sample-core。
主键自增(Auto Increment)
对于主键自增的类,需要在ORM定义WCDB_PRIMARY_AUTO_INCREMENT(className, propertyName),然后通过isAutoIncrement接口设置自增属性,并通过lastInsertedRowID接口获取插入的RowID。
|
|
as重定向
基于ORM的支持,我们可以从数据库直接取出一个Object。然而,有时候需要取出并非是某个字段,而是有一些组合。例如:
|
|
这段代码从数据库中取出了消息的最新的修改时间,并以此将此时间作为消息的创建时间,新建了一个message。这种情况下,就可以使用as重定向。
as重定向,它可以将一个查询结果重定向到某一个字段,如下:
|
|
通过as(Message.createTime)的语法,将查询结果重新指向了createTime。因此只需一行代码便可完成原来的任务。
多表查询
SQLite支持联表查询,在某些特定的场景下,可以起到优化性能、简化表结构的作用。
WCDB同样提供了对应的接口,并在ORM的支持下,通过WCTMultiSelect的链式接口,可以同时从表中取出多个类的对象。
|
|
ORM
ORM宏
WCDB使用内置的宏来连接类、属性与表、字段。共有三类宏,分别对应数据库的字段、索引和约束。所有宏都定义在WCTCodingMacro.h中。
关于字段、索引、约束的具体描述及用法,请参考SQLite的相关文档:Create Table和Create Index。
字段宏
字段宏以WCDB_SYNTHESIZE开头,定义了类属性与字段之间的联系。支持自定义字段名和默认值。
WCDB_SYNTHESIZE(className, propertyName)是最简单的用法,它直接使用propertyName作为数据库字段名。WCDB_SYNTHESIZE_COLUMN(className, propertyName, columnName)支持自定义字段名。WCDB_SYNTHESIZE_DEFAULT(className, propertyName, defaultValue)支持自定义字段的默认值。默认值可以是任意的C类型或NSString、NSData、NSNumber、NSNull。WCDB_SYNTHESIZE_COLUMN_DEFAULT(className, propertyName, columnName, defaultValue)为以上两者的组合。
关于字段宏的例子,请参考WCTSampleORM。
索引宏
索引宏以WCDB_INDEX开头,定义了数据库的索引属性。支持定义索引的排序方式。
WCDB_INDEX(className, indexSubfixName, propertyName)是最简单的用法,它直接定义某个字段为索引。同时,WCDB会将tableName+indexSubfixName作为该索引的名称。WCDB_INDEX_ASC(className, indexSubfixName, propertyName)定义索引为升序。WCDB_INDEX_DESC(className, indexSubfixName, propertyName)定义索引为降序。WCDB_UNIQUE_INDEX(className, indexSubfixName, propertyName)定义唯一索引。WCDB_UNIQUE_INDEX_ASC(className, indexSubfixName, propertyName)定义唯一索引为升序。WCDB_UNIQUE_INDEX_DESC(className, indexSubfixName, propertyName)定义唯一索引为降序。
多字段索引
WCDB通过indexSubfixName匹配多索引。相同的indexSubfixName会被组合为多字段索引。
|
|
关于索引宏的例子,请参考WCTSampleORMIndex。
约束宏
约束宏包括字段约束和表约束
字段约束
主键约束以
WCDB_PRIMARY开头,定义了数据库的主键,支持自定义主键的排序方式、是否自增。WCDB_PRIMARY(className, propertyName)是最基本的用法,它直接使用propertyName作为数据库主键。WCDB_PRIMARY_ASC(className, propertyName)定义主键升序。WCDB_PRIMARY_DESC(className, propertyName)定义主键降序。WCDB_PRIMARY_AUTO_INCREMENT(className, propertyName)定义主键自增。WCDB_PRIMARY_ASC_AUTO_INCREMENT(className, propertyName)是主键自增和升序的组合。WCDB_PRIMARY_DESC_AUTO_INCREMENT(className, propertyName)是主键自增和降序的组合。
非空约束为
WCDB_NOT_NULL(className, propertyName),当该字段插入数据为空时,数据库会返回错误。- 唯一约束为
WCDB_UNIQUE(className, propertyName),当该字段插入数据与其他列冲突时,数据库会返回错误。
关于字段约束的例子,请参考WCTSampleORMColumnConstraint
表约束
- 多主键约束以
WCDB_MULTI_PRIMARY开头,定义了数据库的多主键,支持自定义每个主键的排序方式。WCDB_MULTI_PRIMARY(className, constraintName, propertyName)是最基本的用法,与索引类似,多个主键通过constraintName匹配。WCDB_MULTI_PRIMARY_ASC(className, constraintName, propertyName)定义了多主键propertyName对应的主键升序。WCDB_MULTI_PRIMARY_DESC(className, constraintName, propertyName)定义了多主键中propertyName对应的主键降序。
- 多字段唯一约束以
WCDB_MULTI_UNIQUE开头,定义了数据库的多字段组合唯一,支持自定义每个字段的排序方式。WCDB_MULTI_UNIQUE(className, constraintName, propertyName)是最基本的用法,与索引类似,多个字段通过constraintName匹配。WCDB_MULTI_UNIQUE_ASC(className, constraintName, propertyName)定义了多字段中propertyName对应的字段升序。WCDB_MULTI_UNIQUE_DESC(className, constraintName, propertyName)定义了多字段中propertyName对应的字段降序。
关于表约束的例子,请参考WCTSampleORMTableConstraint
类与数据库对应类型
SQLite数据库的字段有整型、浮点数、字符串、二进制数据等五种类型。WCDB的ORM会自动识别property的类型,并映射到适合的数据库类型。其对应关系为:
| C类型 | 数据库类型 |
|---|---|
整型(包括但不限于int、unsigned、long、unsigned long、long long、unsigned long long等所有基于整型的C基本类型) |
整型(INTEGER) |
枚举型(enum及所有基于枚举型的C基本类型) |
整型(INTEGER) |
浮点数(包括但不限于float、double、long double等所有基于浮点型的C基本类型) |
浮点型( REAL) |
字符串(const char *的C字符串类型) |
字符串( TEXT) |
| Objective-C类型 | 数据库类型 |
|---|---|
| Objective-C类型 | 数据库类型 |
|---|---|
NSDate |
整型(INTEGER) |
NSNumber |
浮点型( REAL) |
NSString、NSMutableString |
字符串( TEXT) |
其他所有符合NSCoding协议的NSObject子类 |
二进制(BLOB) |
自定义类-数据库类型映射
WCDB支持开发者自定义绑定类型。
类只需实现WCTColumnCoding协议,即可支持绑定。
|
|
columnTypeForWCDB接口定义类对应数据库中的类型。unarchiveWithWCTValue:接口定义从数据库类型反序列化到类的转换方式。archivedWCTValue接口定义从类序列化到数据库类型的转换方式。
为了简化定义,WCDB提供了Xcode文件模版来创建类字段绑定。
取消内置类型
上面提到WCDB内置对NSData、NSArray等等常用的Objective-C类型的支持,并且基于NSCoding协议进行序列化和反序列化。这些内置绑定的实现都在 builtin 目录下,这些实现也可以作为例子参考。
若开发者希望自定义基本类型的绑定,可以将内置的绑定关闭。关闭方法为:删除工程文件的Build Settings->Preprocessor Macros下各个scheme的WCDB_BUILTIN_COLUMN_CODING宏。
关于自定义类型的例子,请参考sample_advance。
修改字段
增
对于需要增加的字段,只需在定义处添加,并再次执行createTableAndIndexesOfName:withClass:即可。
|
|
删
对于需要删除字段,只需将其定义删除即可。
|
|
由于SQLite不支持删除字段,因此,删除定义后,WCDB只是将该字段忽略,其旧数据依然存在在数据库内,但新增加的数据基本不会因为该字段产生额外的性能和空间损耗。
改
由于SQLite不支持修改字段名称,因此WCDB使用WCDB_SYNTHESIZE_COLUMN(className, propertyName, columnName)重新映射宏。
对于已经定义的字段WCDB_SYNTHESIZE(MyClass, myValue)可以修改为WCDB_SYNTHESIZE_COLUMN(MyClass, newMyValue, "myValue")。
对于已经定义的字段类型,可以任意修改为其他类型。但旧数据会使用新类型的解析方式进行反序列化,因此需要确保其兼容性。
更多扩展性
由于ORM不可能覆盖所有用法,因此WCDB提供了 core 接口,开发者可以根据自己的需求执行SQL。请参考 核心层接口
如果这些接口仍不满足你的需求,欢迎给我们提 Issue 。
注意事项
- 由于
WCDB_SYNTHESIZE(className, propertyName)宏默认使用propertyName作为字段名,因此在修改propertyName后,会导致错误,需用WCDB_SYNTHESIZE_COLUMN(className, newPropertyName, "oldPropertyName")重新映射。 - 每个进行ORM的property,都必须实现getter/setter。因为ORM会在初始化时通过objc-runtime获取property的getter/setter的
IMP,以此实现通过object存取数据库的特性。getter/setter不必须是公开的,也可以是私有接口。
全局监控与错误处理
错误处理WCTError
WCTError继承自NSError,包含了WCDB错误的所有信息,以供调试或发现问题。
type表示错误的类型,不同类型的错误其错误码和拥有的信息不同。其对应关系如下
| Type | Code |
|---|---|
| SQLite,表示该错误来自SQLite接口 | 请参考rescode |
| SystemCall,表示该错误来自系统调用 | 请参考errno |
| Core,表示该错误来自WCDB Core层 | 请参考源码的error.hpp |
| Interface,表示该错误来自WCDB Interface层 | 请参考源码的error.hpp |
| Abort,表示中断,该错误一般是开发错误,应该在发布前修复 | / |
| Warning,表示警告,建议修复 | / |
| SQLiteGlobal,表示该信息来自SQLite的log接口,一般只作为debug log | 请参考rescode |
其他错误信息通过infoForKey接口获得,包括:
- Tag,正在操作的数据库的tag
- Operation,正在进行的操作,请参考error.hpp
- Extended Code,SQLite的扩展码,请参考rescode
- Message,错误信息
- SQL,发生错误时正在执行的SQL
- Path,发生错误时正在操作的文件的路径。
获取错误
由于便捷接口的设计原则是易用,因此不提供获取错误的方式。错误处理需使用链式接口
|
|
开发者也可以注册全局的错误接口,以调试、上报、打log
|
|
性能监控
WCDB支持获取单次操作的耗时,也支持对单个DB或全局注册统一接口监控性能。
所有性能监控都会有少量的性能损坏,请根据需求开启。
操作耗时
由于便捷接口的设计原则是易用,因此不提供获取错误的方式。操作耗时需使用链式接口。
首先安通过setStatisticsEnabled:打开耗时监控
|
|
在操作执行完成后,通过cost接口获取耗时
|
|
监控耗时
WCDB支持对所有SQL操作进行全局监控,也支持监控单个特定的数据库。
所有监控的返回数据都相同,包括三个数据:
- Tag,执行操作的数据库的tag
- sqls,执行的SQL和对应的次数
- 对于非事务操作,则为单条SQL
- 对于事务操作,则为该次事务所执行的所有SQL和每个sql执行的次数
- cost,耗时
全局监控
监控所有db的数据库操作耗时,该接口需要在所有db打开、操作之前调用。
|
|
特定数据库监控
对于特定的数据库,该接口会覆盖全局监控的注册。
|
|
操作耗时与监控耗时的不同
- 操作耗时的
cost返回的耗时为浮点数的秒,监控耗时的cost返回的耗时为整型的纳秒。 - 监控耗时仅包括SQL在SQLite层面的耗时,包括SQL的编译、I/O等。而操作耗时除以上之外,还包括了WCDB层面对类封装等产生的耗时
SQL执行监控
WCDB可以监控所有SQL的执行,以确定代码符合预期
|
|
