本教程主要介绍WCDB-iOS/macOS中的基础类、CRUD(增删改查)与transaction(事务)的用法。

阅读本教程前,建议先阅读iOS/macOS使用教程

基础类

WCDB提供了三个基础类进行数据库操作:WCTDatabaseWCTTableWCTTransaction。它们的接口都是线程安全的。

WCTDatabase

WCTDatabase表示一个数据库,可以进行所有数据库操作,包括增删查改、表操作、事务、文件操作、损坏修复等。

创建

WCTDatabase通过initWithPath:接口进行创建。该接口会同时创建path中不存在的目录。

  1. NSString* path = @"~/Intermediate/Directories/Will/Be/Created/sample.db";
  2. WCTDatabase *database = [[WCTDatabase alloc] initWithPath:path];
  3. database.tag = 1;

打开

WCDB大量使用延迟初始化(Lazy initialization)的方式管理对象,因此SQLite连接会在第一次被访问时被打开。开发者不需要手动打开数据库。

通过canOpen接口可测试数据库是否能够打开。通过isOpened接口可测试数据库是否已经打开。

  1. if ([database canOpen]) {
  2. //...
  3. }
  4. if ([database isOpened]) {
  5. //...
  6. }

关闭

WCDB通过close接口直接关闭数据库

  1. [database close];

由于WCDB支持多线程访问数据库,因此,该接口会阻塞等待所有数据库操作结束,以确保数据库完全关闭。

对于一个特定路径的数据库,WCDB会在所有对象对其的引用结束时,自动关闭数据库,并且回收内存和SQLite连接。因此,大部分情况下开发者不需要手动关闭数据库。

加密

WCDB提供基于sqlcipher的数据库加密功能,如下:

  1. WCTDatabase *database = [[WCTDatabase alloc] initWithPath:path];
  2. NSData *password = [@"MyPassword" dataUsingEncoding:NSASCIIStringEncoding];
  3. [database setCipherKey:password];

文件操作

WCDB提供了删除数据库、移动数据库、获取数据库占用空间和使用路径的文件操作接口。

  1. - (BOOL)removeFilesWithError:(WCTError **)error;
  2. - (BOOL)moveFilesToDirectory:(NSString *)directory withExtraFiles:(NSArray<NSString *> *)extraFiles andError:(WCTError **)error;
  3. - (NSArray<NSString *> *)getPaths;
  4. - (NSUInteger)getFilesSizeWithError:(WCTError **)error;

文件操作不是一个原子操作。若一个线程正在操作数据库,而另一个线程进行移动数据库文件,可能导致数据库损坏。因此,文件操作的最佳实践是确保数据库已关闭。

  1. [database close:^{
  2. WCTError *error = nil;
  3. BOOL ret = [database moveFilesToDirectory:otherDirectory withError:&error];
  4. if (!ret) {
  5. NSLog(@"Move files Error %@", error);
  6. }
  7. }];

关于文件操作的例子,可以参考Sample-file

事务

WCTDatabase不支持跨线程事务。事务内的操作必须在同一个线程运行完。可以通过两种方式运行事务:

  • beginTransaction commitTransactionrollbackTransaction
  1. //[beginTransaction], [commitTransaction], [rollbackTransaction] and all interfaces inside this transaction should run in same thread
  2. BOOL ret = [database beginTransaction];
  3. WCTSampleTransaction *object = [[WCTSampleTransaction alloc] init];
  4. ret = [database insertObject:object
  5. into:tableName];
  6. if (ret) {
  7. ret = [database commitTransaction];
  8. } else {
  9. ret = [database rollbackTransaction];
  10. }
  • runTransaction:
  1. BOOL commited = [database runTransaction:^BOOL {
  2. WCTSampleTransaction *object = [[WCTSampleTransaction alloc] init];
  3. BOOL ret = [database insertObject:object
  4. into:tableName];
  5. //return YES to do a commit and return NO to do a rollback
  6. if (ret) {
  7. return YES;
  8. }
  9. return NO;
  10. }
  11. event:^(WCTTransactionEvent event) {
  12. NSLog(@"Event %d", event);
  13. }];

跨线程事务可以参考WCTTransaction

