iOS FMDB迁移到WCDB

6,845 阅读6分钟

移动端的数据库,除了使用"SQLite"这个共识,基本各自为政。

iOS这边之前使用的是基于SQLite封装的FMDB。一开始使用并无问题。但在长期的使用中反映出,有性能瓶颈,比如说某个用户长期未登录,在登录时收到大量消息,由于FMDB不支持多线程的写操作,会导致写入很慢。

遇到性能瓶颈后我们开始寻找FMDB的替代品,就是WCDB,微信开源官方移动端数据库组件。进入我们的实现。依托微信的用户量和对数据库的依赖,WCDB已经处理了很多坑点和瓶颈,开源1年多,不断地迭代功能,完善文档。同时WCDB在Github的wiki上提供了专门的教程,帮助使用FMDB的开发者进行迁移。

性能对比

对于已经上线运行的项目,解决性能瓶颈会是一个常见的迁移理由。相较于FMDB直白的封装,WCDB上到OC层的ORM,下到SQLite源码,都做了各类性能优化。 为了验证优化效果,微信提供benchmark,并将性能测试结果和测试代码上传到了Github。同时,benchmark中也加入了FMDB的测试代码,用于横向比较。 以下性能测试均为WAL模式、缓存大小2000字节、页大小4 kb:

PRAGMA cache_size=-2000
PRAGMA page_size=4096
PRAGMA journal_mode=WAL

测试数据均为含有一个整型和一个二进制数据的表:CREATE TABLE benchmark(key INTEGER, value BLOB),二进制数据长度为100字节。

  • 读操作性能测试
  • 写操作性能测试
  • 批量写操作性能测试 (事务)
    对于读操作,SQLite速度很快,因此封装层的消耗占比较多。FMDB只做了最简单的封装, 而WCDB还包括ORM、WINQ等操作,因此执行的指令会比FMDB多,从而导致性能劣于FMDB 5%。 而写操作通常是性能的瓶颈,WCDB对其做了许多针对性的优化,使得写操作和批量写操作的性能分别优于FMDB 28% 和 180%。
  • 多线程读并发性能测试
  • 多线程读写并发性能测试
  • 多线程写并发性能测试
    在多线程读操作的测试中,WCDB多线程并发的优势,将读操作的性能劣势拉了回来,使得最终结果与FMDB基本持平,而多线程读写操作性能则优于FMDB 62% 。 在多线程写操作的测试中,FMDB直接返回错误SQLITE_BUSY,无法完成。
  • 初始化性能测试
    SQLite连接的初始化速度会随着数据库内表的数量增加而逐渐上升,WCDB也针对这个场景做了优化。相较于没有优化的FMDB,WCDB 有107% 的性能优势。

平滑迁移

文件格式

由于FMDB和WCDB都基于SQLite,因此两者在数据库的文件格式上一致。用FMDB创建、操作的数据库,可以直接通过WCDB打开、使用。因此开发者无需做额外的数据迁移。

表结构

WCDB提供了ORM的功能,将类的属性绑定到数据库表的字段。在日常实践中,类的属性名和表的字段名通常不一致。因此,WCDB提供了WCDB_SYNTHESIZE_COLUMN(className, propertyName, columnName)宏,用于映射属性名。 对于 表:CREATE TABLE message (db_id INTEGER, db_content TEXT) 类:

//Message.h
@interface Message : NSObject

@property int localID;

@property(retain) NSString *content;

@end

//Message.mm
@implementation Message

@end

这里表字段都加了"db_"的前缀,并且使用了不一样的字段名。通过WCDB的ORM,可以映射为

//Message.h
@interface Message : NSObject <WCTTableCoding>

@property int localID;
@property(retain) NSString *content;
WCDB_PROPERTY(localID)
WCDB_PROPERTY(content)

@end
//Message.mm
@implementation Message

WCDB_IMPLEMENTATION(Message)
WCDB_SYNTHESIZE_COLUMN(Message, localID, "db_id")
WCDB_SYNTHESIZE_COLUMN(Message, content, "db_content")

@end

通过WCDB_SYNTHESIZE_COLUMN宏映射后,WCDB同样能兼容FMDB的表结构,开发者也不需要做数据迁移。由于WCDB较之FMDB性能上有着较大提升,迁移起来由于都是基于SQLite封装,基本上都是兼容的,所以我们决定使用WCDB。

替换前代码

