WCDB初体验

作者: 785aab7894ed | 来源:发表于2017-08-30 15:44 被阅读211次

    一、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提供基于SQLCipher的数据库加密。
    • 损坏修复:WCDB内建了Repair Kit用于修复损坏的数据库。
    • 反注入:WCDB内建了对SQL注入的保护。

    二、安装

    方式:

    • 通过Carthage安装
    • 通过cocoapods安装
    • 手动安装

    安装请参考 README

    版本:

    • 1.0.3 目前版本是1.0.3,但是在pod install之后编译错误,找不到#include <sqlcipher/sqlite3.h>这个文件。
    • 1.0.2 pod install的时候时间比较长,可能主要是在下载sqlcipher相关的代码吧。
    • 文件对比


      1.0.3和1.0.2文件对比

    三、使用

    1、修改Model

    // UserModel.h
    #import <WCDB/WCDB.h>
    
    @interface UserModel : NSObject <WCTTableCoding>
    
    @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<UserModel *> *)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<NSString *> *)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<UserModel *> *)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<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);
        }];
        
        //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<WCTTableCoding> WCTObject;
    

    2、使用Model的Category方法

    减少修改为.mm的文件

    3、自定义属性

    七:参考资源

    相关文章

      网友评论

      • 冰三尺:请问作者有了解过Swift版本的WCDB吗? 为什么Swift 版本的找不到删除表的方法?
        785aab7894ed:没有看过Swift版本

      本文标题:WCDB初体验

      本文链接:https://www.haomeiwen.com/subject/qrmzdxtx.html