关于事务的例子,可以参考Sample-transaction

WCTTable

WCTTable表示一个表。它等价于预设了classtableNameWCTDatabase,仅可以进行数据的增删查改等。

  1. WCTTable* table = [database getTableOfName:tableName
  2. withClass:WCTSampleTable.class];

WCTTransaction

WCTTransaction表示一个事务。

  1. WCTTransaction *transaction = [database getTransaction];

WCTDatabase的事务不同,WCTTransaction可以在函数和对象之间传递,实现跨线程的事务。

  1. //You can do a transaction in different threads using WCTTransaction.
  2. //But it's better to run serially, or an inner thread mutex will guarantee this.
  3. BOOL ret = [transaction begin];
  4. dispatch_async(dispatch_queue_create("other thread", DISPATCH_QUEUE_SERIAL), ^{
  5. WCTSampleTransaction *object = [[WCTSampleTransaction alloc] init];
  6. BOOL ret = [transaction insertObject:object
  7. into:tableName];
  8. if (ret) {
  9. [transaction commit];
  10. } else {
  11. [transaction rollback];
  12. }
  13. });

关于事务的例子,可以参考Sample-transaction

基础类共享

对于同一个路径的数据库,不同的WCTDatabaseWCTTableWCTTransaction对象共享同一个WCDB核心。因此,你可以在代码的不同位置、不同线程任意创建不同的基础类对象,WCDB会自动管理它们的共享数据和线程并发。

  1. WCTDatabase* database1 = [[WCTDatabase alloc] initWithPath:path];
  2. WCTDatabase* database2 = [[WCTDatabase alloc] initWithPath:path];
  3. database1.tag = 1;
  4. NSLog(@"%d", database2.tag);//print 1

关于WCDB的架构和实现,可以参考:TODO

CRUD

WCDB的增删改查分为表操作和数据操作两种。

表操作

表操作包括创建/删除 表/索引、判断表、索引是否存在等。WCTDatabaseWCTransaction都支持表操作的接口。

开发者可以根据ORM的定义创建表或索引:

  1. BOOL ret = [database createTableAndIndexesOfName:tableName
  2. withClass:WCTSampleTable.class];

也可以通过WINQ自定义表或索引:

  1. BOOL ret = [database createTableOfName:tableName
  2. withColumnDefList:{
  3. WCTSampleTable.intValue.def(WCTColumnTypeInteger32),
  4. WCTSampleTable.stringValue.def(WCTColumnTypeString)
  5. }];

关于表操作的例子,可以参考Sample-table

数据操作

数据操作分为便捷接口和链式接口两种。WCTDatabaseWCTTableWCTTransaction均支持数据操作接口。

便捷接口

便捷接口的设计原则为,通过一行代码即可完成数据的操作。

插入
  • insertObject:into:insertObjects:into:,插入单个或多个对象
  • insertOrReplaceObject:intoinsertOrReplaceObjects:into,插入单个或多个对象。当对象的主键在数据库内已经存在时,更新数据;否则插入对象。
  • insertObject:onProperties:into:insertObjects:onProperties:into:,插入单个或多个对象的部分属性
  • insertOrReplaceObject:onProperties:intoinsertOrReplaceObjects:onProperties:into,插入单个或多个对象的部分属性。当对象的主键在数据库内已经存在时,更新数据;否则插入对象。
删除
  • deleteAllObjectsFromTable:删除表内的所有数据
  • deleteObjectsFromTable:后可组合接 whereorderBylimitoffset以删除部分数据
更新
  • updateAllRowsInTable:onProperties:withObject:,通过object更新数据库中所有指定列的数据
  • updateRowsInTable:onProperties:withObject:后可组合接 whereorderBylimitoffset以通过object更新指定列的部分数据
  • updateAllRowsInTable:onProperty:withObject:,通过object更新数据库某一列的数据
  • updateRowsInTable:onProperty:withObject:后可组合接 whereorderBylimitoffset以通过object更新某一列的部分数据
  • updateAllRowsInTable:onProperties:withRow:,通过数组更新数据库中的所有指定列的数据
  • updateRowsInTable:onProperties:withRow:后可组合接 whereorderBylimitoffset以通过数组更新指定列的部分数据
  • updateAllRowsInTable:onProperty:withRow:,通过数组更新数据库某一列的数据
  • updateRowsInTable:onProperty:withRow:后可组合接 whereorderBylimitoffset以通过数组更新某一列的部分数
