IOS数据存储 之WCDB (一)

1. WCDB 简介

  • WCDB是腾讯开发的,微信中使用的DB开源框架:

引用官方说法:“WCDB是一个易用、高效、完整的移动数据库框架,它基于 SQLite 和 SQLCipher 开发。”

1.1 使用WCDB框架3大优势

  • 易用性
  1. one line of code 是它坚持的原则,大多数操作只需要一行代码即可完成.
  2. 使用WINQ 语句查询,不用为拼接SQL语句而烦恼了,模型绑定映射也是按照规定模板去实现方便快捷。
  • 高效性
  1. 和fmdb做对比

WCDB.swift和fmdb做对比WCDB.swift和fmdb做对比

  • 完整性
  1. 支持基于SQLCipher 加密
  2. 持全文搜索
  3. 支持反注入,可以避免第三方从输入框注入 SQL,进行预期之外的恶意操作。
  4. 用户不用手动管理数据库字段版本,升级方便自动.
  5. 提供数据库修复工具。

1.2 WCDB 的一些基础概念

1.2.1 类字段绑定(ORM)

  • ORM定义:

在WCDB内,ORM(Object Relational Mapping)是指

  1. 将一个ObjC的类,映射到数据库的表和索引;
  2. 将类的property,映射到数据库表的字段;
  3. 这一过程。通过ORM,可以达到直接通过Object进行数据库操作,省去拼装过程的目的。
  • WCDB通过内建的宏实现ORM的功能。如下:
//Message.h
@interface Message : NSObject

@property int localID;
@property(retain) NSString *content;
@property(retain) NSDate *createTime;
@property(retain) NSDate *modifiedTime;
@property(assign) int unused; //You can only define the properties you need

@end
//Message.mm
#import "Message.h"
@implementation Message

WCDB_IMPLEMENTATION(Message)
WCDB_SYNTHESIZE(Message, localID)
WCDB_SYNTHESIZE(Message, content)
WCDB_SYNTHESIZE(Message, createTime)
WCDB_SYNTHESIZE(Message, modifiedTime)

WCDB_PRIMARY(Message, localID)

WCDB_INDEX(Message, "_index", createTime)

@end
//Message+WCTTableCoding.h
#import "Message.h"
#import <WCDB/WCDB.h>

@interface Message (WCTTableCoding) <WCTTableCoding>

WCDB_PROPERTY(localID)
WCDB_PROPERTY(content)
WCDB_PROPERTY(createTime)
WCDB_PROPERTY(modifiedTime)

@end
  • 将一个已有的ObjC类进行ORM绑定的过程如下:
  1. 定义该类遵循WCTTableCoding协议。可以在类声明上定义,也可以通过文件模版在category内定义。
  2. 使用WCDB_PROPERTY宏在头文件声明需要绑定到数据库表的字段。
  3. 使用WCDB_IMPLEMENTATIO宏在类文件定义绑定到数据库表的类。
  4. 使用WCDB_SYNTHESIZE宏在类文件定义需要绑定到数据库表的字段。
  • 简单几行代码,就完成了将类和需要的字段绑定到数据库表的过程。这三个宏在名称和使用习惯上,也都和定义一个ObjC类相似,以此便于记忆。
  • 除此之外,WCDB还提供了许多可选的宏,用于定义数据库索引、约束等,如:

WCDB_PRIMARY用于定义主键
WCDB_INDEX用于定义索引
WCDB_UNIQUE用于定义唯一约束
WCDB_NOT_NULL用于定义非空约束

  • 定义完成后,只需要调用createTableAndIndexesOfName:withClass:接口,即可创建表和索引。
WCTDatabase *database = [[WCTDatabase alloc] initWithPath:path];
/*
 CREATE TABLE messsage (localID INTEGER PRIMARY KEY,
 						content TEXT,
 						createTime BLOB,
	 					modifiedTime BLOB)
 */
BOOL result = [database createTableAndIndexesOfName:@"message"
                                          withClass:Message.class];
  • 接口会根据ORM的定义,创建对应表和索引。

1.2.2 WINQ(WCDB语言集成查询)

  • WINQ简介
  1. WINQ(WCDB Integrated Query,音’wink’),是将自然查询的SQL集成到WCDB框架中的技术,基于C++实现。
  2. 传统的SQL语句,通常是开发者拼接字符串完成。这种方式不仅繁琐、易错,而且出错后很难定位到问题所在。同时也容易给SQL注入留下可乘之机。
  3. 而WINQ将查询语言集成到了C++中,可以通过类似函数调用的方式来写SQL查询。借用IDE的代码提示和编译器的语法检查,达到易用、纠错的效果。
  4. WINQ的使用上接近于C函数调用。对于熟悉SQL的开发者,无须特别学习即可立刻上手使用.
1.2.2.1 字段映射与运算符
  • 对于一个已绑定ORM的类,可以通过className.propertyName的方式,获得数据库内字段的映射,以此书写SQL的条件、排序、过滤等等所有语句。如下是几个例子:
/*
 SELECT MAX(createTime), MIN(createTime)
 FROM message
 WHERE localID>0 AND content IS NOT NULL
 */
[database getObjectsOnResults:{Message.createTime.max(), Message.createTime.min()}
                    fromTable:@"message"
                        where:Message.localID > 0 && Message.content.isNotNull()];
/*
 SELECT DISTINCT localID
 FROM message
 ORDER BY modifiedTime ASC
 LIMIT 10
 */
[database getObjectsOnResults:Message.localID.distinct()
                    fromTable:@"message"
                      orderBy:Message.modifiedTime.order(WCTOrderedAscending)
                        limit:10];
/*
 DELETE FROM message
 WHERE localID BETWEEN 10 AND 20 OR content LIKE 'Hello%'
 */
