-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Objc 高级接口
本文将介绍 WCD 的一些高级接口,包括链式调用与联表查询、查询重定向 和 核心层接口。
在增删查改一章中,已经介绍了通过 WCTDatabase
、WCTTable
操作数据库的方式。它们是经过封装的便捷接口,其实质都是通过调用链式接口完成的。
WCTSelect* select = [[[database prepareSelect] ofClass:Sample.class] fromTable:@"sampleTable"];
NSArray<Sample*>* allObjects = [[[select where:Sample.identifier > 1] limit:10] allObjects];
WCTDelete* delete_ = [[[database prepareDelete] fromTable:@"sampleTable"] where:Sample.identifier != 0];
BOOL ret = [delete_ execute];
NSLog(@"%d", [delete_ getChanges]); // 获取该操作删除的行数
链式接口都以 prepareXXX
开始,根据不同的调用返回 WCTInsert
、WCTUpdate
、WCTDelete
、WCTSelect
或 WCTMultiSelect
对象。
这些对象的基本接口都返回其 self
,因此可以链式连续调用。
最后调用对应的函数使其操作生效,如 allObjects
、execute
等。
通过链式接口,可以更灵活的控制数据库操作。
在链式类中,WCTInsert
、WCTUpdate
、WCTDelete
和WCTSelect
都有其对应的增删查改接口。而 WCTMultiSelect
则不同。
WCTMultiSelect
用于联表查询,在某些场景下可提供性能,获取更多关联数据等。以下是联表查询的示例代码:
BOOL ret = [database createTable:@"sampleTable" withClass:Sample.class];
ret &= [database createTable:@"sampleTableMulti" withClass:SampleMulti.class];
// 相当于执行 SQL: SELECT sampleTable.identifier, sampleTable.content, sampleTableMulti.identifier, sampleTableMulti.content FROM sampleTable, sampleTableMulti WHERE sampleTable.identifier = sampleTableMulti.identifier
WCTMultiSelect* multiSelect = [[[[database prepareMultiSelect] onResultColumns:{
Sample.identifier.table(@"sampleTable"),
Sample.content.table(@"sampleTable"),
SampleMulti.identifier.table(@"sampleTableMulti"),
SampleMulti.content.table(@"sampleTableMulti")
}] fromTables:@[@"sampleTable", @"sampleTableMulti"] ]
where:Sample.identifier.table(@"sampleTable") == SampleMulti.identifier.table(@"sampleTableMulti")];
//读取两个表格的结果
WCTMultiObject* multiObject = multiSelect.firstMultiObject;
Sample* sample = (Sample*)[multiObject objectForKey:@"sampleTable"];
SampleMulti* multiSample = (SampleMulti*)[multiObject objectForKey:@"sampleTableMulti"];
该查询将 "sampleTable" 和 "sampleTableMulti" 表联合起来,取出它们中具有相等 identifier
值的数据。
多表查询时,所有同名字段都需要通过 table()
接口指定表名,否则会因为无法确定其属于哪个表从而出错。
查询到的 multiObject
是表名到对象的映射,取出后进行类型转换即可。
查询的数据有时候不是字段本身,但仍需将其取出为对象时,可以使用查询重定向。 它可以将查询接口重定向到一个指定字段,从而简化操作。
Sample *obj = [database getObjectOnResultColumns:Sample.identifier.redirect(Sample.identifier.max())
fromTable:@"sampleTable" ];
NSLog(@"max identifier: %d", obj.identifier);
示例代码查询了 identifier
的最大值,然后重新定向其到 Sample
的 identifier
字段,因此可以直接以对象的形式取出该值。
在之前的所有教程中,WCDB 通过其封装的各种接口,简化了最常用的增删查改操作。但 SQL 的操作千变万化,仍会有部分功能无法通过便捷接口满足。此时则可以直接操作WCTHandle
和 WCTPreparedStatement
,来做更精细的控制。
WCTHandle
是单个数据库连接(具体见:Database Connection Handle)的包装,WCTDatabase
则是相当于一个数据库连接的池子。WCTHandle
可以通过WCDB_GET_SCOPED_HANDLE
宏来获取,或者使用-[WCTDatabase getHandle]
方法来获取,后者在WCTHandle
用完之后需要调用-[WCTHandle invalidate]
方法来回收资源。WCTHandle
具备WCTDatabase
的全部建表和CRUD接口,还可以精细控制SQL的执行过程。下面是一个示例:
// 获取handle
WCDB_GET_SCOPED_HANDLE(database, handle);
BOOL ret = [handle createTable:@"sampleTable" withClass:Sample.class];
// 先 prepare statement, 其实是sqlite3_prepare函数的封装。
ret &= [handle prepare:WCDB::StatementInsert()
.insertIntoTable(@"sampleTable")
.columns(Sample.allProperties)
.values(WCDB::BindParameter::bindParameters(Sample.allProperties.size()))];
for(int i = 0; i < 100000; i++) {
// 先 reset,其实是sqlite3_reset函数的封装。
[handle reset];
//1. 可以直接使用对象来bind,会逐个属性调用sqlite3_bind系列接口
Sample* obj = [[Sample alloc] init];
obj.identifier = i;
[handle bindProperties:Sample.allProperties ofObject:obj];
//2. 也可以逐个字段bind,更高效一点
[handle bindInteger:i toIndex:1];
[handle bindNullToIndex:2];
// step 写入数据
ret &= [handle step];
}
//一个statement用完之后需要调用finalize,底下会调用sqlite3_finalize函数
[handle finalizeStatement];
NSMutableArray* objects = [[NSMutableArray alloc] init];
ret = [handle prepare:WCDB::StatementSelect()
.select(Sample.allProperties)
.from(@"sampleTable")];
while ([handle step] && ![handle done]) {
//1. 可以直接读取对象,会逐个属性来调用sqlite3_column系列接口来读取数据,并赋值给对象
Sample* obj = [handle extractObjectOnResultColumns:Sample.allProperties];
//2. 也可以逐个字段读取,更高效一点
obj = [[Sample alloc] init];
obj.identifier = [handle extractIntegerAtIndex:0];
obj.content = [handle extractStringAtIndex:1];
[objects addObject:obj];
}
//一个statement用完之后需要调用finalize,底下会调用sqlite3_finalize函数
[handle finalizeStatement];
一个值得注意的点是,
handle
是用到的时候才获取,不能长时间持有它。
使用WCTHandle
虽然可以精细控制 SQL 的执行过程,但是一次只能执行一个 SQL,如果需要同时执行多个,就没办法了。这时候就需要用到WCTPreparedStatement
。WCTPreparedStatement
是sqlite3_stmt
(具体见Prepared Statement Object)的封装,它保存了 SQL 的语法解析结果,用它来重复执行SQL语句的话,就可以节省SQL语句的解析耗时,提高性能,下面是示例代码:
// 获取handle
WCDB_GET_SCOPED_HANDLE(database, handle);
BOOL ret = [handle createTable:@"sampleTable" withClass:Sample.class];
ret &= [handle createTable:@"newSampleTable" withClass:Sample.class];
// prepare statement, 成功则返回 WCTPreparedStatement,失败则返回nil
WCTPreparedStatement* selectSTMT = [handle getOrCreatePreparedStatement:WCDB::StatementSelect()
.select(Sample.allProperties)
.from(@"sampleTable")];
WCTPreparedStatement* insertSTMT = [handle getOrCreatePreparedStatement:WCDB::StatementInsert()
.insertIntoTable(@"newSampleTable")
.columns(Sample.allProperties)
.values(WCDB::BindParameter::bindParameters(Sample.allProperties.size()))];
while ([selectSTMT step] && ![selectSTMT done]) {
//1. 可以直接读取对象,会逐个属性来调用sqlite3_column系列接口来读取数据,并赋值给对象
Sample* obj = (Sample*)[selectSTMT extractObjectOnResultColumns:Sample.allProperties];
//2. 也可以逐个字段读取,更高效一点
obj = [[Sample alloc] init];
obj.identifier = [selectSTMT extractIntegerAtIndex:0];
obj.content = [selectSTMT extractStringAtIndex:1];
// 先 reset,其实是sqlite3_reset函数的封装。
[insertSTMT reset];
obj.identifier += 10000;
//1. 可以直接使用对象来bind,会逐个属性调用sqlite3_bind系列接口
[insertSTMT bindProperties:Sample.allProperties ofObject:obj];
//2. 也可以逐个字段bind,更高效一点
[insertSTMT bindInteger:obj.identifier toIndex:1];
[insertSTMT bindString:obj.content toIndex:2];
// step 写入数据
ret &= [insertSTMT step];
}
//statement用完之后可以一次性finalize,底下会调用sqlite3_finalize函数逐个释放preparedStatement的资源
//不调用的话,handle dealloc之后也会自动finalize它所创建的所有preparedStatement
[handle finalizeAllStatements];
在需要对数据库进行大量数据更新的场景,我们的开发习惯一般是将这些更新操作统一到子线程处理,这样可以避免阻塞主线程,影响用户体验。
对于这类场景,如果只是将数据更新操作放到子线程执行,是不能完整解决问题的。因为 SQLite 的同个DB不支持并行写入,如果子线程的数据更新操作耗时太久,而主线程又有数据写入操作,比如用户在收消息的同时还会发消息,这样也会造成主线程阻塞。一种可行的做法是,将子线程的数据更新操作拆成一个个耗时很小的独立操作分别执行。但这样又会导致磁盘 IO 量大和增加子线程耗时的问题。
因为SQLite读写数据库时以一个数据页为单位的,一个数据页的大小在 WCDB 中是4kb,单个数据页一般可以存多条数据,逐条数据更新容易导致同一个数据页被读写多次。为了减少磁盘写入量,只能将所有的数据更新操作放到一个事务中执行,这样又会造成主线程阻塞的问题。
为了解决大事务会阻塞主线程的问题,我们在 WCDB 中开发了一种可中断事务。可中断事务把一个流程很长的事务过程看成一个循环逻辑,每次循环执行一次短时间的DB操作。操作之后根据外部传入的参数判断当前事务是否可以结束,如果可以结束的话,就直接Commit Transaction
,将事务修改内容写入磁盘。如果事务还不可以结束,再判断主线程是否因为当前事务阻塞,没有的话就回调外部逻辑,继续执行后面的循环,直到外部逻辑处理完毕。如果检测到主线程因为当前事务阻塞,则会立即 Commit Transaction
,先将部分修改内容写入磁盘,并唤醒主线程执行DB操作。等到主线程的DB操作执行完成之后,在重新开一个新事务,让外部可以继续执行之前中断的逻辑。可中断事务的整体逻辑如下图所示:
下面是可中断事务的使用示例:
NSMutableArray* objects = [[NSMutableArray alloc] init];
for(int i = 0; i < 100000; i++) {
Sample* obj = [[Sample alloc] init];
obj.identifier = i;
[objects addObject:obj];
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
__block int index = 0;
[database runPauseableTransactionWithOneLoop:^BOOL(WCTHandle *handle, BOOL *stop, BOOL isNewTransaction) {
BOOL ret = NO;
// isNewTransaction表示第一次执行,或者事务在上次循环结束之后被中断提交了
if(isNewTransaction) {
//新事务先建一下表,避免事务被中断之后,表已经被其他逻辑删除
ret = [handle createTable:@"sampleTable" withClass:Sample.class];
}
//写入一个对象,这里还可以用WCTPreparedStatement来减少SQL解析的耗时
ret &= [handle insertObject:objects[index] intoTable:@"sampleTable"];
index++;
//给stop赋值成YES表示事务结束,根据返回值ret提交或者回滚事务。
*stop = index >= objects.count;
return ret;
}];
});
- 欢迎使用 WCDB
- 基础教程
- 进阶教程
- 欢迎使用 WCDB
- 基础教程
- 进阶教程
- 欢迎使用 WCDB
- 基础教程
- 进阶教程
- 欢迎使用 WCDB
- 基础教程
- 进阶教程