本教程主要介绍WCDB-iOS/macOS中的基础类、CRUD(增删改查)与transaction(事务)的用法。
阅读本教程前,建议先阅读iOS/macOS使用教程。
基础类
WCDB提供了三个基础类进行数据库操作:WCTDatabase
、WCTTable
、WCTTransaction
。它们的接口都是线程安全的。
WCTDatabase
WCTDatabase
表示一个数据库,可以进行所有数据库操作,包括增删查改、表操作、事务、文件操作、损坏修复等。
创建
WCTDatabase
通过initWithPath:
接口进行创建。该接口会同时创建path
中不存在的目录。
- NSString* path = @"~/Intermediate/Directories/Will/Be/Created/sample.db";
- WCTDatabase *database = [[WCTDatabase alloc] initWithPath:path];
- database.tag = 1;
打开
WCDB大量使用延迟初始化(Lazy initialization)的方式管理对象,因此SQLite连接会在第一次被访问时被打开。开发者不需要手动打开数据库。
通过canOpen
接口可测试数据库是否能够打开。通过isOpened
接口可测试数据库是否已经打开。
- if ([database canOpen]) {
- //...
- }
- if ([database isOpened]) {
- //...
- }
关闭
WCDB通过close
接口直接关闭数据库
- [database close];
由于WCDB支持多线程访问数据库,因此,该接口会阻塞等待所有数据库操作结束,以确保数据库完全关闭。
对于一个特定路径的数据库,WCDB会在所有对象对其的引用结束时,自动关闭数据库,并且回收内存和SQLite连接。因此,大部分情况下开发者不需要手动关闭数据库。
加密
WCDB提供基于sqlcipher的数据库加密功能,如下:
- WCTDatabase *database = [[WCTDatabase alloc] initWithPath:path];
- NSData *password = [@"MyPassword" dataUsingEncoding:NSASCIIStringEncoding];
- [database setCipherKey:password];
文件操作
WCDB提供了删除数据库、移动数据库、获取数据库占用空间和使用路径的文件操作接口。
- - (BOOL)removeFilesWithError:(WCTError **)error;
- - (BOOL)moveFilesToDirectory:(NSString *)directory withExtraFiles:(NSArray<NSString *> *)extraFiles andError:(WCTError **)error;
- - (NSArray<NSString *> *)getPaths;
- - (NSUInteger)getFilesSizeWithError:(WCTError **)error;
文件操作不是一个原子操作。若一个线程正在操作数据库,而另一个线程进行移动数据库文件,可能导致数据库损坏。因此,文件操作的最佳实践是确保数据库已关闭。
- [database close:^{
- WCTError *error = nil;
- BOOL ret = [database moveFilesToDirectory:otherDirectory withError:&error];
- if (!ret) {
- NSLog(@"Move files Error %@", error);
- }
- }];
关于文件操作的例子,可以参考Sample-file。
事务
WCTDatabase
不支持跨线程事务。事务内的操作必须在同一个线程运行完。可以通过两种方式运行事务:
beginTransaction
、commitTransaction
、rollbackTransaction
- //[beginTransaction], [commitTransaction], [rollbackTransaction] and all interfaces inside this transaction should run in same thread
- BOOL ret = [database beginTransaction];
- WCTSampleTransaction *object = [[WCTSampleTransaction alloc] init];
- ret = [database insertObject:object
- into:tableName];
- if (ret) {
- ret = [database commitTransaction];
- } else {
- ret = [database rollbackTransaction];
- }
runTransaction:
- BOOL commited = [database runTransaction:^BOOL {
- WCTSampleTransaction *object = [[WCTSampleTransaction alloc] init];
- BOOL ret = [database insertObject:object
- into:tableName];
- //return YES to do a commit and return NO to do a rollback
- if (ret) {
- return YES;
- }
- return NO;
- }
- event:^(WCTTransactionEvent event) {
- NSLog(@"Event %d", event);
- }];
跨线程事务可以参考WCTTransaction。
关于事务的例子,可以参考Sample-transaction。
WCTTable
WCTTable
表示一个表。它等价于预设了class
和tableName
的WCTDatabase
,仅可以进行数据的增删查改等。
- WCTTable* table = [database getTableOfName:tableName
- withClass:WCTSampleTable.class];
WCTTransaction
WCTTransaction
表示一个事务。
WCTTransaction *transaction = [database getTransaction];
与WCTDatabase
的事务不同,WCTTransaction
可以在函数和对象之间传递,实现跨线程的事务。
- //You can do a transaction in different threads using WCTTransaction.
- //But it's better to run serially, or an inner thread mutex will guarantee this.
- BOOL ret = [transaction begin];
- dispatch_async(dispatch_queue_create("other thread", DISPATCH_QUEUE_SERIAL), ^{
- WCTSampleTransaction *object = [[WCTSampleTransaction alloc] init];
- BOOL ret = [transaction insertObject:object
- into:tableName];
- if (ret) {
- [transaction commit];
- } else {
- [transaction rollback];
- }
- });
关于事务的例子,可以参考Sample-transaction。
基础类共享
对于同一个路径的数据库,不同的WCTDatabase
、WCTTable
、WCTTransaction
对象共享同一个WCDB核心。因此,你可以在代码的不同位置、不同线程任意创建不同的基础类对象,WCDB会自动管理它们的共享数据和线程并发。
- WCTDatabase* database1 = [[WCTDatabase alloc] initWithPath:path];
- WCTDatabase* database2 = [[WCTDatabase alloc] initWithPath:path];
- database1.tag = 1;
- NSLog(@"%d", database2.tag);//print 1
关于WCDB的架构和实现,可以参考:TODO
CRUD
WCDB的增删改查分为表操作和数据操作两种。
表操作
表操作包括创建/删除 表/索引、判断表、索引是否存在等。WCTDatabase
和WCTransaction
都支持表操作的接口。
开发者可以根据ORM的定义创建表或索引:
- BOOL ret = [database createTableAndIndexesOfName:tableName
- withClass:WCTSampleTable.class];
也可以通过WINQ自定义表或索引:
- BOOL ret = [database createTableOfName:tableName
- withColumnDefList:{
- WCTSampleTable.intValue.def(WCTColumnTypeInteger32),
- WCTSampleTable.stringValue.def(WCTColumnTypeString)
- }];
关于表操作的例子,可以参考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
- WCTSelect *select = [database prepareSelectObjectsOnResults:Message.localID.max()
- fromTable:@"message"];
- NSArray<Message *> *objects = [[[[select where:Message.localID > 0]
- groupBy:{Message.content}]
- orderBy:Message.createTime.order()]
- limit:10].allObjects;
where
、orderBy
、limit
等接口的返回值均为self
,因此可以通过链式调用,更自然更灵活的写出对应的查询。
开发者可以通过链式接口获取数据库操作的耗时、错误信息;也可以通过遍历逐个生成object。
- //Error message
- WCTError *error = select.error;
- //Performance
- int cost = select.cost;
- //Iteration
- Message *message;
- while ((message = [select nextObject])) {
- //...
- }
关于链式接口的例子,请参考Sample-chaincall。
核心层接口
WCDB封装了常用的增删查改操作,但不可能覆盖所有SQL的用法。因此核心层提供了执行为封装的SQL的能力。
- - (BOOL)exec:(const WCDB::Statement &)statement;
- - (WCTStatement *)prepare:(const WCDB::Statement &)statement;
结合WINQ,开发者可以用核心层接口执行其他未封装的复杂SQL。
- //run unwrapped SQL
- //PRAGMA case_sensitive_like=1
- [database exec:WCDB::StatementPragma().pragma(WCDB::Pragma::CaseSensitiveLike, true)];
- //get value from unwrapped SQL
- //PRAGMA case_sensitive
- WCTStatement *statement = [database prepare:WCDB::StatementPragma().pragma(WCDB::Pragma::CacheSize)];
- if (statement && statement.step) {
- NSLog(@"Cache size %@", [statement getValueAtIndex:0]);
- }
- //complex statement
- //EXPLAIN CREATE TABLE message(localID INTEGER PRIMARY KEY ASC, content TEXT);
- NSLog(@"Explain:");
- WCDB::ColumnDef localIDColumnDef(WCDB::Column("localID"), WCDB::ColumnType::Integer32);
- localIDColumnDef.makePrimary(WCDB::OrderTerm::ASC);
- WCDB::ColumnDef contentColumnDef(WCDB::Column("content"), WCDB::ColumnType::Text);
- WCDB::ColumnDefList columnDefList = {localIDColumnDef, contentColumnDef};
- WCDB::StatementCreateTable statementCreate = WCDB::StatementCreateTable().create("message", columnDefList);
- WCTStatement *statementExplain = [database prepare:WCDB::StatementExplain().explain(statementCreate)];
- if (statementExplain && [statementExplain step]) {
- for (int i = 0; i < [statementExplain getCount]; ++i) {
- NSString *columnName = [statementExplain getNameAtIndex:i];
- WCTValue *value = [statementExplain getValueAtIndex:i];
- NSLog(@"%@:%@", columnName, value);
- }
- }
调试SQL
[WCTStatistics SetGlobalSQLTrace:]
会监控所有执行的SQL,该接口可用于调试,确定SQL是否执行正确。
- //SQL Execution Monitor
- [WCTStatistics SetGlobalSQLTrace:^(NSString *sql) {
- NSLog(@"SQL: %@", sql);
- }];
关于核心层接口的例子,请参考Sample-core。
高级用法
主键自增(Auto Increment)
对于主键自增的类,需要在ORM定义WCDB_PRIMARY_AUTO_INCREMENT(className, propertyName)
,然后通过isAutoIncrement
接口设置自增属性,并通过lastInsertedRowID
接口获取插入的RowID
。
- WCTSampleConvenient *object = [[WCTSampleConvenient alloc] init];
- object.isAutoIncrement = YES;
- object.stringValue = @"Insert auto increment";
- [database insertObject:object
- into:tableName];
- long long lastInsertedRowID = object.lastInsertedRowID;
as重定向
基于ORM的支持,我们可以从数据库直接取出一个Object。然而,有时候需要取出并非是某个字段,而是有一些组合。例如:
- NSNumber *maxModifiedTime = [database getOneValueOnResult:Message.modifiedTime.max()
- fromTable:@"message"];
- Message *message = [[Message alloc] init];
- message.createTime = [NSDate dateWithTimeIntervalSince1970:maxModifiedTime.doubleValue];
这段代码从数据库中取出了消息的最新的修改时间,并以此将此时间作为消息的创建时间,新建了一个message。这种情况下,就可以使用as重定向。
as重定向,它可以将一个查询结果重定向到某一个字段,如下:
- Message *message = [database getOneObjectOnResults:Message.modifiedTime.max().as(Message.createTime)
- fromTable:@"message"];
通过as(Message.createTime)
的语法,将查询结果重新指向了createTime。因此只需一行代码便可完成原来的任务。
多表查询
SQLite支持联表查询,在某些特定的场景下,可以起到优化性能、简化表结构的作用。
WCDB同样提供了对应的接口,并在ORM的支持下,通过WCTMultiSelect的链式接口,可以同时从表中取出多个类的对象。
- /*
- SELECT contact.nickname, contact_ext.headImg
- FROM contact, contact_ext
- WHERE contact.name==contact_ext.name
- */
- WCTMultiSelect *multiSelect = [[database prepareSelectMultiObjectsOnResults:{
- Contact.nickname.inTable(@"contact"),
- ContactExt.nickname.inTable(@"contact_ext")
- } fromTables:@[ @"contact", @"contact_ext" ]] where:Contact.name.inTable(@"contact") == ContactExt.name.inTable(@"contact_ext")];
- while ((multiObject = [multiSelect nextMultiObject])) {
- Contact *contact = (Contact *) [multiObject objectForKey:@"contact"];
- ContactExt *contact = (ContactExt *) [multiObject objectForKey:@"contact_ext"];
- //...
- }