[database deleteObjectsFromtable:@"message"
                           where:Message.local.between(10, 20) 
 								 || Message.content.like("Hello%")];
  • 由于WINQ通过接口调用实现SQL查询,因此在书写过程中会有IDE的代码提示和编译器的语法检查,从而提升开发效率,避免写错。
  • WINQ的接口包括但不限于:

一元操作符:+、-、!等
二元操作符:||、&&、+、-、*、/、|、&、<<、>>、<、<=、==、!=、>、>=等
范围比较:IN、BETWEEN等
字符串匹配:LIKE、GLOB、MATCH、REGEXP等
聚合函数:AVG、COUNT、MAX、MIN、SUM等

  • 凡是SQLite支持的语法规则,WINQ基本都有其对应的接口。且接口名称与SQLite的语法规则基本保持一致。
1.2.2.2 字段组合
  • 多个字段映射可通过大括号{}进行组合,如:
/*
 SELECT localID, content
 FROM message
 */
[database getAllObjectsOnResults:{Message.localID, Message.content}
 					   fromTable:@"message"];
/*
 SELECT *
 FROM message
 ORDER BY createTime ASC, localID DESC
 */
[database getObjectsOfClass:Message.class 
				  fromTable:@"message" 
				    orderBy:{Message.createTime.order(WCTOrderedAscending),  Message.localID.order(WCTOrderedDescending)}];
1.2.2.3 AllProperties
  • 在上述的组合中,大括号{}的语法实质是C++中列表std::list的隐式初始化。而className.AllProperties则用于获取类定义的所有字段映射的列表,如:
/*
 SELECT localID, content, createTime, modifiedTime
 FROM message
*/
[database getAllObjectsOnResults:Message.AllProperties
 					   fromTable:@"message"];
1.2.2.4 AnyProperty
  • className.AnyProperty用于指代SQL中的*,如:
/*
 SELECT count(*)
 FROM message
 */
 [database getOneValueOnResult:Message.AnyProperty.count()
	  			  	 fromTable:@"message"];

1.2.3 加密

  • WCDB提供基于sqlcipher的数据库加密功能,如下:
WCTDatabase *database = [[WCTDatabase alloc] initWithPath:path];
NSData *password = [@"MyPassword" dataUsingEncoding:NSASCIIStringEncoding];
[database setCipherKey:password];

1.2.4 全局监控

  • WCDB提供了对错误和性能的全局监控,可用于调试错误和性能。

  • WCDB可以对所有错误进行统一的监控,也可以获取某个特定操作的错误信息。所有错误都以WCTError的形式出现。

1.2.4.1 WCTError
  • WCTError继承自NSError,包含了WCDB错误的所有信息,以供调试或发现问题。
    type表示错误的类型,不同类型的错误其错误码和拥有的信息不同。其对应关系如下
type描述相关参考代码
SQLite表示该错误来自SQLite接口请参考rescode
SystemCall表示该错误来自系统调用请参考errno
Core表示该错误来自WCDB Core层请参考源码的error.hpp
Interface表示该错误来自WCDB Interface层请参考源码的error.hpp
Abort表示中断,该错误一般是开发错误,应该在发布前修复
Warning表示警告,建议修复
SQLiteGlobal表示该信息来自SQLite的log接口,一般只作为debug log请参考rescode
  • 其他错误信息通过infoForKey接口获得,包括:
type描述相关参考代码
Tag正在操作的数据库的tag
Operation正在进行的操作请参考源码的error.hpp
Extended CodeSQLite的扩展码请参考rescode
Message错误信息
SQL发生错误时正在执行的SQL
Path发生错误时正在操作的文件的路径
1.2.4.2 获取错误
  • 由于便捷接口的设计原则是易用,因此不提供获取错误的方式。错误处理需使用链式接口
WCTSelect *select = [database prepareSelectObjectsOfClass:Message.class
                                                fromTable:@"message"];
NSArray<Message *> *objects = [[[select where:Message.localID > 0] 
                                	  orderBy:Message.createTime.order()] 
                               			limit:10].allObjects;
WCTError *error = select.error;
  • 开发者也可以注册全局的错误接口,以调试、上报、打log, 代码如下:
//Error Monitor
[WCTStatistics SetGlobalErrorReport:^(WCTError *error) {
	NSLog(@"[WCDB]%@", error);
}];
//Performance Monitor
[WCTStatistics SetGlobalPerformanceTrace:^(WCTTag tag, NSDictionary<NSString *, NSNumber *> *sqls, NSInteger cost) {
	NSLog(@"Database with tag:%d", tag);
	NSLog(@"Run :");
	[sqls enumerateKeysAndObjectsUsingBlock:^(NSString *sqls, NSNumber *count, BOOL *) {
		NSLog(@"SQL %@ %@ times", sqls, count);
	}];
	NSLog(@"Total cost %lld nanoseconds", cost);
}];
//SQL Execution Monitor
[WCTStatistics SetGlobalSQLTrace:^(NSString *sql) {
	NSLog(@"SQL: %@", sql);
}];

1.2.5 损坏修复

  • WCDB内建了修复工具,以应对数据库损坏,无法使用的情况。
  • 开发者需要在数据库未损坏时,对数据库元信息定时进行备份,如下:
NSData *backupPassword = [@"MyBackupPassword" dataUsingEncoding:NSASCIIStringEncoding];
[database backupWithCipher:backupPassword];
  • 当检测到数据库损坏,即WCTErrortypeWCTErrorTypeSQLitecode为11或26(SQLITE_CORRUPTSQLITE_NOTADB)时,可以进行修复。
//Since recovering is a long time operation, you'd better call it in sub-thread.
[view startLoading];
dispatch_async(DISPATCH_QUEUE_PRIORITY_BACKGROUND, ^{
	WCTDatabase *recover = [[WCTDatabase alloc] initWithPath:recoverPath];
	NSData *password = [@"MyPassword" dataUsingEncoding:NSASCIIStringEncoding];
	NSData *backupPassword = [@"MyBackupPassword" dataUsingEncoding:NSASCIIStringEncoding];
  	int pageSize = 4096;//Default to 4096 on iOS and 1024 on macOS.
	[database close:^{
		[recover recoverFromPath:path 
         			withPageSize:pageSize 
         			backupCipher:cipher 
         		  databaseCipher:password];
	}];
	[view stopLoading];
});

