美文网首页IOS知识积累
聊聊移动端跨平台数据库 Realm

聊聊移动端跨平台数据库 Realm

作者: 不是坏人的自来卷 | 来源:发表于2017-11-29 17:59 被阅读81次

    开发杏仁 App 的过程中,我们在相对独立的模块试水了当前非常流行的移动端数据库:Realm,有挑战也有惊喜。下面以 iOS(Object-C) 平台为例,简单介绍下 Realm 的基本使用,并且总结下心得。

    什么是Realm

    Realm

    Realm 是一个针对移动端开发的、跨平台、跨语言数据存储方案。它上手方便,性能强大,功能丰富而且还在不断更新。Realm 在语言上支持 JavaJS.NETSwiftOC,基本覆盖了当前移动端的所有场景。

    目前,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 还是一个不错的选择。但是就目前的版本来说,还存在一些局限性。

    先说说优点:

    1. 简单:光速上手(然后光速踩坑,APP 狂闪不止)。这个确实是小团队福音,不需要学习曲线陡峭的 CoreData,甚至不用写 sql,大家简单阅读下文档,就可以在实际项目中开用了。升级、迁移等都有非常成熟的接口。

    2. 性能优秀:简单看过原理,相比于传统数据库链接 - 查询 - 命中 - 内存拷贝 - 对象序列化的复杂过程,Realm 采用基于内存映射的 Zero-Copy 技术,速度快一个数量级。而且内部采用了类似 git 的对象版本管理机制,并发的性能和安全性也不错。

    3. 线程安全:Realm 拒绝跨线程访问对象,同时,在不同线程中进行增删查改都是绝对安全的。安全的代价是代码上的不便,下面会讲。

    4. 跨平台:iOS 和安卓两边可以共用一套存储方案,Realm 数据库则方便地在不同平台中进行迁移。对于 RN 开发来说,Realm更是数据库方案的首选,真正做到 write once run everywhere。

    5. 响应式:Realm 的查询结果是随数据库变化实时更新的(要求对象在 Run Loop 线程中),配合KVO或者Realm 自带的 ObjectNotification,可以轻松构建即时反映数据变化的响应式 UI,这点和目前主流的响应式框架(ReactNative,ReactiveCocoa)应该是天作之和了。但是要求使用者改变思路,不然会出现很多诡异的 bug。

    6. ORM:虽然 Realm 自己号称是『为移动开发者定制的全功能数据库』,但是其中确实包含 ORM 的很多特性,不用手动写中间层了,取出来就是新鲜活泼的对象,everyone is happy。

    再说说坑:

    1. 无法多线程共享数据库对象:这是 Realm 设计的要求,跨线程访问的话,只能自己重新 query 出来。前面说的上手快,踩坑也快,就是因为这个:异步的 Block、网络接口的回调、从不同线程发出的通知都会触发这个访问异常。ORM 出来的是对象最关键的还是思维方式的转换,虽然是 ORM,但毕竟还是是个数据库,该 query 的地方,还是不要偷懒。

    2. 数据库对象管理:这里有很多坑,比如访问一个被删除的对象时,会直接异常;数据库被 close 后,所有查询出的 object 都无法使用;修改被管理对象属性,必须在指定 block 或者数据库事物集中完成,相当于入库。而 Realm 对象和普通对象是没有任何区别的,所以使用 Realm 的一个重要原则是:不要将数据库对象作为参数传递

    3. 对代码的侵蚀严重:所有的的数据库对象要继承指定的类(没法继承自己的基类了),增删改查,对查询结果处理都有特殊的语法要求,这使得在旧项目中引入 Realm 或者放弃使用将 Realm 从项目中剥离都面临很大的成本。

    4. 静态库大、版本尚未稳定:引入这样一个三方静态库会增加 App 体积,目测大了 1M 至少了。另外 Realm 目前还不是很稳定,之前测试新出的 ObjectNotification 功能,居然会出现偶尔拿不到回调的 bug,相比于成熟的 sqlite 方案,还不是很放心。

    以上介绍了下 Realm Database 的基本情况,主要场景是移动端的本地存储。其实 Realm 还有提供 Realm Platform 产品,提供移动响应式后台解决方案,有兴趣的团队可以了解下。

    总而言之,Realm 还是一个值得尝试的存储方案。如果你追求快速部署、优秀前卫的特性、以及跨平台,Realm 是你的首选;如果你追求稳定,而已有的项目庞大成熟,可以选择暂时观望技术的新进展,谨慎选择。

    相关文章

      网友评论

        本文标题:聊聊移动端跨平台数据库 Realm

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