Realm 杂谈 Swift 版

Realm 在五月终于发布了1.0正式版。不知道是不是见证了一个历史性的时刻,毕竟 Realm 是号称用来取代 SQLite 而诞生的。话不多说,下面就来揭开她的面纱,说一说我是怎么和她愉快的玩耍。

故事的背景要说到,最近开始的一个新项目。项目选定使用 Swift 开发,所以需要在前期的时候制定相关的技术方案。数据存储是老生常谈的模块,IOS 开发无非 SQLIte 和 Core Data 可选。Realm 在之前就有听说过,但一直没有静下心来研究。这次经过一番研究后,觉得甚是妙哉。再考虑到她对 Android 平台也有不俗的表现,以后业务逻辑方便迁移。故最终选定 Realm。

先附上 Realm Swift 中文文档地址:https://realm.io/cn/docs/swift/latest/ 内容相当详细。当然这都是基础用法,更高级的用法还是参考官方例子吧。以下是我封装的 RealmHelper 类:

class RealmHelper {

    /// realm 数据库的名称
 private let username = "MY-DB"
 
    static let sharedInstance = try! Realm()
    
//--MARK: 初始化 Realm
    /// 初始化进过加密的 Realm, 加密过的 Realm 只会带来很少的额外资源占用(通常最多只会比平常慢10%)
    static func initEncryptionRealm() {
        // 说明: 以下内容是可以合并操作的,但为了能最大限度的展示各个操作内容,故分开设置 Realm
        
        // 产生随机密钥
        let key = NSMutableData(length: 64)!
        SecRandomCopyBytes(kSecRandomDefault, key.length,
                           UnsafeMutablePointer<UInt8>(key.mutableBytes))
        
        // 获取加密 Realm 文件的配置文件
        var config = Realm.Configuration(encryptionKey: key)
        
        // 使用默认的目录,但是使用用户名来替换默认的文件名
        config.fileURL = config.fileURL!.URLByDeletingLastPathComponent?
            .URLByAppendingPathComponent("\(username).realm")
        
        // 获取我们的 Realm 文件的父级目录
        let folderPath = config.fileURL!.URLByDeletingLastPathComponent!.path!
        
        /**
         *  设置可以在后台应用刷新中使用 Realm
         *  注意:以下的操作其实是关闭了 Realm 文件的 NSFileProtection 属性加密功能,将文件保护属性降级为一个不太严格的、允许即使在设备锁定时都可以访问文件的属性
         */
        // 解除这个目录的保护
        try! NSFileManager.defaultManager().setAttributes([NSFileProtectionKey: NSFileProtectionNone],
                                                          ofItemAtPath: folderPath)
        
        // 将这个配置应用到默认的 Realm 数据库当中
        Realm.Configuration.defaultConfiguration = config
        
    }
    
    /// 初始化默认的 Realm
    static func initRealm() {
        var config = Realm.Configuration()
        
        // 使用默认的目录,但是使用用户名来替换默认的文件名
        config.fileURL = config.fileURL!.URLByDeletingLastPathComponent?
            .URLByAppendingPathComponent("\(username).realm")
        
        // 获取我们的 Realm 文件的父级目录
        let folderPath = config.fileURL!.URLByDeletingLastPathComponent!.path!
        
        // 解除这个目录的保护
        try! NSFileManager.defaultManager().setAttributes([NSFileProtectionKey: NSFileProtectionNone],
                                                          ofItemAtPath: folderPath)
        
        // 将这个配置应用到默认的 Realm 数据库当中
        Realm.Configuration.defaultConfiguration = config
    }

//--- MARK: 操作 Realm
    /// 做写入操作
    static func doWriteHandler(clouse: ()->()) { // 这里用到了 Trailing 闭包
        try! sharedInstance.write {
            clouse()
        }
    }
    