1.2.6 性能监控

  • WCDB支持获取单次操作的耗时,也支持对单个DB或全局注册统一接口监控性能。
  • 所有性能监控都会有少量的性能损坏,请根据需求开启.
1.2.6.1 操作耗时
  • 由于便捷接口的设计原则是易用,因此不提供获取错误的方式。操作耗时需使用链式接口。

  • 首先安通过setStatisticsEnabled:打开耗时监控

WCTSelect *select = [database prepareSelectObjectsOfClass:Message.class
                                                fromTable:@"message"];
[select setStatisticsEnabled:YES];//You should call this before all other operations
  • 在操作执行完成后,通过cost接口获取耗时
NSArray<Message *> *objects = [[[select where:Message.localID > 0] 
                                	  orderBy:Message.createTime.order()] 
                               			limit:10].allObjects;
NSLog(@"%f", select.cost);//You should call this after all other operations
1.2.6.2 监控耗时
  • WCDB支持对所有SQL操作进行全局监控,也支持监控单个特定的数据库.

所有监控的返回数据都相同,包括三个数据:

  1. Tag,执行操作的数据库的tag
  2. sqls,执行的SQL和对应的次数。
    对于非事务操作,则为单条SQL
    对于事务操作,则为该次事务所执行的所有SQL和每个sql执行的次数
  3. cost,耗时
  • 全局监控:

    监控所有db的数据库操作耗时,该接口需要在所有db打开、操作之前调用。

[WCTStatistics SetGlobalTrace:^(WCTTag tag, NSDictionary<NSString *, NSNumber *> *sqls, NSInteger cost) {
    NSLog(@"Tag: %d", tag);
    [sqls enumerateKeysAndObjectsUsingBlock:^(NSString *sql, NSNumber *count, BOOL *) {
      NSLog(@"SQL: %@ Count: %d", sql, count.intValue);
    }];
    NSLog(@"Total cost %ld nanoseconds", (long) cost);
}];
  • 特定数据库监控

对于特定的数据库,该接口会覆盖全局监控的注册。

[db setTrace:^(WCTTag tag, NSDictionary<NSString *, NSNumber *> *sqls, NSInteger cost) {
    NSLog(@"Tag: %d", tag);
    [sqls enumerateKeysAndObjectsUsingBlock:^(NSString *sql, NSNumber *count, BOOL *) {
      NSLog(@"SQL: %@ Count: %d", sql, count.intValue);
    }];
    NSLog(@"Total cost %ld nanoseconds", (long) cost);
}];
  • 操作耗时监控耗时的区别
  1. 操作耗时cost返回的耗时为浮点数的秒,监控耗时的cost返回的耗时为整型的纳秒。
  2. 监控耗时仅包括SQL在SQLite层面的耗时,包括SQL的编译、I/O等。而操作耗时除以上之外,还包括了WCDB层面对类封装等产生的耗时
1.2.6.3 SQL执行监控
  • WCDB可以监控所有SQL的执行,以确定代码符合预期
//SQL Execution Monitor
[WCTStatistics SetGlobalSQLTrace:^(NSString *sql) {
	NSLog(@"SQL: %@", sql);
}];

2. WCDB OC版本

  • 得益于ORM的定义,WCDB可以直接进行通过object进行增删改查(CRUD)操作。开发者可以通过WCTDatabaseWCTTable两个类进行一般的增删改查操作。

2.1 WCDB OC版本 增删改查(CRUD)

2.1.1 增

//插入
Message *message = [[Message alloc] init];
message.localID = 1;
message.content = @"Hello, WCDB!";
message.createTime = [NSDate date];
message.modifiedTime = [NSDate date];
/*
 INSERT INTO message(localID, content, createTime, modifiedTime) 
 VALUES(1, "Hello, WCDB!", 1496396165, 1496396165);
 */
BOOL result = [database insertObject:message
                                into:@"message"];

2.1.2 删

//删除
//DELETE FROM message WHERE localID>0;
BOOL result = [database deleteObjectsFromTable:@"message"
                                         where:Message.localID > 0];

2.1.3 改

//修改
//UPDATE message SET content="Hello, Wechat!";
Message *message = [[Message alloc] init];
message.content = @"Hello, Wechat!";
BOOL result = [database updateRowsInTable:@"message"
		                     onProperties:Message.content
        		               withObject:message];

2.1.4 查

//查询
//SELECT * FROM message ORDER BY localID
NSArray<Message *> *message = [database getObjectsOfClass:Message.class
                           fromTable:@"message"                                                         orderBy:Message.localID.order()];
  • WCTTable相当于预设了表名和类名的WCTDatabase对象,接口和WCTDatabase基本一致。
WCTTable *table = [database getTableOfName:@"message"
                                 withClass:Message.class];
//查询
//SELECT * FROM message ORDER BY localID
NSArray<Message *> *message = [table getObjectsOrderBy:Message.localID.order()];

2.2 事务 (Transaction)

  • WCDB内可通过两种方式执行事务,一是runTransaction:接口,如下:
BOOL commited = [database runTransaction:^BOOL {
	[database insertObject:message into:@"message"];
	return YES; //return YES to commit transaction and return NO to rollback transaction.
}];
  • 这种方式要求数据库操作在一个BLOCK内完成,简单易用。
  • 另一种方式则是获取WCTTransaction对象,如下:
WCTTransaction *transaction = [database getTransaction];
BOOL result = [transaction begin];
[transaction insertObject:message into:@"message"];
result = [transaction commit];
if (!result) {
    [transaction rollback];
    NSLog(@"%@", [transaction error]);
}
  • WCTTransaction对象可以在类或函数间传递,因此这种方式也更具灵活性。

3. WCDB Swift版本

3.1 WCDB.swift安装

  • 安装要求:

Swift 4.0 及以上
Xcode 9.0 及以上

 pod 'WCDB.swift'

3.2 WCDB.swift使用

  • 模型绑定,直接用wcdb提供的模板
class Sample: TableCodable {
    var identifier: Int? = nil
    var description: String? = nil
    
    enum CodingKeys: String, CodingTableKey {
        typealias Root = Sample
        static let objectRelationalMapping = TableBinding(CodingKeys.self)
        case identifier
        case description
    }
}
  • 数据库创建以及操作单独写了个单例类 HMDataBaseManager.swift
import Foundation
import WCDBSwift

struct HMDataBasePath {
    
    let dbPath = NSSearchPathForDirectoriesInDomains(.documentDirectory,
                                                     .userDomainMask,
                                                     true).last! + "/HMDB/HMDB.db"
}

class HMDataBaseManager: NSObject {
    
    static let share = HMDataBaseManager()
    
    let dataBasePath = URL(fileURLWithPath: HMDataBasePath().dbPath)
    var dataBase: Database?
    private override init() {
        super.init()
        dataBase = createDb()
    }
    ///创建db
   private func createDb() -> Database {
        debugPrint("数据库路径==\(dataBasePath.absoluteString)")
        return Database(withFileURL: dataBasePath)
    }
    ///创建表
    func createTable<T: TableDecodable>(table: String, of ttype:T.Type) -> Void {
        do {
            try dataBase?.create(table: table, of:ttype)
        } catch let error {
            debugPrint("create table error \(error.localizedDescription)")
        }
    }
    ///插入
    func insertToDb<T: TableEncodable>(objects: [T] ,intoTable table: String) -> Void {
        do {
            try dataBase?.insert(objects: objects, intoTable: table)
        } catch let error {
            debugPrint(" insert obj error \(error.localizedDescription)")
        }
    }
    
    ///修改
    func updateToDb<T: TableEncodable>(table: String, on propertys:[PropertyConvertible],with object:T,where condition: Condition? = nil) -> Void{
        do {
            try dataBase?.update(table: table, on: propertys, with: object,where: condition)
        } catch let error {
            debugPrint(" update obj error \(error.localizedDescription)")
        }
    }
    
    ///删除
    func deleteFromDb(fromTable: String, where condition: Condition? = nil) -> Void {
        do {
            try dataBase?.delete(fromTable: fromTable, where:condition)
        } catch let error {
            debugPrint("delete error \(error.localizedDescription)")
        }
    }
    
    ///查询
    func qureyFromDb<T: TableDecodable>(fromTable: String, cls cName: T.Type, where condition: Condition? = nil, orderBy orderList:[OrderBy]? = nil) -> [T]? {
        do {
            let allObjects: [T] = try (dataBase?.getObjects(fromTable: fromTable, where:condition, orderBy:orderList))!
            debugPrint("\(allObjects)");
            return allObjects
        } catch let error {
            debugPrint("no data find \(error.localizedDescription)")
        }
        return nil
    }
    
    ///删除数据表
    func dropTable(table: String) -> Void {
        do {
            try dataBase?.drop(table: table)
        } catch let error {
            debugPrint("drop table error \(error)")
        }
    }
    
    /// 删除所有与该数据库相关的文件
    func removeDbFile() -> Void {
        do {
            try dataBase?.close(onClosed: {
                try dataBase?.removeFiles()
            })
        } catch let error {
            debugPrint("not close db \(error)")
        }
    }
}
  • 比较复杂的查询可以使用 prepareSelect 查询接口
//查询所有站点并按字母排序去重
    func qureyAllStations(cityId: Int) -> [StationModel]{
        var stationArray = [StationModel]()
        
        do {
            let selectPrep = try HMDataBaseManager.share.dataBase?.prepareSelect(on: StationModel.Properties.all, fromTable: String(describing: StationModel.self)).where(StationModel.Properties.cityid == cityId).group(by: StationModel.Properties.statid).order(by: StationModel.Properties.statpname.asOrder(by: .ascending))
            stationArray = try selectPrep?.allObjects() ?? []
        } catch let error {
            debugPrint("\(error)")
        }
        
        return stationArray
    }

4. 数据库从FMDB迁移到WCDB

4.1 为什么要迁移到WCDB?

  1. WCDB依托于微信上亿用户的实际场景,解决了许多在开发和线上遇到的共性问题,在性能、易用性、功能完整性以及兼容性上都有较好的表现。并且,开发者可以平滑地从FMDB升级到WCDB。
  2. WCDB有三大优势:更高效,更易用,功能更完整,具体优势比较上面已经对比过了。

4.1.1 WCDB 对比FMDB的优势一:高效

  • WCDB在并发、ORM以及SQLite源码都做了许多针对性的优化,使得在写入、多线程并发、初始化等方面比FMDB有30%-280%的性能提升。
    WCDB 对比FMDB的优势一:高效

4.1.2 WCDB 对比FMDB的优势一:易用

  • WCDB通过WINQ和ORM,使得从拼接SQL、获取数据、拼装Object的整个过程,只需要一行代码即可完成。

  • FMDB代码,实现一个查询需要一堆胶水代码:

/*
 FMDB Code
 */
FMResultSet *resultSet = [fmdb executeQuery:@"SELECT * FROM message"];
NSMutableArray<Message *> *messages = [[NSMutableArray alloc] init];
while ([resultSet next]) {
    Message *message = [[Message alloc] init];
    message.localID = [resultSet intForColumnIndex:0];
    message.content = [resultSet stringForColumnIndex:1];
    message.createTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:2]];
    message.modifiedTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:3]];
    [messages addObject:message];
}
  • WCDB只需要一行代码:
