一、WCDB介绍
1、概述
WCDB(WeChat DataBase)是微信官方的移动端数据库组件,致力于提供一个高效、易用、完整的移动端存储方案。基于SQLCipher,支持iOS, macOS和Android。
备注:WCDB使用的SQLCipher是fork了原版本且修改过的。
SQLCipher is an SQLite extension that provides 256 bit AES encryption of database files.
2、基本特性
易用,通过ORM,可以达到直接通过Object进行数据库操作,省去拼装过程。
- WINQ(WCDB Integrated Query):通过WINQ,开发者无须拼接字符串,即可完成SQL的条件、排序、过滤等语句。
备注:LINQ(Language Integrated Query)语言集成查询。 - ORM(Object Relational Mapping):开发者可以很便捷地定义表、索引、约束,并进行增删改查(CRUD)操作。
高效,WCDB通过框架层和sqlcipher源码优化,使其更高效的表现。
- 多线程高并发:WCDB支持多线程读与读、读与写并发执行,写与写串行执行。
- 批量写操作性能测试:
更多关于WCDB的性能数据,请参考benchmark。
参考:《微信iOS SQLite源码优化实践》
完整,WCDB覆盖了数据库相关各种场景的所需功能。
- 加密:WCDB提供基于SQLCipher的数据库加密。
- 损坏修复:WCDB内建了Repair Kit用于修复损坏的数据库。
- 反注入:WCDB内建了对SQL注入的保护。
二、安装
方式:
- 通过Carthage安装
- 通过cocoapods安装
- 手动安装
安装请参考 README
版本:
- 1.0.3 目前版本是1.0.3,但是在pod install之后编译错误,找不到#include
这个文件。 - 1.0.2 pod install的时候时间比较长,可能主要是在下载sqlcipher相关的代码吧。
-
文件对比
三、使用
1、修改Model
// UserModel.h
#import
@interface UserModel : NSObject
@property (nonatomic, copy) NSString *userID;
@property (nonatomic, copy) NSString *username;
WCDB_PROPERTY(userID)
WCDB_PROPERTY(username)
@end
// UserModel.mm
#import "UserModel.h"
@implementation UserModel
WCDB_IMPLEMENTATION(UserModel)
WCDB_SYNTHESIZE(UserModel, userID)
WCDB_SYNTHESIZE(UserModel, username)
WCDB_PRIMARY(UserModel, userID)
WCDB_INDEX(UserModel, "_index", userID)
@end
将一个已有的ObjC类进行ORM绑定的过程如下:
- 定义该类遵循
WCTTableCoding
协议。可以在类声明上定义,在category内定义。 - 使用
WCDB_PROPERTY
宏在头文件声明需要绑定到数据库表的字段。 - 使用
WCDB_IMPLEMENTATIO
宏在类文件定义绑定到数据库表的类。 - 使用
WCDB_SYNTHESIZE
宏在类文件定义需要绑定到数据库表的字段。
2、创建表和索引
只需要调用createTableAndIndexesOfName:withClass:
接口,即可创建表和索引。
WCTDatabase *database = [[WCTDatabase alloc] initWithPath:path];
[database createTableAndIndexesOfName:UsersTableName withClass:UserModel.class];
3、CRUD
#pragma mark - 增
- (BOOL)insertUsers:(NSArray *)users {
return [self.database insertOrReplaceObjects:users
into:UsersTableName];
}
#pragma mark - 删
- (BOOL)deleteUserWithID:(NSString *)userID {
return [self.database deleteObjectsFromTable:UsersTableName
where:UserModel.userID == userID];
}
- (BOOL)deleteUsersWithIDs:(NSArray *)userIDs {
return [self.database deleteObjectsFromTable:UsersTableName
where:UserModel.userID.in(userIDs)];
}
#pragma mark - 改
- (BOOL)updateUserWithUserID:(NSString *)userID
username:(NSString *)username {
UserModel *user = [[UserModel alloc] init];
user.username = username;
BOOL result = [self.database updateRowsInTable:UsersTableName
onProperty:UserModel.username
withObject:user
where:UserModel.userID == userID];
return result;
}
#pragma mark - 查
- (NSArray *)getUserList {
return [self.database getAllObjectsOfClass:UserModel.class
fromTable:UsersTableName];
// WINQ
return [self.database getObjectsOfClass:UserModel.class
fromTable:UsersTableName
where:UserModel.gender == 1
orderBy:UserModel.userID.order(WCTOrderedDescending)
limit:10];
// WINQ
return [self.database getObjectsOfClass:UserModel.class
fromTable:UsersTableName
where:UserModel.userID.in(@[@"1", @"3"])];
}
4、多线程操作
WCDB与FMDB都支持多线程操作。
在FMDB内,当开发者需要进行多线程操作时,需要使用另外一个类FMDatabaseQueue
来进行操作。
而WCDB基础的CRUD接口都支持多线程,因此开发者不需要额外关心线程安全的问题。同样的,WCDB多线程使用的代码量也比FMDB少得多。
FMDB
/*
FMDB Code
*/
//thread-1 read
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
[_FMDatabaseQueue 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), ^{
[_FMDatabaseQueue 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];
}
}];
});
FMDB使用dispatch_queue_set_specific
方式确保在同一个线程中操作数据库,防止死锁。在初始化时用assert(sqlite3_threadsafe())
检测是否线程安全。
// 创建一个串行队列来执行数据库的所有操作
_queue = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL);
dispatch_queue_set_specific(_queue, kDispatchQueueSpecificKey, (__bridge void *)self, NULL);
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"];
});
WCDB在handle.cpp中也开启了数据库的SQLITE_CONFIG_MULTITHREAD多线程模式
// handle.cpp
const auto UNUSED_UNIQUE_ID = []() {
sqlite3_config(SQLITE_CONFIG_LOG, GlobalLog, nullptr);
sqlite3_config(SQLITE_CONFIG_MULTITHREAD);
sqlite3_config(SQLITE_CONFIG_MEMSTATUS, false);
// sqlite3_config(SQLITE_CONFIG_MMAP_SIZE, 0x7fff0000, 0x7fff0000);
return nullptr;
}();
5、数据库加密
NSData *cipherKey = [@"123456" dataUsingEncoding:NSASCIIStringEncoding];
[database setCipherKey:cipherKey];
加密之后就无法打开数据库文件查看了。
6、全局监控
WCDB提供了对错误和性能的全局监控,可用于调试错误和性能。
- (void)registerWCDBTrace {
// Error Monitor
[WCTStatistics SetGlobalErrorReport:^(WCTError *error) {
NSLog(@"[WCDB]%@", error);
}];
// 监控所有db的数据库操作耗时,该接口需要在所有db打开、操作之前调用
[WCTStatistics SetGlobalPerformanceTrace:^(WCTTag tag, NSDictionary *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);
}];
//SQL Execution Monitor
[WCTStatistics SetGlobalSQLTrace:^(NSString *sql) {
NSLog(@"SQL: %@", sql);
}];
}
7、Repair Kit
参考:《数据库修复三板斧》
四、ORM
WCDB使用内置的宏来连接类、属性与表、字段。
共有三类宏,分别对应数据库的字段、索引和约束。所有宏都定义在WCTCodingMacro.h
中。
基本类型
SQLite数据库的字段有整型、浮点数、字符串、二进制数据等五种类型。WCDB的ORM会自动识别property的类型,并映射到适合的数据库类型。
typedef NS_ENUM(int, WCTColumnType) {
WCTColumnTypeInteger32 = (WCTColumnType) WCDB::ColumnType::Integer32,
WCTColumnTypeInteger64 = (WCTColumnType) WCDB::ColumnType::Integer64,
WCTColumnTypeDouble = (WCTColumnType) WCDB::ColumnType::Float,
WCTColumnTypeString = (WCTColumnType) WCDB::ColumnType::Text,
WCTColumnTypeBinary = (WCTColumnType) WCDB::ColumnType::BLOB,
WCTColumnTypeNil = (WCTColumnType) WCDB::ColumnType::Null,
};
自定义类型
自定义类型需要实现WCTColumnCoding
协议。
@protocol WCTColumnCoding
@required
+ (instancetype)unarchiveWithWCTValue:(WCTValue *)value; //value could be nil
- (id /* WCTValue* */)archivedWCTValue; //value could be nil
+ (WCTColumnType)columnTypeForWCDB;
@end
大概流程
在WCTRuntimeObjCAccessor.mm
文件里面使用runtime做Model的相互转换
WCTRuntimeObjCAccessor::ValueGetter WCTRuntimeObjCAccessor::generateValueGetter(Class instanceClass, const std::string &propertyName)
{
static const SEL ArchiveSelector = NSSelectorFromString(@"archivedWCTValue");
Class propertyClass = GetPropertyClass(instanceClass, propertyName);
IMP implementation = GetInstanceMethodImplementation(propertyClass, ArchiveSelector);
return [this, propertyClass, implementation](InstanceType instance) -> OCType {
using Archiver = OCType (*)(InstanceType, SEL);
PropertyType property = getProperty(instance);
OCType value = property ? ((Archiver) implementation)(property, ArchiveSelector) : nil;
return value;
};
}
WCTRuntimeObjCAccessor::ValueSetter WCTRuntimeObjCAccessor::generateValueSetter(Class instanceClass, const std::string &propertyName)
{
static const SEL UnarchiveSelector = NSSelectorFromString(@"unarchiveWithWCTValue:");
Class propertyClass = GetPropertyClass(instanceClass, propertyName);
IMP implementation = GetClassMethodImplementation(propertyClass, UnarchiveSelector);
return [this, propertyClass, implementation](InstanceType instance, OCType value) {
using Unarchiver = PropertyType (*)(Class, SEL, OCType);
if (instance) {
PropertyType property = ((Unarchiver) implementation)(propertyClass, UnarchiveSelector, value);
setProperty(instance, property);
}
};
}
WCTColumnType WCTRuntimeObjCAccessor::GetColumnType(Class instanceClass, const std::string &propertyName)
{
static const SEL ColumnTypeSelector = NSSelectorFromString(@"columnTypeForWCDB");
Class propertyClass = GetPropertyClass(instanceClass, propertyName);
if (![propertyClass conformsToProtocol:@protocol(WCTColumnCoding)]) {
WCDB::Error::Abort([NSString stringWithFormat:@"[%@] should conform to WCTColumnCoding protocol, which is the class of [%@ %s]", NSStringFromClass(propertyClass), NSStringFromClass(instanceClass), propertyName.c_str()].UTF8String);
}
IMP implementation = GetClassMethodImplementation(propertyClass, ColumnTypeSelector);
using GetColumnTyper = WCTColumnType (*)(Class, SEL);
return ((GetColumnTyper) implementation)(propertyClass, ColumnTypeSelector);
}
在handle_statement
文件里面操作sqlite3方法保存和获取数据。
sqlite3_bind_text(statement, 1, userID, -1, NULL)
sqlite3_column_text(statement, columnIdx)
参考:ORM使用教程
五:WINQ(WCDB语言集成查询)
WINQ(WCDB Integrated Query,音'wink'),是将自然查询的SQL集成到WCDB框架中的技术,基于C++实现。
传统的SQL语句,通常是开发者拼接字符串完成。这种方式不仅繁琐、易错,而且出错后很难定位到问题所在。同时也容易给SQL注入留下可乘之机。
而WINQ将查询语言集成到了C++中,可以通过类似函数调用的方式来写SQL查询。借用IDE的代码提示和编译器的语法检查,达到易用、纠错的效果。
参考:WINQ原理
六:DEMO演示
1、修改Model为WCTObject
typedef NSObject WCTObject;
2、使用Model的Category方法
减少修改为.mm的文件
3、自定义属性
七:参考资源
- 微信iOS SQLite源码优化实践
- 数据库修复三板斧
- ORM使用教程
- WINQ原理
- 从FMDB迁移到WCDB