iOS 数据库-WCDB

2022-10-20  本文已影响0人  小和大大

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

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

SQLite 简介

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

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

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

一、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 使用过程中一个非常常见的问题是:以后加字段怎么办

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

表字段的定义是这样的:

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 时会自动从文件中反序列化出这些字段,并赋值到这个对象。

字段定义的最佳实践

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

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 条件操作,包括:

条件语句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

上一篇下一篇

猜你喜欢

热点阅读