/*
 WCDB Code
 */
NSArray<Message *> *messages = [wcdb getAllObjectsOfClass:Message.class fromTable:@"message"];

4.1.3 WCDB 对比FMDB的优势一:完整

  • FMDB只是简单对Sqlite3的封装,没有提供错误统计,性能统计,损坏修复,反注入,加密等功能。
  • 相反WCDB提供了一套完整的功能。

错误统计
性能统计
损坏修复
反注入
加密

4.2 FMDB迁移

4.2.1 安装

  • 首先在工程的配置Build Phases->Link Binary With Libraries中,将FMDB以及SQLite的库移出工程。
  • 然后参考安装教程选择适合方式链入WCDB的库。

4.2.2 创建数据库

  • WCTDatabase通过指定路径进行创建。同时,该接口会自动创建路径中未创建的目录。
NSString* path = @"intermediate/directory/will/be/created/automatically/wcdb";
WCTDatabase* wcdb = [[WCTDatabase alloc] initWithPath:path];
  • 临时数据库可以创建在iOS/macOS的临时目录上。
NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:@"tmp.db"];
WCTDatabase* wcdb = [[WCTDatabase alloc] initWithPath:path];
  • WCDB暂不支持创建内存数据库。由于移动平台的磁盘介质大多为SSD,其性能与纯内存操作差别不大。同时内存数据库会占用大量内存,从而导致FOOM。

4.2.3 打开数据库

  • WCDB会在第一次访问数据库时,自动打开数据库,不需要开发者主动操作。
  • canOpen接口可用于测试数据库能否正常打开,isOpened接口可用于测试数据库是否已打开。
if (![wcdb canOpen]) {
  NSLog(@"open failed");
}

if ([wcdb isOpened]) {
  NSLog(@"database is already opened");
}

4.2.4 建表与ORM

  • FMDB不支持ORM,而WCDB可以通过绑定类与表绑定起来,从而大幅度减少代码量。
  • 对于在FMDB已经定义的类:
//Message.h
@interface Message : NSObject

@property int localID;
@property(retain) NSString *content;
@property(retain) NSDate *createTime;
@property(retain) NSDate *modifiedTime;

@end
  • 对于在FMDB已经定义的表:
FMDatabase* fmdb = [[FMDatabase alloc] initWithPath:path];
[fmdb executeUpdate:@"CREATE TABLE message(localID INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT, createTime INTEGER, db_modifiedTime INTEGER)"];
[fmdb executeUpdate:@"CREATE INDEX message_index ON message(createTime)"];
  • 可以将其建模为:
//Message.h
@interface Message : NSObject <WCTTableCoding>

@property int localID;
@property(retain) NSString *content;
@property(retain) NSDate *createTime;
@property(retain) NSDate *modifiedTime;

WCDB_PROPERTY(localID)
WCDB_PROPERTY(content)
WCDB_PROPERTY(createTime)
WCDB_PROPERTY(modifiedTime)

@end

//Message.mm
@implementation Message

WCDB_IMPLEMENTATION(Message)
WCDB_SYNTHESIZE(Message, localID)
WCDB_SYNTHESIZE(Message, content)
WCDB_SYNTHESIZE(Message, createTime)
WCDB_SYNTHESIZE_COLUMN(Message, modifiedTime, "db_modifiedTime")

WCDB_PRIMARY_AUTO_INCREMENT(Message, localID)
WCDB_INDEX(Message, "_index", createTime)

@end

其中:

  1. WCDB_IMPLEMENTATION(className)用于定义进行绑定的类
  2. WCDB_PROPERTY(propertyName)WCDB_SYNTHESIZE(className, propertyName)用于声明和定义字段。
  3. WCDB_SYNTHESIZE(className, propertyName)默认使用属性名作为数据库表的字段名。对于属性名与字段名不同的情况,可以使用WCDB_SYNTHESIZE_COLUMN(className, propertyName, columnName)进行映射。
    对于在FMDB已经创建的表,若属性名与字段名不同,则可以用WCDB_SYNTHESIZE_COLUMN宏进行映射,如例子中的db_modifiedTime字段
  4. WCDB_PRIMARY_AUTO_INCREMENT(className, propertyName)用于定义主键且自增。
  5. WCDB_INDEX(className, indexNameSubfix, propertyName)用于定义索引。
  • 定义完成后,调用createTableAndIndexesOfName:withClass:即可完成创建。
WCTDatabase* wcdb = [[WCTDatabase alloc] initWithPath:path];
[wcdb createTableAndIndexesOfName:@"message" withClass:Message.class]

注:该接口使用的是IF NOT EXISTS的SQL,因此可以用重复调用。不需要在每次调用前判断表或索引是否已经存在。

4.2.5 数据库升级

createTableAndIndexesOfName:withClass:会根据ORM的定义,创建表或索引。
当定义发生变化时,该接口也会对应的增加字段或索引。
因此,该接口可用于数据库表的升级。

  • 定义模型
//Message.h
@interface Message : NSObject <WCTTableCoding>

@property int localID;
@property(assign) const char *newContent;
//@property(retain) NSDate *createTime;
@property(retain) NSDate *modifiedTime;
@property(retain) NSDate *newProperty;

WCDB_PROPERTY(localID)
WCDB_PROPERTY(newContent)
//WCDB_PROPERTY(createTime)
WCDB_PROPERTY(modifiedTime)
WCDB_PROPERTY(newProperty)

@end

//Message.mm
@implementation Message

WCDB_IMPLEMENTATION(Message)
WCDB_SYNTHESIZE(Message, localID)
WCDB_SYNTHESIZE_COLUMN(Message, newContent, "content")
//WCDB_SYNTHESIZE(Message, createTime)
WCDB_SYNTHESIZE_COLUMN(Message, modifiedTime, "db_modifiedTime")
WCDB_SYNTHESIZE(Message, newProperty)

