iOS 数据库-WCDB
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)
注:
- 这里比表字段多了容器类型、CG 结构体,也就是说跟 PBCoding 支持的类型完全一致。表达能力再次超过了 SQLite。
- 这里的 index 需要单调递增,不能重复。
- 如果非常确定以后不会增加字段,可以指定不用压缩字段:
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