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,分享的樂趣就是讓更多的人看到你的態度。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章