WCDB_PRIMARY_AUTO_INCREMENT(Message, localID)
WCDB_INDEX(Message, "_index", createTime)
WCDB_UNIQUE(Message, modifiedTime)
WCDB_INDEX(Message, "_newIndex", newProperty)

@end
  • 新建表
WCTDatabase* db = [[WCTDatabase alloc] initWithPath:path];
[db createTableAndIndexesOfName:@"message" withClass:Message.class]
  • 删除字段

如例子中的createTime字段,删除字段只需直接将ORM中的定义删除即可。

注:由于SQLite不支持删除字段,因此该操作只是将对应字段忽略。

  • 增加字段

如例子中的newProperty字段,增加字段只需直接在ORM定义出添加,并再次调用createTableAndIndexesOfName:withClass:

  • 修改字段

如例子中的newContent字段,字段类型可以直接修改,但需要确保新类型与旧类型兼容;字段名称则需要通过WCDB_SYNTHESIZE_COLUMN(className, proeprtyName, columnName)重新映射到旧字段。

注:由于SQLite不支持修改字段名,因此该操作只是将新的属性映射到原来的字段名。

  • 增加约束

如例子中的WCDB_UNIQUE(Message, modifiedTime),新的约束只需直接在ORM中添加,并再次调用createTableAndIndexesOfName:withClass:

  • 增加索引

如例子中的WCDB_INDEX(Message, "_newIndex", newProperty),新的索引只需直接在ORM添加,并再次调用createTableAndIndexesOfName:withClass:

4.2.6 访问数据库

  • 得益于ORM的定义,开发者无需使用类似intForColumnIndex:的接口手动组装Object。以下是增删查改的代码示例。
4.2.6.1 查询
  • FMDB代码
/*
 FMDB Code
 */
FMResultSet *resultSet = [fmdb executeQuery:@"SELECT * FROM message"];
NSMutableArray<Message *> *messages = [[NSMutableArray alloc] init];
while ([resultSet next]) {
    Message *message = [[Message alloc] init];
    message.localID = [resultSet intForColumnIndex:0];
    message.content = [resultSet stringForColumnIndex:1];
    message.createTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:2]];
    message.modifiedTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:3]];
    [messages addObject:message];
}
  • WCDB代码
NSArray<Message *> *messages = [wcdb getAllObjectsOfClass:Message.class fromTable:@"message"];
4.2.6.2 插入
  • FMDB代码
/*
 FMDB Code
 */
[fmdb executeUpdate:@"INSERT INTO message VALUES(?, ?, ?, ?)", @(message.localID), message.content, @(message.createTime.timeIntervalSince1970), @(message.modifiedTime.timeIntervalSince1970)];
  • WCDB代码
[wcdb insertObject:message into:@"message"];
4.2.6.3 修改
  • FMDB代码
/*
 FMDB Code
 */
[fmdb executeUpdate:@"UPDATE message SET modifiedTime=?", @(message.modifiedTime.timeIntervalSince1970)];
  • WCDB代码
[wcdb updateAllRowsInTable:@"message" onProperties:Message.modifiedTime withObject:message];
4.2.6.4 删除
  • FMDB代码
/*
 FMDB Code
 */
[fmdb executeUpdate:@"DELETE FROM message"];
  • WCDB代码
[wcdb deleteAllObjects];

4.2.7 条件语句

  • WCDB通过WINQ完成条件语句,以减轻了拼装SQL的繁琐,并提供一系列优化和反注入等特性。
  • 以下是SQLWINQ之间转换的一些例子。
类型SQL示例WINQ示例
排序ORDER BY localID ASCMessage.localID.order(WCTOrderedAscending)
多字段排序ORDER BY localID ASC, content DESC{Message.localID.order(WCTOrderedAscending), Message.content.order(WCTOrderedDescending)}
聚合函数MAX(localID)Message.localID.max()
条件语句localID==2 AND content IS NOT NULLMessage.localID==2&&Message.content.isNotNull()
多个字段组合localID, content{Message.localID, Message.content}
*COUNT(*)Message.AnyProperty.count()
所有ORM定义的字段(localID, content, createTime, modifiedTime)Message.AllProperties
指定tablemyTable.localIDMessage.localID.inTable(“myTable”)
4.2.7.1 改写条件语句
  • 了解了WINQ,就可以完成更复杂的增删查改操作了。
4.2.7.1.1 部分查询
  • FMDB代码
/*
 FMDB Code
 */
NSMutableArray<Message *> *messages = [[NSMutableArray alloc] init];
FMResultSet* resultSet = [fmdb executeQuery:@"SELECT localID, createTime FROM message WHERE localID>=1 OR modified!=createTime"];
while (resultSet && [resultSet next]) {
    Message *message = [[Message alloc] init];
    message.localID = [resultSet intForColumnIndex:0];
    message.createTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:2]];
    [messages addObject:message];
}
  • WCDB代码
NSArray *messages = [wcdb getObjectsOnResults:{Message.localID, Message.createTime} 
                                    fromTable:@"message"
                                        where:Message.localID>1||Message.modifiedTime!=Message.createTime];
4.2.7.1.2 自增插入
  • FMDB代码
/*
 FMDB Code
 */
[fmdb executeUpdate:@"INSERT INTO message(localID, content) VALUES(?, ?)", nil, message.content];
  • WCDB代码
message.isAutoIncrement = YES;
[wcdb insertObject:message 
          onProperties:{Message.localID, Message.content} 
                  into:@"message"];
4.2.7.1.3 数值更新
  • FMDB代码
/*
 FMDB Code
 */
[fmdb executeUpdate:@"UPDATE message SET modifiedTime=? WHERE localID==?", @([NSDate date].timeIntervalSince1970), @(1)];
  • WCDB代码