查找
  • getOneObjectOfClass:fromTable:后可接 whereorderBylimitoffset以从数据库中取出一行数据并组合成object
  • getOneObjectOnResults:fromTable:后可接 whereorderBylimitoffset以从数据库中取出一行数据的部分列并组合成object
  • getOneRowOnResults:fromTable:后可接 whereorderBylimitoffset以从数据库中取出一行数据的部分列并组合成数组
  • getOneColumnOnResult:fromTable:后可接 whereorderBylimitoffset以从数据库中取出一列数据并组合成数组
  • getOneDistinctColumnOnResult:fromTable:后可接 whereorderBylimitoffset以从数据库中取出一列数据,并取distinct后组合成数组。
  • getOneValueOnResult:fromTable:后可接 whereorderBylimitoffset以从数据库中取出一行数据的某一列
  • getAllObjectsOfClass:fromTable:,取出所有数据,并组合成object
  • getObjectsOfClass:fromTable:后可接 whereorderBylimitoffset以从数据库中取出一部分数据,并组合成object
  • getAllObjectsOnResults:fromTable:,取出所有数据的指定列,并组合成object
  • getObjectsOnResults:fromTable:后可接whereorderBylimitoffset以从数据库中取出一部分数据的指定列,并组合成object
  • getAllRowsOnResults:fromTable:,取出所有数据的指定列,并组合成数组
  • getRowsOnResults:fromTable:后可接whereorderBylimitoffset以从数据库中取出一部分数据的指定列,并组合成数组
    具体例子可直接参考Sample-convenience

链式接口

链式调用是指对象的接口返回一个对象,从而允许在单个语句中将调用链接在一起,而不需要变量来存储中间结果。

WCDB对于增删改查操作,都提供了对应的类以实现链式调用

  • WCTInsert
  • WCTDelete
  • WCTUpdate
  • WCTSelect
  • WCTRowSelect
  • WCTMultiSelect
  1. WCTSelect *select = [database prepareSelectObjectsOnResults:Message.localID.max()
  2. fromTable:@"message"];
  3. NSArray<Message *> *objects = [[[[select where:Message.localID > 0]
  4. groupBy:{Message.content}]
  5. orderBy:Message.createTime.order()]
  6. limit:10].allObjects;

whereorderBylimit等接口的返回值均为self,因此可以通过链式调用,更自然更灵活的写出对应的查询。

开发者可以通过链式接口获取数据库操作的耗时、错误信息;也可以通过遍历逐个生成object。

  1. //Error message
  2. WCTError *error = select.error;
  3. //Performance
  4. int cost = select.cost;
  5. //Iteration
  6. Message *message;
  7. while ((message = [select nextObject])) {
  8. //...
  9. }

关于链式接口的例子,请参考Sample-chaincall

核心层接口

WCDB封装了常用的增删查改操作,但不可能覆盖所有SQL的用法。因此核心层提供了执行为封装的SQL的能力。

  1. - (BOOL)exec:(const WCDB::Statement &)statement;
  2. - (WCTStatement *)prepare:(const WCDB::Statement &)statement;

