美文网首页
iOS 数据库-WCDB

iOS 数据库-WCDB

作者: 小和大大 | 来源:发表于2022-10-20 11:10 被阅读0次

    WCDB —— 高性能易用的 SQLite 面向对象组件

    WCDB 是基于 SQLite 的数据库封装,他简单易用,通过在结构体里定义描述表,就能方便地进行数据库操作;同时还保持着高性能,基本上与裸写 SQLite 相当。

    SQLite 简介

    SQLite 是一个精巧简单的嵌入式数据库,整个数据库只有一个文件,方便管理和备份。麻雀虽小五脏俱全。具体使用和优化经验这里不细述,可以参考 官方优化指南KM上的总结

    SQLite 提供了 C 接口,这些接口非常原始,实际使用中很少直接使用,一般都会封装一下方便开发。封装得好不好,直接影响开发效率

    • iOS 自带的 CoreData 是 SQLite 的封装之一,功能非常丰富(面向对象、数据库版本升级、多线程同步、内存缓存、表达式断言等),相应地也非常复杂庞大,学习曲线陡峭。
    • 微信早期自行封装的 CBaseDB 则处于另一个极端,封装得非常简单,基本上是将C接口封一层,很容易上手,同时也要写很多胶水代码;另外性能也不太好。
    • FMDB 类似 CBaseDB,性能不高,并且也不提供面向对象的接口,

    我们要做的是取两者之长弃其短,要面向对象,要简单易用不繁琐,用起来一个字:

    一、WCDB 快速上手

    头文件:

    @interface MMInfo : NSObject<WCDBCoding>
    
    WCDB_OBJ_PROPERTY_DEF(username, NSString);
    //@property (nonatomic, strong) NSString* username;
    @property (nonatomic, strong) NSString* nickname;
    @property (nonatomic, strong) NSString* signature;
    @property (nonatomic, assign) CGFloat height;
    @property (nonatomic, assign) CGFloat weight;
    @property (nonatomic, assign) UInt32 age;
    
    @end
    
    

    .mm 文件

    @implementation MMInfo
    
    WCDB_TABLE_BEGIN(MMInfo)
        WCDB_OBJ_PROPERTY(MMInfo, username, NSString, 1)
        WCDB_OBJ_PROPERTY(MMInfo, nickname, NSString, 2)
        WCDB_OBJ_PROPERTY(MMInfo, signature, NSString, 3)
        WCDB_FLOAT_PROPERTY(MMInfo, height, 4)
        WCDB_FLOAT_PROPERTY(MMInfo, weight, 5)
        WCDB_UINT32_PROPERTY(MMInfo, age, 6)
    WCDB_TABLE_END(MMInfo)
    
    @end
    
    

    然后就可以方便地进行 CRUD 操作:

    MMInfo* info = [[MMInfo alloc]init];
    info.username = @"weixin";
    info.nickname = @"微信";
    info.signature = @"你说我是错的,你最好证明你是对的。";
    info.height = 0.618;
    info.weight = 3.1415926;
    info.age = 3;
    
    // 建DB
    WCDataBase* db = [[WCDataBase alloc] initWithPath:nsDBPath withEncryptKey:nil];
    // 建表
    if ([db createTableOfName:@"mminfo" withClass:MMInfo.class]) {
        WCDataBaseTable* table = [db getTable:@"mminfo" withClass:MMInfo.class];
        // 插入
        [table insertOrUpdateObject:info];
        // 查询
        MMInfo* newInfo = [table getOneObjectWhere:info.db_username == @"weixin"];
        // 删除
        [table deleteObject:newInfo];
    }
    
    

    二、WCDB 进阶

    WCDB 为了便于扩展和使用,引入了一些简单的概念。

    1、可扩展性:表字段、压缩字段和文件字段

    SQLite 使用过程中一个非常常见的问题是:以后加字段怎么办

    • CoreData 通过 schema 升级,部分地解决了这个问题,如果版本只升不降则毫无问题。
    • CBaseDB 则完全不管,一般做法是开发者预留几个字符串字段+几个整型字段。如果预留的都用完了怎么办?那就将所有扩展字段用XML塞到一个字符串字段中。而XML的序列化反序列化毫无意外的低性能。

    WCDB 通过这两个手段解决扩展问题:1)自动扩展列 2)在所有列的后面自动附加一个 Data 列。

    • 所有扩展字段都使用 PBCoding 高效地序列化到这个附加的 Data 列,这些字段就叫做压缩字段
    • 如果预计到某些字段数据量非常大(例如大于1k),还可以选择将他放到小文件中(PBCoding),这些字段称为文件字段
    • 除此之外的字段都叫表字段
    • WCDB 可以自动检测到表字段的增加,并自动增加对应的列。

    表字段的定义是这样的:

    WCDB_OBJ_PROPERTY(WCDBStruct, name, type, uIndex)
    WCDB_BOOL_PROPERTY(WCDBStruct, name, uIndex)
    WCDB_INT32_PROPERTY(WCDBStruct, name, uIndex)
    WCDB_UINT32_PROPERTY(WCDBStruct, name, uIndex)
    WCDB_INT64_PROPERTY(WCDBStruct, name, uIndex)
    WCDB_UINT64_PROPERTY(WCDBStruct, name, uIndex)
    WCDB_FLOAT_PROPERTY(WCDBStruct, name, uIndex)
    WCDB_DOUBLE_PROPERTY(WCDBStruct, name, uIndex)
    
    

    这里的 OBJ 类型包括:NSData、NSDate、NSNumber,以及支持 PBCoding 的结构体。所以说 WCDB 的表达能力甚至超过了 SQLite。如果结构体的代码文件增加一个列到最后,WCDB 在初始化对应的 table 时,会检查 table 的** schema 跟代码描述的是否匹配,不匹配的话会自动 alter-table 增加列**。

    压缩字段的定义是这样的:

    WCDB_PACKED_OBJ_PROPERTY(WCDBStruct, name, type, uIndex)
    WCDB_PACKED_BOOL_PROPERTY(WCDBStruct, name, uIndex)
    WCDB_PACKED_INT32_PROPERTY(WCDBStruct, name, uIndex)
    WCDB_PACKED_UINT32_PROPERTY(WCDBStruct, name, uIndex)
    WCDB_PACKED_INT64_PROPERTY(WCDBStruct, name, uIndex)
    WCDB_PACKED_UINT64_PROPERTY(WCDBStruct, name, uIndex)
    WCDB_PACKED_FLOAT_PROPERTY(WCDBStruct, name, uIndex)
    WCDB_PACKED_DOUBLE_PROPERTY(WCDBStruct, name, uIndex)
    WCDB_PACKED_CONTAINER_PROPERTY(WCDBStruct, name, type, valueType, uIndex)
    WCDB_PACKED_POINT_PROPERTY(WCDBStruct, name, uIndex)
    WCDB_PACKED_SIZE_PROPERTY(WCDBStruct, name, uIndex)
    WCDB_PACKED_RECT_PROPERTY(WCDBStruct, name, uIndex)
    
    

    注:

    1. 这里比表字段多了容器类型、CG 结构体,也就是说跟 PBCoding 支持的类型完全一致。表达能力再次超过了 SQLite。
    2. 这里的 index 需要单调递增,不能重复
    3. 如果非常确定以后不会增加字段,可以指定不用压缩字段:
    WCDB_PACKED_PROPERTY_HOLDER_NO_NEED(WCDBStruct)
    
    

    文件字段的定义是这样的:

    WCDB_FILE_OBJ_PROPERTY(WCDBStruct, name, type, uIndex)
    WCDB_FILE_BOOL_PROPERTY(WCDBStruct, name, uIndex)
    WCDB_FILE_INT32_PROPERTY(WCDBStruct, name, uIndex)
    WCDB_FILE_UINT32_PROPERTY(WCDBStruct, name, uIndex)
    WCDB_FILE_INT64_PROPERTY(WCDBStruct, name, uIndex)
    WCDB_FILE_UINT64_PROPERTY(WCDBStruct, name, uIndex)
    WCDB_FILE_FLOAT_PROPERTY(WCDBStruct, name, uIndex)
    WCDB_FILE_DOUBLE_PROPERTY(WCDBStruct, name, uIndex)
    WCDB_FILE_CONTAINER_PROPERTY(WCDBStruct, name, type, valueType, uIndex)
    WCDB_FILE_POINT_PROPERTY(WCDBStruct, name, uIndex)
    WCDB_FILE_SIZE_PROPERTY(WCDBStruct, name, uIndex)
    WCDB_FILE_RECT_PROPERTY(WCDBStruct, name, uIndex)
    
    

    WCDB 会将一个对象的所有文件字段使用 PBCoding 序列化到一个小文件中。在 select 时会自动从文件中反序列化出这些字段,并赋值到这个对象。

    字段定义的最佳实践

    由于压缩字段和文件字段不支持条件语句,并且目前还不能很方便地迁移到表字段,因此在字段定义时,有几个最佳实践要遵循:

    • 将主键字段、索引字段、用于条件语句的字段、可能会用于条件语句的字段,都定义为表字段。
    • 将非关键字段、肯定不会用于条件语句的字段,定义为压缩字段。(注意,一旦定义为压缩字段,是不能用于条件语句的。)
    • 将超大字段(例如1K以上)定义为文件字段;或者直接剥离到别处存储,不放DB。

    2、主键和索引

    SQLite 性能优化的一大途径是创建 primary key、index和multi-index。在 WCDB 里面的定义如下:

    WCDB_INDEX_BEGIN(WCDBStruct)
        WCDB_CREATE_PRIMARY_KEY(name, order, autoIncrement)
        WCDB_CREATE_INDEX(name, order)
        WCDB_CREATE_MULTI_INDEX(...)
    WCDB_INDEX_END(WCDBStruct)
    
    

    举个例子:

    WCDB_INDEX_BEGIN(CContact)
        WCDB_CREATE_PRIMARY_KEY(m_nsUsrName, WCDB_ORDER_ASC, false)
    WCDB_INDEX_END(CContact)
    
    

    3、条件语句

    程序员大都喜欢裸写SQL语句,然而裸写的 SQL 语句有几个弊端:

    • 没有类型检查,例如字符串比较需要加单引号;
    • 没有非法字符过滤,例如单引号——双引号转换;
    • 字段名称很容易忘记,书写麻烦。

    WCDB 在设计支持就决心要实现一套简单易用、带类型检查的条件语句。
    下面是来自 Mac 版微信的一个真实的例子,比较这两个等价的条件语句:

        nsWhere = [NSString stringWithFormat:@" %s = %u AND %s = %u AND %s != %u AND (%s != %u OR (%s = %u AND %s & %u))"
                         , COL_STATUS, MM_MSGSTATUS_DELIVERED
                         , COL_DES, DES_TO
                         , COL_TYPE, MM_DATA_SYS
                         , COL_TYPE, MM_DATA_VOICEMSG, COL_TYPE, MM_DATA_VOICEMSG, COL_IMG_STATUS, AUDIO_DOWNLOAD_BITSET];
    
        condition = dummy.db_msgStatus == MM_MSGSTATUS_DELIVERED
                            && dummy.db_mesDes == DES_TO
                            && dummy.db_messageType != MM_DATA_SYS
                            && (dummy.db_messageType != MM_DATA_VOICEMSG ||
                                (dummy.db_messageType == MM_DATA_VOICEMSG && dummy.db_msgImgStatus & AUDIO_DOWNLOAD_BITSET));
    
    

    可以看到,裸写的 SQL 查询基本上不具备可读性,需要一个个对比才知道是什么等于什么、什么不等于什么,后续维护的人看到这坨代码肯定会很头疼。而WCDB的条件语句就清醒得多了,什么跟什么作比较,一清二楚,基本上可以一边看代码一边将逻辑口头读出来。

    条件语句的定义

    需要用于条件语句的表字段,可以在头文件这样定义:

    WCDB_OBJ_PROPERTY_DEF(name, type)
    WCDB_CPP_PROPERTY_DEF(name, type)
    
    

    分别对应 ObjC 对象(NSString)和 C/C++ 基础类型。使用时加上** db_xxx 前缀**即可,可以参考上面的例子。

    WCDB 支持基本上所有 SQL 条件操作,包括:

    • 比较运算:> < >= <= == !=
    • 位运算:& | ~
    • 布尔运算:&& ||
    • order by: order(WCDB_ORDER_DESC)、order(WCDB_ORDER_ASC)
    • 集合:in( std::vector<t style="max-width: 100%;"> arr )</t>
    • 字符串: like

    条件语句Place Holders

    为了方便条件语句的书写,WCDB 在定义 WCDB_TABLE 时顺带加了个** dummyObject 的接口,用于获取全局唯一的空实例**。

    // you need a dummy object
    MMInfo* dummy = [MMInfo dummyObject];
    // get one object
    MMInfo* obj = [table getOneObjectWhere:dummy.db_username == @"weixin"];
    
    

    在书写条件语句时 dummy 对象只是一个 place holder,他字段的具体的数值并不会用上。(也就是说随便任何一个对象都可以用在这里,并不是一定要用 dummy 对象)

    4、PBCoding

    WCDBCoding 协议是 PBCoding 协议的超集,也就是说支持 WCDBCoding 的结构体也可以被 PBCoder 序列化到一个文件中。

    三、版本升级

    一般来说结构体不会一成不变,随着业务的升级发展,会有字段增删的情况,WCDB 对此有充分的考虑,做了仔细的分析和支持。

    增加表字段

    WCDB 支持自动增加表字段,你所要做的就是在 WCDB_TABLE 里增加一行表字段的定义。
    具体来说,WCDB 在打开一个 table 时,会检查他的 schema 是否跟代码定义的一致,不一致的话会自动 alter table 增加列。

    增加索引

    WCDB 支持动态增加索引,在 WCDB_INDEX 表里增加需要的项目即可。
    具体来说,WCDB 在打开一个 table 时,会尝试创建一遍 WCDB_INDEX 里定义所有索引。

    增加压缩字段、文件字段

    压缩字段和文件字段低下都是使用 PBCoding 实现,天然支持字段扩展。

    压缩字段移到表字段

    WCDB 目前还不支持将压缩字段移到表字段,也不支持表字段迁移到压缩字段。类似的也不支持文件字段和表字段、压缩字段直接的迁移。

    现有 SQLite DB 迁移到 WCDB

    是的,WCDB 兼容现有的 SQLite DB!不像 CoreData 需要一个数据升级的过程,再也不用担心升级过程出现什么数据丢失的问题。
    考虑到之前定义的 SQLite DB 字段的定义很可能跟结构体的命名不一致,WCDB 提供了字段映射的功能:

    WCDB_OBJ_PROPERTY_EX(WCDBStruct, isSuper, name, originName, type, uIndex)
    WCDB_BOOL_PROPERTY_EX(WCDBStruct, isSuper, name, originName, uIndex)
    WCDB_INT32_PROPERTY_EX(WCDBStruct, isSuper, name, originName, uIndex)
    WCDB_UINT32_PROPERTY_EX(WCDBStruct, isSuper, name, originName, uIndex)
    WCDB_INT64_PROPERTY_EX(WCDBStruct, isSuper, name, originName, uIndex)
    WCDB_UINT64_PROPERTY_EX(WCDBStruct, isSuper, name, originName, uIndex)
    WCDB_FLOAT_PROPERTY_EX(WCDBStruct, isSuper, name, originName, uIndex)
    WCDB_DOUBLE_PROPERTY_EX(WCDBStruct, isSuper, name, originName, uIndex)
    
    

    其中 originName 是该字段在 SQLite DB 里面的定义,name 是结构体代码里的字段命名。
    isSuper 标明这个是父类的字段。(如果该字段不是继承父类的,这个可以不管,设为 NO 即可。)

    四、性能对比

    WCDB 的性能接近直接使用 SQLite C 接口。举个例子,select 5k 个微信联系人(iPhone 5S,iOS8):

    WeChat546664778824b8341ddef9cf0c7b7960.png

    原文链接:https://www.jianshu.com/p/7ef19ac6bb15

    相关文章

      网友评论

          本文标题:iOS 数据库-WCDB

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