[wcdb updateRowsInTable:@"message" 
             onProperty:Message.modifiedTime 
              withValue:[NSDate date]
                  where:Message.localID==1];
4.2.7.1.4 部分删除
  • FMDB代码
/*
 FMDB Code
 */
[fmdb executeUpdate:@"DELETE FROM message WHERE localID>0 AND content IS NULL LIMIT ?", @(1)];
  • WCDB代码
[wcdb deleteObjectsFromTable:@"messsage" 
                           where:Message.localID>0&&Message.content!=nil
                           limit:1];

4.2.8 特殊语句和核心层接口

  • WCDBObjC层接口封装了绝大部分场景下适用的增删查改语句。但SQL千变万化,接口层不可能覆盖全部场景。对于这种情况,可以通过WINQ的核心层接口进行调用。
  • 对于SQL:EXPLAIN QUERY PLAN CREATE TABLE message(localID INTEGER)
  1. 找到其对应的sql-stmt,然后通过以WCDB::Statement开头的类进行调用。如例子中,其对应的sql-stmt为WCDB::StatementExplain和WCDB::StatementCreateTable。
  2. 获取字段映射。对于已经定义ORM的字段,可以通过className.propertyName获取,如:Message.localID。对于未定义ORM的字段,可以通过WCDB::Column columnName(“columnName”)创建,如 WCDB::Column localID(“localID”).
  3. 根据Statement内的定义,按照与SQL同名的函数调用获得完整的WINQ语句。如例子中,其对应的WINQ语句为:
WCDB::ColumnDefList columnDefList = {WCTSampleORM.identifier.def(WCTColumnTypeInteger32, true)};
WCDB::StatementExplain statementExplain = WCDB::StatementExplain().explainQueryPlan(WCDB::StatementCreateTable().create("message", columnDefList));
4.2.8.1 执行WINQ

通过exec:执行WINQ statement

[wcdb exec:statement];
4.2.8.2 获取WINQ运行结果

通过prepare:运行WINQ statement,获得WCTStatement,并以此获取返回值。

WCTStatement *statement = [wcdb prepare:statementExplain];
if (statement && [statement step]) {
    for (int i = 0; i < [statement getCount]; ++i) {
        NSString *columnName = [statement getNameAtIndex:i];
        WCTValue *value = [statement getValueAtIndex:i];
        NSLog(@"%@:%@", columnName, value);
    }
}
  1. 通过getDescription()打印log,调试确保SQL正确
NSLog(@"SQL: %s", statementExplain.getDescription().c_str());

4.2.9 事务

  • WCDB的基础事务接口与FMDB的接口类似。

  • FMDB代码

/*
 FMDB Code
 */
BOOL result = [fmdb beginTransaction];
if (!result) {
    //failed
}
//do sth...
if (![fmdb commit]) {
    //failed
    [fmdb rollback];
}
  • WCDB代码
/*
 WCDB Code
 */
BOOL result = [wcdb beginTransaction];
if (!result) {
    //failed
}
//do sth...
if (![wcdb commitTransaction]) {
    [wcdb rollbackTransaction];
}
4.2.9.1 便捷事务接口
  • runTransaction:接口会在commit失败时自动rollback事务。开发者也可以在BLOCK结束时返回YESNO来决定commitrollback事务,以此减少代码量。
[wcdb runTransaction:^BOOL{
    //do sth...
    return result;//YES to commit transaction and NO to rollback transaction
}];

4.2.10 多重语句和批处理

  • WCDB不支持多重语句。多个语句需拆分单独写。

  • WCDB对于涉及批量操作的接口,都有内置的事务。如createTableAndIndexesOfName:withClass:insertObjects:into:等,这类接口通常不止执行一条SQL,因此WCDB会自动嵌入事务,以提高性能。

4.2.11 线程安全与并发

  • FMDB通过FMDatabasePool完成多线程任务。

  • 而对于WCDBWCTDatabaseWCTTableWCTTransaction的所有SQL操作接口都是线程安全,并且自动管理并发的。

  • WCDB的连接池会根据数据库访问所在的线程、是否处于事务、并发状态等,自动分发合适的SQLite连接进行操作,并在完成后回收以供下一次再利用。

  • 因此,开发者既不需要使用一个新的类来完成多线程任务,也不需要过多关注线程安全的问题。同时,还能获得更高的性能表现。

  • FMDB代码

/*
 FMDB Code
 */
//thread-1 read
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
    [fmdbPool inDatabase:^(FMDatabase *_Nonnull db) {
        NSMutableArray *messages = [[NSMutableArray alloc] init];
        FMResultSet *resultSet = [db executeQuery:@"SELECT * FROM message"];
        while ([resultSet next]) {
            Message *message = [[Message alloc] init];
            message.localID = [resultSet intForColumnIndex:0];
            message.content = [resultSet stringForColumnIndex:1];
            message.createTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:2]];
            message.modifiedTime = [NSDate dateWithTimeIntervalSince1970:[resultSet doubleForColumnIndex:3]];
            [messages addObject:message];
        }
        //...
    }];
});
//thread-2 write
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
    [fmdbPool inDatabase:^(FMDatabase *_Nonnull db) {
		[db beginTransaction]
        for (Message *message in messages) {
            [db executeUpdate:@"INSERT INTO message VALUES(?, ?, ?, ?)", @(message.localID), message.content, @(message.createTime.timeIntervalSince1970), @(message.modifiedTime.timeIntervalSince1970)];
        }
        if (![db commit]) {
            [db rollback];
        }
    }];
});
  • WCDB代码
/*
 WCDB Code
 */
//thread-1 read
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
    NSArray *messages = [wcdb getAllObjectsOfClass:Message.class fromTable:@"message"];
    //...
});
//thread-2 write
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
    [wcdb insertObjects:messages into:@"message"];
});