+ (BOOL)insertGroupInfoData:(KitGroupInfoData *)infoData{
    BOOL result;
    //UPDATE
    KitGroupInfoData *groupInfoExit = [KitGroupInfoData getGroupInfoWithGroupId:infoData.groupId];
    if(groupInfoExit){//存在 updateHIYUNTON Group
        NSString *sql = [NSString stringWithFormat:@"UPDATE %@ SET groupName = ?, declared = ?, memberCount = ?,type = ?,isAnonymity = ?,isDiscuss = ? WHERE groupId = '%@' ",DATA_GROUPINFO_DBTABLE,infoData.groupId];
        result = [self updateTable:sql,!KCNSSTRING_ISEMPTY(infoData.groupName)? infoData.groupName:@"",!KCNSSTRING_ISEMPTY(infoData.declared)?infoData.declared:@"",[NSNumber numberWithInteger:infoData.memberCount],[NSNumber numberWithInteger:infoData.type],infoData.isAnonymity?@"1":@"0",infoData.isDiscuss?@"1":@"0"];
        return result;
    }else{//不存在insert
        NSString *sql = [NSString stringWithFormat:@"INSERT INTO %@ %@", DATA_GROUPINFO_DBTABLE, @"(groupId, groupName ,declared, createTime,owner,memberCount,type,isAnonymity,isDiscuss) VALUES (?, ?, ? , ?, ?, ?, ?,?,?)"];
        result = [self updateTable:sql,infoData.groupId, infoData.groupName,infoData.declared,infoData.createTime,infoData.owner,[NSNumber numberWithInteger:infoData.memberCount],[NSNumber numberWithInteger:infoData.type],infoData.isAnonymity?@"1":@"0",infoData.isDiscuss?@"1":@"0"];
        return result;
    }
    return YES;
}

+ (BOOL)upDateGroupInfo:(KitGroupInfoData *)groupInfo{
    __block BOOL result;//UPDATE
    [[[KitDataBaseManager sharedInstance] userDB_Queue] inDatabase:^(FMDatabase *db) {
        [db open];
        NSString *sql = [NSString stringWithFormat:@"UPDATE %@ SET groupName = ?, declared = ?,owner = ? WHERE groupId = '%@' ",DATA_GROUPINFO_DBTABLE,groupInfo.groupId];
        result = [db executeUpdate:sql,!KCNSSTRING_ISEMPTY(groupInfo.groupName)? groupInfo.groupName:@"",!KCNSSTRING_ISEMPTY(groupInfo.declared)?groupInfo.declared:@"",groupInfo.owner];
        [db close];
    }];
    return result;
}

替换后代码

+ (BOOL)insertGroupInfoData:(KitGroupInfoData*)infoData{
    WCTDatabase *dataBase = [DataBaseManager sharedInstance].dataBase;
    return [dataBase insertOrReplaceObject:infoData into:DATA_GROUPINFO_DBTABLE];
}

+ (BOOL)upDateGroupInfo:(KitGroupInfoData *)groupInfo{
    WCTDatabase *dataBase = [DataBaseManager sharedInstance].dataBase;
    return [dataBase updateRowsInTable:DATA_GROUPINFO_DBTABLE onProperties:{KitGroupInfoData.groupName,KitGroupInfoData.declared,KitGroupInfoData.owner} withObject:groupInfo where:KitGroupInfoData.groupId == groupInfo.groupId];
}

总结

在使用了WCDB之后,代码变得更加简洁的同时,性能还得到了提高,也不需要额外关注数据库升级和多线程操作的问题。WCDB还提供了加密、统计、修复等功能供我们使用。在解决性能瓶颈的同时,也解决之前使用FMDB的如下问题:

  1. 胶水代码的问题 过去一个几十行的函数,绝大部分都是拼接SQL、处理SQLite返回的空数据和错误码之类的“裹脚布”代码。而且这种代码四处分布,字里行间都写着"Copy & Paste"。 而现在ORM取出即为对象无需拼接SQL。
  2. 效率问题 SQL基于字符串,命令行爱好者甚喜之。但对于基于现代IDE的移动开发者,却是一大痛。字符串得不到任何编译器的检查,业务开发往往心中一团热火,奋笔疾书下几百行代码,满心欢喜点下Run后才发现:出错了!静心下来逐步看log、断点后才发现,噢,SELECT敲成SLEECT了。改正,再等待编译完成,此时已过去十几分钟,还谈何效率?而现在通过ORM式能够通过IDE来检测是否错误。
  3. SQL注入问题 SQL注入通常是利用SQL字符串拼接的特点,用一些特殊符号提前截断SQL,达到执行其他SQL的目的。试想这么一段代码
    这段封装很简单,就是将消息内容插入到数据库中。假设对方发来这么一条消息:"');DELETE FROM message;--",那么这条SQL就会被截断成三部分:
    它会在插入一条消息后,将表内的所有消息删除。倘若存在这样的漏洞,后果将不堪设想。 其实反注入并不难,通过绑定参数或替换单引号为双单引号即可解决。但要在业务开发的过程时时刻刻警惕这样的风险,并不现实,毕竟人总会犯错的。
  4. 多端同步问题 由于之前没有安卓和iOS端通用的三方库,大家也是各自为政的,使用不同自然会出现各种各样的不同步问题。在接入了WCDB后,我们的数据库方面便会统一

迁移过程中遇到的问题

  1. 工程中有的类是SDK提供的仅有头文件。由于WCDB是基于对象绑定的,所以最后通过创建子类对象绑定WCDB做中间转换实现。
  2. 项目中存了一个用于查询运营商的db文件,只是单纯的从db里查询,同样由于WCDB需要绑定无法实现,由于只是一个小查找,使用位置不多,最后使用了sqlite3原生方法。

参考资料

为什么要从FMDB迁移到WCDB?

github官方wiki