开发杏仁 App 的过程中,我们在相对独立的模块试水了当前非常流行的移动端数据库:Realm
,有挑战也有惊喜。下面以 iOS(Object-C) 平台为例,简单介绍下 Realm 的基本使用,并且总结下心得。
什么是Realm
RealmRealm 是一个针对移动端开发的、跨平台、跨语言数据存储方案。它上手方便,性能强大,功能丰富而且还在不断更新。Realm 在语言上支持 Java
、 JS
、 .NET
、 Swift
、 OC
,基本覆盖了当前移动端的所有场景。
目前,Realm 已经完全开源,并且有很多三方的插件可以使用,生态已经相对比较成熟了。
配置
Realm 的配置比较简单,升级和数据迁移都很直观,不过需要注意:
- 每次对数据库表有更新都必须手动增加版本号,不然会闪退。
- 升级表(增加、删除字段或表)不需要手写迁移代码;如果有数据迁移、修改字段名、合并字段、数据升级等高级操作,则在 block 中写相关代码,具体可参照文档。
- 一般来说要写全递归升级的所有版本分支,做好每个版本的清理工作,以免发生意外(比如版本2比版本1删除了字段A,版本3又添加回来,用户如果直接从版本1升级到版本3,则会有脏数据)。
启动Realm的基本流程:
// 1.配置数据库文件地址
NSString *filePath = path;
RLMRealmConfiguration *config = [RLMRealmConfiguration defaultConfiguration];
config.fileURL = [[[NSURL URLWithString:filePath] URLByAppendingPathComponent:@"YF"] URLByAppendingPathExtension:@"realm"];
// 2. 加密
config.encryptionKey = [self getKey];
// 3. 设置版本号(每次发布都应该增加版本号)
config.schemaVersion = 1;
// 4. 数据迁移
config.migrationBlock = ^(RLMMigration *migration, uint64_t oldSchemaVersion) {
if (oldSchemaVersion < 1) {
// do something
}
};
// 5. 设置配置选项
[RLMRealmConfiguration setDefaultConfiguration:config];
// 6. 启动数据库,完成相关的配置
[RLMRealm defaultRealm];
建表
直接继承 RLMObject
的类将会自动创建一张表,表项为这个类的属性。需要注意几点:
- Number,BOOL 等类型要用 Realm 指定的格式。
- Array 要用 RLMArray 容器,来表示一对多的关系。
- 没有入库前,可以像正常 Object 一样操作;但是入库后或者是 query 出来的对象,则要按照 Realm 的规范处理。这点是刚上手时需要特别注意的。
一个典型的 Realm 对象:
@interface YFRecommendHistoryObject : RLMObject
@property (nonatomic) NSNumber<RLMInt> *recommendId;
@property (nonatomic) NSNumber<RLMInt> *patientId;
@property (nonatomic) NSString *patientName;
@property (nonatomic) NSNumber<RLMInt> *created;
@property (nonatomic) NSString *createdTimeString; // yyyy-MM-dd hh:mm
@property (nonatomic) NSNumber<RLMInt> *count;
@property (nonatomic) NSNumber<RLMInt> *totalPrice; ///< 总价,分
@property (nonatomic) NSString *totalPriceString; /// ¥xxxx.xx
@property (nonatomic) RLMArray<YFRecommendDrugItem> *items;
@end
所有被 Realm 管理的对象都是线程不安全的,绝对不可以跨线程访问对象,也不要将 Realm 对象作为参数传递。推荐的做法是每个线程甚至是每次需要访问对象的时候都重新 query。
PS:最新版本的 Realm 提供了一些跨线程传递对象的途径(
RLMThreadSafeReference
)。
增删查改
入库:
// 创建对象
Person *author = [[Person alloc] init];
author.name = @"David Foster Wallace";
// 获取到Realm对象
RLMRealm *realm = [RLMRealm defaultRealm];
// 入库
[realm beginWriteTransaction];
[realm addObject:author];
[realm commitWriteTransaction];
另外对一对一关系或者一对多关系的赋值也相当于入库。
对被管理对象的所有赋值,删除等操作都必须放到 begin-end 对当中,否则就是非法操作,直接闪退。
// 删除对象
[realm beginWriteTransaction];
[realm deleteObject:cheeseBook];
[realm commitWriteTransaction];
需要注意的是如果一个对象从数据库中被删除了,那么它就是非法对象了,对其的任何访问都会导致异常。
查询的话语法和 NSPredicate
类似。
// 使用 query string 来查询
RLMResults<Dog *> *tanDogs = [Dog objectsWhere:@"color = 'tan' AND name BEGINSWITH 'B'"];
// 使用 Cocoa 的 NSPredicate 对象来查询
NSPredicate *pred = [NSPredicate predicateWithFormat:@"color = %@ AND name BEGINSWITH %@",
@"tan", @"B"];
tanDogs = [Dog objectsWithPredicate:pred];
通知
不要手动去管理数据库对象更新的通知,Realm 会自动在 Runloop 中同步各个线程的数据,但是同步的时机是无法预料的,应该使用 Realm 框架自带的通知系统。
对集合对象的通知:
self.notificationToken = [[Person objectsWhere:@"age > 5"] addNotificationBlock:^(RLMResults<Person *> *results, RLMCollectionChange *changes, NSError *error) {
if (error) {
NSLog(@"Failed to open Realm on background worker: %@", error);
return;
}
UITableView *tableView = weakSelf.tableView;
// 在回调中更新相关UI
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:[changes deletionsInSection:0]
withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView insertRowsAtIndexPaths:[changes insertionsInSection:0]
withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView reloadRowsAtIndexPaths:[changes modificationsInSection:0]
withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView endUpdates];
}];
Realm 在 OC 中实现响应式主要靠这种方式。在其它语言平台上(JS,JAVA,Swift),Realm 对响应式编程的支持要更好一些。
对普通 object 的通知:
RLMStepCounter *counter = [[RLMStepCounter alloc] init];
counter.steps = 0;
RLMRealm *realm = [RLMRealm defaultRealm];
[realm beginWriteTransaction];
[realm addObject:counter];
[realm commitWriteTransaction];
__block RLMNotificationToken *token = [counter addNotificationBlock:^(BOOL deleted,
NSArray<RLMPropertyChange *> *changes,
NSError *error) {
if (deleted) {
NSLog(@"The object was deleted.");
} else if (error) {
NSLog(@"An error occurred: %@", error);
} else {
for (RLMPropertyChange *property in changes) {
if ([property.name isEqualToString:@"steps"] && [property.value integerValue] > 1000) {
NSLog(@"Congratulations, you've exceeded 1000 steps.");
[token stop];
token = nil;
}
}
}
}];
关于通知的使用,有两点需要注意:
- 必须保持对 token 的引用,token 被释放,则通知失效(在被释放前最好按规范 stop 掉 token)。
- object 通知并不监听得到 object 的 RLMArray 等集合属性成员的变化,需要另外处理。
一些总结
Realm 真的好用吗?相对于陈旧的 sqlite 和学习曲线陡峭的 CoreData,Realm 还是一个不错的选择。但是就目前的版本来说,还存在一些局限性。
先说说优点:
-
简单:光速上手(然后光速踩坑,APP 狂闪不止)。这个确实是小团队福音,不需要学习曲线陡峭的 CoreData,甚至不用写 sql,大家简单阅读下文档,就可以在实际项目中开用了。升级、迁移等都有非常成熟的接口。
-
性能优秀:简单看过原理,相比于传统数据库链接 - 查询 - 命中 - 内存拷贝 - 对象序列化的复杂过程,Realm 采用基于内存映射的
Zero-Copy
技术,速度快一个数量级。而且内部采用了类似 git 的对象版本管理机制,并发的性能和安全性也不错。 -
线程安全:Realm 拒绝跨线程访问对象,同时,在不同线程中进行增删查改都是绝对安全的。安全的代价是代码上的不便,下面会讲。
-
跨平台:iOS 和安卓两边可以共用一套存储方案,Realm 数据库则方便地在不同平台中进行迁移。对于 RN 开发来说,Realm更是数据库方案的首选,真正做到 write once run everywhere。
-
响应式:Realm 的查询结果是随数据库变化实时更新的(要求对象在 Run Loop 线程中),配合KVO或者Realm 自带的 ObjectNotification,可以轻松构建即时反映数据变化的响应式 UI,这点和目前主流的响应式框架(ReactNative,ReactiveCocoa)应该是天作之和了。但是要求使用者改变思路,不然会出现很多诡异的 bug。
-
ORM:虽然 Realm 自己号称是『为移动开发者定制的全功能数据库』,但是其中确实包含 ORM 的很多特性,不用手动写中间层了,取出来就是新鲜活泼的对象,everyone is happy。
再说说坑:
-
无法多线程共享数据库对象:这是 Realm 设计的要求,跨线程访问的话,只能自己重新 query 出来。前面说的上手快,踩坑也快,就是因为这个:异步的 Block、网络接口的回调、从不同线程发出的通知都会触发这个访问异常。ORM 出来的是对象最关键的还是思维方式的转换,虽然是 ORM,但毕竟还是是个数据库,该 query 的地方,还是不要偷懒。
-
数据库对象管理:这里有很多坑,比如访问一个被删除的对象时,会直接异常;数据库被 close 后,所有查询出的 object 都无法使用;修改被管理对象属性,必须在指定 block 或者数据库事物集中完成,相当于入库。而 Realm 对象和普通对象是没有任何区别的,所以使用 Realm 的一个重要原则是:不要将数据库对象作为参数传递。
-
对代码的侵蚀严重:所有的的数据库对象要继承指定的类(没法继承自己的基类了),增删改查,对查询结果处理都有特殊的语法要求,这使得在旧项目中引入 Realm 或者放弃使用将 Realm 从项目中剥离都面临很大的成本。
-
静态库大、版本尚未稳定:引入这样一个三方静态库会增加 App 体积,目测大了 1M 至少了。另外 Realm 目前还不是很稳定,之前测试新出的 ObjectNotification 功能,居然会出现偶尔拿不到回调的 bug,相比于成熟的 sqlite 方案,还不是很放心。
以上介绍了下 Realm Database 的基本情况,主要场景是移动端的本地存储。其实 Realm 还有提供 Realm Platform 产品,提供移动响应式后台解决方案,有兴趣的团队可以了解下。
总而言之,Realm 还是一个值得尝试的存储方案。如果你追求快速部署、优秀前卫的特性、以及跨平台,Realm 是你的首选;如果你追求稳定,而已有的项目庞大成熟,可以选择暂时观望技术的新进展,谨慎选择。
网友评论