结合WINQ,开发者可以用核心层接口执行其他未封装的复杂SQL。

  1. //run unwrapped SQL
  2. //PRAGMA case_sensitive_like=1
  3. [database exec:WCDB::StatementPragma().pragma(WCDB::Pragma::CaseSensitiveLike, true)];
  4.  
  5. //get value from unwrapped SQL
  6. //PRAGMA case_sensitive
  7. WCTStatement *statement = [database prepare:WCDB::StatementPragma().pragma(WCDB::Pragma::CacheSize)];
  8. if (statement && statement.step) {
  9. NSLog(@"Cache size %@", [statement getValueAtIndex:0]);
  10. }
  11.  
  12. //complex statement
  13. //EXPLAIN CREATE TABLE message(localID INTEGER PRIMARY KEY ASC, content TEXT);
  14. NSLog(@"Explain:");
  15. WCDB::ColumnDef localIDColumnDef(WCDB::Column("localID"), WCDB::ColumnType::Integer32);
  16. localIDColumnDef.makePrimary(WCDB::OrderTerm::ASC);
  17. WCDB::ColumnDef contentColumnDef(WCDB::Column("content"), WCDB::ColumnType::Text);
  18. WCDB::ColumnDefList columnDefList = {localIDColumnDef, contentColumnDef};
  19. WCDB::StatementCreateTable statementCreate = WCDB::StatementCreateTable().create("message", columnDefList);
  20. WCTStatement *statementExplain = [database prepare:WCDB::StatementExplain().explain(statementCreate)];
  21. if (statementExplain && [statementExplain step]) {
  22. for (int i = 0; i < [statementExplain getCount]; ++i) {
  23. NSString *columnName = [statementExplain getNameAtIndex:i];
  24. WCTValue *value = [statementExplain getValueAtIndex:i];
  25. NSLog(@"%@:%@", columnName, value);
  26. }
  27. }

调试SQL

[WCTStatistics SetGlobalSQLTrace:]会监控所有执行的SQL,该接口可用于调试,确定SQL是否执行正确。

  1. //SQL Execution Monitor
  2. [WCTStatistics SetGlobalSQLTrace:^(NSString *sql) {
  3. NSLog(@"SQL: %@", sql);
  4. }];

关于核心层接口的例子,请参考Sample-core

高级用法

主键自增(Auto Increment)

对于主键自增的类,需要在ORM定义WCDB_PRIMARY_AUTO_INCREMENT(className, propertyName),然后通过isAutoIncrement接口设置自增属性,并通过lastInsertedRowID接口获取插入的RowID

  1. WCTSampleConvenient *object = [[WCTSampleConvenient alloc] init];
  2. object.isAutoIncrement = YES;
  3. object.stringValue = @"Insert auto increment";
  4. [database insertObject:object
  5. into:tableName];
  6. long long lastInsertedRowID = object.lastInsertedRowID;

as重定向

基于ORM的支持,我们可以从数据库直接取出一个Object。然而,有时候需要取出并非是某个字段,而是有一些组合。例如:

  1. NSNumber *maxModifiedTime = [database getOneValueOnResult:Message.modifiedTime.max()
  2. fromTable:@"message"];
  3. Message *message = [[Message alloc] init];
  4. message.createTime = [NSDate dateWithTimeIntervalSince1970:maxModifiedTime.doubleValue];

这段代码从数据库中取出了消息的最新的修改时间,并以此将此时间作为消息的创建时间,新建了一个message。这种情况下,就可以使用as重定向。

as重定向,它可以将一个查询结果重定向到某一个字段,如下:

  1. Message *message = [database getOneObjectOnResults:Message.modifiedTime.max().as(Message.createTime)
  2. fromTable:@"message"];

通过as(Message.createTime)的语法,将查询结果重新指向了createTime。因此只需一行代码便可完成原来的任务。

多表查询

SQLite支持联表查询,在某些特定的场景下,可以起到优化性能、简化表结构的作用。

WCDB同样提供了对应的接口,并在ORM的支持下,通过WCTMultiSelect的链式接口,可以同时从表中取出多个类的对象。

  1. /*
  2. SELECT contact.nickname, contact_ext.headImg
  3. FROM contact, contact_ext
  4. WHERE contact.name==contact_ext.name
  5. */
  6. WCTMultiSelect *multiSelect = [[database prepareSelectMultiObjectsOnResults:{
  7. Contact.nickname.inTable(@"contact"),
  8. ContactExt.nickname.inTable(@"contact_ext")
  9. } fromTables:@[ @"contact", @"contact_ext" ]] where:Contact.name.inTable(@"contact") == ContactExt.name.inTable(@"contact_ext")];
  10.  
  11. while ((multiObject = [multiSelect nextMultiObject])) {
  12. Contact *contact = (Contact *) [multiObject objectForKey:@"contact"];
  13. ContactExt *contact = (ContactExt *) [multiObject objectForKey:@"contact_ext"];
  14. //...
  15. }