IOS数据存储 之WCDB (一)
1. WCDB 简介
- WCDB是腾讯开发的,微信中使用的DB开源框架:
引用官方说法:“WCDB是一个易用、高效、完整的移动数据库框架,它基于 SQLite 和 SQLCipher 开发。”
1.1 使用WCDB框架3大优势
- 易用性
- one line of code 是它坚持的原则,大多数操作只需要一行代码即可完成.
- 使用WINQ 语句查询,不用为拼接SQL语句而烦恼了,模型绑定映射也是按照规定模板去实现方便快捷。
- 高效性
- 和fmdb做对比
- 完整性
- 支持基于SQLCipher 加密
- 持全文搜索
- 支持反注入,可以避免第三方从输入框注入 SQL,进行预期之外的恶意操作。
- 用户不用手动管理数据库字段版本,升级方便自动.
- 提供数据库修复工具。
1.2 WCDB 的一些基础概念
1.2.1 类字段绑定(ORM)
- ORM定义:
在WCDB内,ORM(Object Relational Mapping)是指
- 将一个ObjC的类,映射到数据库的表和索引;
- 将类的property,映射到数据库表的字段;
- 这一过程。通过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绑定的过程如下:
- 定义该类遵循WCTTableCoding协议。可以在类声明上定义,也可以通过文件模版在category内定义。
- 使用WCDB_PROPERTY宏在头文件声明需要绑定到数据库表的字段。
- 使用WCDB_IMPLEMENTATIO宏在类文件定义绑定到数据库表的类。
- 使用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简介
- WINQ(WCDB Integrated Query,音’wink’),是将自然查询的SQL集成到WCDB框架中的技术,基于C++实现。
- 传统的SQL语句,通常是开发者拼接字符串完成。这种方式不仅繁琐、易错,而且出错后很难定位到问题所在。同时也容易给SQL注入留下可乘之机。
- 而WINQ将查询语言集成到了C++中,可以通过类似函数调用的方式来写SQL查询。借用IDE的代码提示和编译器的语法检查,达到易用、纠错的效果。
- WINQ的使用上接近于C函数调用。对于熟悉SQL的开发者,无须特别学习即可立刻上手使用.
- WINQ原理:请参考官方文档:WINQ原理
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 Code | SQLite的扩展码 | 请参考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];
- 当检测到数据库损坏,即
WCTError
的type
为WCTErrorTypeSQLite
,code
为11或26(SQLITE_CORRUPT
或SQLITE_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操作进行全局监控,也支持监控单个特定的数据库.
所有监控的返回数据都相同,包括三个数据:
- Tag,执行操作的数据库的tag
- sqls,执行的SQL和对应的次数。
对于非事务操作,则为单条SQL
对于事务操作,则为该次事务所执行的所有SQL和每个sql执行的次数- 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);
}];
- 操作耗时与监控耗时的区别
- 操作耗时的
cost
返回的耗时为浮点数的秒,监控耗时的cost返回的耗时为整型的纳秒。- 监控耗时仅包括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)操作。开发者可以通过
WCTDatabase
和WCTTable
两个类进行一般的增删改查操作。
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 及以上
- 腾讯官方文档:WCDB.swift安装与兼容性
- pod 安装:
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?
- WCDB依托于微信上亿用户的实际场景,解决了许多在开发和线上遇到的共性问题,在性能、易用性、功能完整性以及兼容性上都有较好的表现。并且,开发者可以平滑地从FMDB升级到WCDB。
- WCDB有三大优势:更高效,更易用,功能更完整,具体优势比较上面已经对比过了。
4.1.1 WCDB 对比FMDB的优势一:高效
- WCDB在并发、ORM以及SQLite源码都做了许多针对性的优化,使得在写入、多线程并发、初始化等方面比FMDB有30%-280%的性能提升。
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
其中:
WCDB_IMPLEMENTATION(className)
用于定义进行绑定的类WCDB_PROPERTY(propertyName)
和WCDB_SYNTHESIZE(className, propertyName)
用于声明和定义字段。WCDB_SYNTHESIZE(className, propertyName)
默认使用属性名作为数据库表的字段名。对于属性名与字段名不同的情况,可以使用WCDB_SYNTHESIZE_COLUMN(className, propertyName, columnName)
进行映射。
对于在FMDB已经创建的表,若属性名与字段名不同,则可以用WCDB_SYNTHESIZE_COLUMN
宏进行映射,如例子中的db_modifiedTime
字段WCDB_PRIMARY_AUTO_INCREMENT(className, propertyName)
用于定义主键且自增。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
的繁琐,并提供一系列优化和反注入等特性。- 以下是
SQL
和WINQ
之间转换的一些例子。
类型 | SQL示例 | WINQ示例 |
---|---|---|
排序 | ORDER BY localID ASC | Message.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 NULL | Message.localID==2&&Message.content.isNotNull() |
多个字段组合 | localID, content | {Message.localID, Message.content} |
* | COUNT(*) | Message.AnyProperty.count() |
所有ORM定义的字段 | (localID, content, createTime, modifiedTime) | Message.AllProperties |
指定table | myTable.localID | Message.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 特殊语句和核心层接口
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语句为:
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);
}
}
- 通过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
结束时返回YES
或NO
来决定commit
或rollback
事务,以此减少代码量。
[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
完成多线程任务。 -
而对于
WCDB
,WCTDatabase
、WCTTable
和WCTTransaction
的所有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_size
、journal_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 关闭数据库
- 关闭数据库通常有两种场景:
- 数据库使用结束,回收对象。
- 数据库进行某些操作,需要临时关闭数据库。如移动、复制数据库文件。
4.2.13.1 回收对象
- 对于这种情况,开发者无需手动操作。
WCDB
会自动管理这个过程。对于某一路径的数据库,WCDB
会在所有对其的引用释放时,自动关闭数据库,并回收资源。 - 对于
iOS
平台,当内存不足时,WCDB
会自动关闭空闲的SQLite
连接,以节省内存。开发者也可以手动调用[db purgeFreeHandles]
对清理单个数据库的空闲SQLite
连接。或调用[WCTDatabase PurgeFreeHandlesInAllDatabases]
清理所有数据库的空闲SQLite
连接。
4.2.13.2 手动关闭数据库
-
无论是
WCDB
的多线程管理,还是FMDB
的FMDatabasePool
,都存在多线程关闭数据库的问题。即,当一个线程希望关闭数据库时,另一个线程还在继续执行操作。 -
而某些特殊的操作需要确保数据库完全关闭,例如移动、重命名、删除数据库等文件层面的操作。
-
例如,若在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-C
的category
特性将其隔离,达到只在model
层使用Objective-C++
编译,而不影响controller
和view
。- 对于已有类
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
文件模版来创建WCTTableCoding
的category
。
- 首先需要安装文件模版。
安装脚本集成在WCDB的编译脚本中,只需编译一次WCDB,就会自动安装文件模版。
也可以手动运行cd path-to-your-wcdb-dir/objc/templates; sh install.sh;
手动安装 文件模版。
-
安装完成后重启Xcode,选择新建文件,滚到窗口底部,即可看到对应的文件模版.
-
选择WCTTableCoding ,输入需要实现WCTTableCoding的类
-
这里以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
- 加上类的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
- 此时,原来的
WCTSampleAdvance.h
中不包含任何C++的代码。因此,其他文件对其引用时,不需要修改文件名后缀。只有Model
层需要使用WCDB
接口的类,才需要包含WCTSampleAdvance+WCTTableCoding.h
,并修改文件名后缀为.mm。