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对于增删改查操作,都提供了对应的类以实现链式调用
WCTInsert
WCTDelete
WCTUpdate
WCTSelect
WCTRowSelect
WCTMultiSelect
|
|
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的执行,以确定代码符合预期
|
|