    /// 添加一条数据
    static func addCanUpdate<T: Object>(object: T) {
        try! sharedInstance.write {
            sharedInstance.add(object, update: true)
        }
    }
    static func add<T: Object>(object: T) {
        try! sharedInstance.write {
            sharedInstance.add(object)
        }
    }
    /// 后台单独进程写入一组数据
    static func addListDataAsync<T: Object>(objects: [T]) {
        let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
        // Import many items in a background thread
        dispatch_async(queue) {
            // 为什么添加下面的关键字,参见 Realm 文件删除的的注释
            autoreleasepool {
                // 在这个线程中获取 Realm 和表实例
                let realm = try! Realm()
                // 批量写入操作
                realm.beginWrite()
                // add 方法支持 update ,item 的对象必须有主键
                for item in objects {
                    realm.add(item, update: true)
                }
                // 提交写入事务以确保数据在其他线程可用
                try! realm.commitWrite()
            }
        }
    }
    
    static func addListData<T: Object>(objects: [T]) {
        autoreleasepool {
            // 在这个线程中获取 Realm 和表实例
            let realm = try! Realm()
            // 批量写入操作
            realm.beginWrite()
            // add 方法支持 update ,item 的对象必须有主键
            for item in objects {
                realm.add(item, update: true)
            }
            // 提交写入事务以确保数据在其他线程可用
            try! realm.commitWrite()
        }
    }
    
    /// 删除某个数据
    static func delete<T: Object>(object: T) {
        try! sharedInstance.write {
            sharedInstance.delete(object)
        }
    }
    
    /// 批量删除数据
    static func delete<T: Object>(objects: [T]) {
        try! sharedInstance.write {
            sharedInstance.delete(objects)
        }
    }
    /// 批量删除数据
    static func delete<T: Object>(objects: List<T>) {
        try! sharedInstance.write {
            sharedInstance.delete(objects)
        }
    }
    /// 批量删除数据
    static func delete<T: Object>(objects: Results<T>) {
        try! sharedInstance.write {
            sharedInstance.delete(objects)
        }
    }
    
    /// 批量删除数据
    static func delete<T: Object>(objects: LinkingObjects<T>) {
        try! sharedInstance.write {
            sharedInstance.delete(objects)
        }
    }
    
    
    /// 删除所有数据。注意,Realm 文件的大小不会被改变,因为它会保留空间以供日后快速存储数据
    static func deleteAll() {
        try! sharedInstance.write {
            sharedInstance.deleteAll()
        }
    }
    
    /// 根据条件查询数据
    static func selectByNSPredicate<T: Object>(_: T.Type , predicate: NSPredicate) -> Results<T>{
        return sharedInstance.objects(T).filter(predicate)
    }
    
//--- MARK: 删除 Realm
    /*
     参考官方文档,所有 fileURL 指向想要删除的 Realm 文件的 Realm 实例,都必须要在删除操作执行前被释放掉。
     故在操作 Realm实例的时候需要加上 autoleasepool 。如下:
        autoreleasepool {
            //所有 Realm 的使用操作
        }
     */
    /// Realm 文件删除操作
    static func deleteRealmFile() {
        let realmURL = Realm.Configuration.defaultConfiguration.fileURL!
        let realmURLs = [
            realmURL,
            realmURL.URLByAppendingPathExtension("lock"),
            realmURL.URLByAppendingPathExtension("log_a"),
            realmURL.URLByAppendingPathExtension("log_b"),
            realmURL.URLByAppendingPathExtension("note")
        ]
        let manager = NSFileManager.defaultManager()
        for URL in realmURLs {
            do {
                try manager.removeItemAtURL(URL)
            } catch {
                // 处理错误
            }
        }
        
    }
}

注释写的很清楚,主要包括初始化 Realm 和其相关的操作。 所以就不赘述封装类,而是讲解一些 Realm 使用中我觉得有意思的地方。

有趣的 Realm

首先来一个例子:

class Dog: Object {
 let friends = List<Dog>
 let owners = LinkingObjects(fromType: Person.self, property: "dogs")
}
class Person: Object {
 let dogs = List<Dog>()
 override static func primaryKey() -> String? {
    return "id"
  }
}
  • 反向关系 -- 关键字 LinkingObjects。在 M-N 的关系中,我们虽然可以在两个数据模型中都使用 List<T> 来双向绑定。但由于手动同步关系会很容易出错,并且还会让内容变得复杂、冗余。反向关系就解决了这个问题。
  • 内容更新 -- add VS create。虽然他们都有更新的作用,既如果主键 id 为1的 Person 对象已经存在于数据库当中了,那么对象就会简单地进行更新。而如果不在数据库中存在的话,那么这个操作将会创建一个新的 Person 对象并添加到数据库当中。那该怎么选择他们呢?create 通过传递想要更新值的集合,从而更新带有主键的某个对象的部分值;add 更新的是对象,在更新的时候,向上的关系会保留,因为向上的关系是通过主键id联系在一起的。向下的关系,既对象内部List属性,如果不重新设置,之前的关系会丢失。这里需要十分小心。
  • 内容删除。会删除向上的关系。但不会将向下关系的对象一起被清除。所以,在删除的时候,需要考虑其向下的关系是否还有存在的意义,如果没有,应一并删除其向下关系的对象。例如删除一个 Dog 对象,会清除该对象向上的 owners 关系,但不会清除 friends 对象。如果其 friends 对象在该 Dog 对象被删除后,就没有存在的必要。则应该一并删除 friends 连接的 Dog 对象。
  • 通知的理解。官方文档的例子很简单也很清楚。在此我只说说不同的见解。通过 addNotificationBlock 可以轻松的监察某个 Results<T> 实例的变化。这样就可以监察某一个特定的查询结果(Realm自动更新的原则)。并对这个查询结果做出相应的处理。我们完全可以通过这种方式将每一个 view 都绑定上 Results,这样就看起十分像 Android DataBinding。但这是不是一个好的设计方案呢?我想 Realm 不一定会这样推荐使用,即便她的查询效率很高。Realm 本事是支持自动更新的,当其他地方更新数据后,之前的对象也会被动态更新。通知只是告知 UI 在什么时候响应更新,所有的修改 Realm 都为我们做完了。
  • 通过 Person 查看 dogs 和 Dog 的 friends 非常简单,就像点出来一个 public 方法和属性一样简单。当然通过 Dog 的 owners 也可以向上追溯 Person。建议在建好数据模型后,打印出整个关系树来查看。会对理解 Realm 的模型关系有很大帮助。

总结

Realm 是一个轻量级的关系型数据库。她和 SQLite 比起来还要轻。SQLite 需要通过 SQL 语句来操作数据库,虽然也可以通过一些第三方控件封装,使得上层不再关心 SQL 语句的编写。但对比起 Realm 天生已经将这繁琐的工作帮我们封装过,只给我提供较为直观的关系树(关系树是我根据打印出来的数据,自己理解的出来的一个词),让使用方法更符合上层逻辑的编写习惯。我相信如果用的得当,她会大大减轻我们对数据库开发的成本。

对比

Swift 版本的 Realm 更加智能化一些。我们在构建完对象之间的关系后,只需要将顶层的对象更新到 Realm 中,其子内容就会自动保存。就像上面的例子,Person 和 Dog 实例的关系构建完后,只需要保存 Person 实例就能完成所有的内容的持久化。然而,在 Android 中我们则需要分别持久化这些实例,既建立完关系后,需要分别取保存这些实例。(2017-1-20 新增)
这里需要更正一下,Android版本中,其实也只需要保存构建过关系树的顶层对象即可将所有数据持久化到Realm。例如,Person 和 Dog 的对象关系树构建成功后,只需要持久化 Person 对象即可。(2017-2-6 修改)

说明

本着分享加记录学习心得的严谨态度,最后还是要声明一下。以上内容皆为阅读官方文档和项目开发中对 Realm 的理解和感悟,不一定是正确的。如果有误导的地方,请联系我。我会在第一时间修正。当然,它也会帮助你从不同的角度理解 Realm,分享的乐趣就是让更多的人看到你的态度。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容