4.2.12 配置

  • 在使用数据库时,通常会对其设置一些默认的配置,如cache_sizejournal_mode等。

  • FMDB通过FMDatabasePoolDelegate进行配置,但其只能在SQLite Handle创建时进行配置。对于已经产生的SQLite handle,很难再次更改配置。

  • WCDB可以随时灵活地对其设置或变更。

  • FMDB代码

/*
 FMDB Code
 */
- (BOOL)databasePool:(FMDatabasePool *)pool shouldAddDatabaseToPool:(FMDatabase *)database
{
    FMResultSet* resultSet = [database executeQuery:@"PRAGMA cache_size=-2000"];
	[result next];
}
  • WCDB代码
/*
 WCDB Code
 */
[wcdb setConfig:^BOOL(std::shared_ptr<WCDB::Handle> handle, WCDB::Error &error) {
    return handle->exec(WCDB::StatementPragma().pragma(WCDB::Pragma::CacheSize, -2000));
} forName:@"CacheSizeConfig"]'

4.2.13 关闭数据库

  • 关闭数据库通常有两种场景:
  1. 数据库使用结束,回收对象。
  2. 数据库进行某些操作,需要临时关闭数据库。如移动、复制数据库文件。
4.2.13.1 回收对象
  • 对于这种情况,开发者无需手动操作。WCDB会自动管理这个过程。对于某一路径的数据库,WCDB会在所有对其的引用释放时,自动关闭数据库,并回收资源。
  • 对于iOS平台,当内存不足时,WCDB会自动关闭空闲的SQLite连接,以节省内存。开发者也可以手动调用[db purgeFreeHandles]对清理单个数据库的空闲SQLite连接。或调用[WCTDatabase PurgeFreeHandlesInAllDatabases]清理所有数据库的空闲SQLite连接。
4.2.13.2 手动关闭数据库
  • 无论是WCDB的多线程管理,还是FMDBFMDatabasePool,都存在多线程关闭数据库的问题。即,当一个线程希望关闭数据库时,另一个线程还在继续执行操作。

  • 而某些特殊的操作需要确保数据库完全关闭,例如移动、重命名、删除数据库等文件层面的操作。

  • 例如,若在A线程进行插入操作的执行过程中,B线程尝试复制数据库,则复制后的新数据库很可能是一个损坏的数据库。

  • 因此,WCDB提供了close:接口确保完全关闭数据库,并阻塞其他线程的访问。

[wcdb close:^(){
    //do something on this closed database
 }];

4.2.14 隔离Objective-C++代码

  • WCDB基于WINQ,引入了Objective-C++代码,因此对于所有引入WCDB的源文件,都需要将其后缀.m改为.mm。为减少影响范围,可以通过Objective-Ccategory特性将其隔离,达到只在model层使用Objective-C++编译,而不影响controllerview
  • 对于已有类WCTSampleAdvance
//WCTSampleAdvance.h
#import <Foundation/Foundation.h>
#import "WCTSampleColumnCoding.h"

@interface WCTSampleAdvance : NSObject

@property(nonatomic, assign) int intValue;
@property(nonatomic, retain) WCTSampleColumnCoding *columnCoding;

@end
  
//WCTSampleAdvance.mm
@implementation WCTSampleAdvance
  
@end
  • 可以创建WCTSampleAdvance (WCTTableCoding)专门用于定义ORM
  • 为简化定义代码,WCDB同样提供了文件模版.
4.2.14.1 WCTTableCoding文件模版
  • 为了简化定义,WCDB同样提供了Xcode文件模版来创建WCTTableCodingcategory
  1. 首先需要安装文件模版。

安装脚本集成在WCDB的编译脚本中,只需编译一次WCDB,就会自动安装文件模版。
也可以手动运行cd path-to-your-wcdb-dir/objc/templates; sh install.sh;手动安装 文件模版。

  1. 安装完成后重启Xcode,选择新建文件,滚到窗口底部,即可看到对应的文件模版.

  2. 选择WCTTableCoding ,输入需要实现WCTTableCoding的类

  3. 这里以WCTSampleAdvance为例,Xcode会自动创建WCTSampleAdvance+WCTTableCoding.h文件模版:

#import "WCTSampleAdvance.h"
#import <WCDB/WCDB.h>

@interface WCTSampleAdvance (WCTTableCoding) <WCTTableCoding>

WCDB_PROPERTY(<#property1 #>)
WCDB_PROPERTY(<#property2 #>)
WCDB_PROPERTY(<#property3 #>)
WCDB_PROPERTY(<#property4 #>)
WCDB_PROPERTY(<#... #>)

@end
  1. 加上类的ORM实现即可。
//WCTSampleAdvance.h
#import <Foundation/Foundation.h>
#import "WCTSampleColumnCoding.h"

@interface WCTSampleAdvance : NSObject

@property(nonatomic, assign) int intValue;
@property(nonatomic, retain) WCTSampleColumnCoding *columnCoding;

@end
  
//WCTSampleAdvance.mm
@implementation WCTSampleAdvance
  
WCDB_IMPLEMENTATION(WCTSampleAdvance)
WCDB_SYNTHESIZE(WCTSampleAdvance, intValue)
WCDB_SYNTHESIZE(WCTSampleAdvance, columnCoding)

WCDB_PRIMARY_ASC_AUTO_INCREMENT(WCTSampleAdvance, intValue)

@end
  
//WCTSampleAdvance+WCTTableCoding.h
#import "WCTSampleAdvance.h"
#import <WCDB/WCDB.h>

@interface WCTSampleAdvance (WCTTableCoding) <WCTTableCoding>

WCDB_PROPERTY(intValue)
WCDB_PROPERTY(columnCoding)

@end
  1. 此时,原来的WCTSampleAdvance.h中不包含任何C++的代码。因此,其他文件对其引用时,不需要修改文件名后缀。只有Model层需要使用WCDB接口的类,才需要包含WCTSampleAdvance+WCTTableCoding.h,并修改文件名后缀为.mm。
  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值