WCDB初体验

一、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相关的代码吧。
  • 文件对比


    WCDB初体验_第1张图片
    1.0.3和1.0.2文件对比

三、使用

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

你可能感兴趣的:(WCDB初体验)