Realm-Swift使用入門

Realm適用iOS和Android平臺,自己相比sqlite、CoreData操做簡單,在這裏記錄下使用方式; (Swift4.2)sql

安裝

pod 'RealmSwift'數據庫

基礎使用

打開 Realm 數據庫

要打開一個 Realm 數據庫,首先須要初始化一個新的 Realm 對象:編程

let realm = try! Realm()
try! realm.write {
    realm.add(myDog)
}
複製代碼

這將會初始化出一個默認 Realm 數據庫。安全

配置 Realm 數據庫

  • 可配置本地 Realm 數據庫在磁盤上的路徑;
  • 對於可同步 Realm 數據庫而言,能夠配置管理用戶,以及服務器上的遠程路徑;
  • 配置版本遷移
  • 壓縮功能,高效地利用磁盤空間。
func setDefaultRealmForUser(username: String) {
	var config = Realm.Configuration()
	// 使用默認的目錄,可是請將文件名替換爲用戶名
	config.fileURL = config.fileURL!.deletingLastPathComponent().appendingPathComponent("\(username).realm")
	// 將該配置設置爲默認 Realm 配置
	Realm.Configuration.defaultConfiguration = config
}
複製代碼

操做 Realm 對象

對象的自更新

Object 實例是底層數據的動態體現,會自動進行更新;所以這意味着無需去刷新對象的當前狀態。修改某個對象的屬性,會當即影響到全部指向該對象的其餘實例。服務器

let myDog = Dog()
myDog.name = "Fido"
myDog.age = 1
try! realm.write {
	realm.add(myDog)
}
let myPuppy = realm.objects(Dog.self).filter("age == 1").first
try! realm.write {
	myPuppy!.age = 2
}
print("age of my dog: \(myDog.age)") // => 2
複製代碼

這不只使得 Realm 保證高速和高效,同時還讓代碼更爲簡潔、更爲靈活。若是您的 UI 代碼基於某個特定的 Realm 對象來實現,那麼在觸發 UI 重繪之前,您根本無需進行數據刷新或者從新檢索。架構

對象存儲

對象的全部更改(添加、修改和刪除)都必須在寫入事務內完成。app

Realm 對象能夠被實例化,還可做爲未管理對象使用(例如,還未添加到 Realm 數據庫),而且使用方式與其它正常 Swift 對象無異。然而,若是要在線程之間共享對象,或者在應用啓動後反覆使用,那麼您必須將這些對象添加到 Realm 數據庫中。向 Realm 數據庫中添加對象必須在寫入事務內完成。因爲寫入事務將會產生沒法忽略的性能消耗,所以您應當檢視您的代碼,以確保儘量減小寫入事務的數量。異步

warning:Realm 的寫入操做是同步以及阻塞進行的,它並不會異步執行。若是線程 A 開始進行寫入操做,而後線程 B 在線程 A 結束以前,對相同的 Realm 數據庫也執行了寫入操做,那麼線程 A 必需要在線程 B 的寫入操做發生以前,結束並提交其事務。寫入事務會在 beginWrite() 執行時自動刷新,所以重複寫入並不會產生競爭條件。oop

更新對象

Realm 提供了一系列更新對象的方法,根據使用場景的不一樣, 每一個方法都有各自的優缺點。性能

直接更新

您能夠在寫入事務中,經過設置對象的屬性從而完成更新。

// 在事務中更新對象
try! realm.write {
    author.name = "Thomas Pynchon"
}複製代碼

鍵值編碼

Object、Result 和 List 均容許使用 鍵值編碼(KVC)。 當您須要在運行時決定何種屬性須要進行更新的時候, 這個方法就很是有用了。 批量更新對象時,爲集合實現 KVC 是一個很好的作法, 這樣就不用承受遍歷集合時爲每一個項目建立訪問器 所帶來的性能損耗。

let persons = realm.objects(Person.self)
try! realm.write {
    persons.first?.setValue(true, forKeyPath: "isFirst")
    // 將每一個 person 對象的 planet 屬性設置爲 "Earth"
    persons.setValue("Earth", forKeyPath: "planet")
}複製代碼

經過主鍵更新

若是數據模型類中包含了主鍵,那麼 可使用 Realm().add(_:update:),從而讓 Realm 基於主鍵來自動更新或者添加對象。

// 建立一個 book 對象,其主鍵與以前存儲的 book 對象相同
let cheeseBook = Book()
cheeseBook.title = "Cheese recipes"
cheeseBook.price = 9000
cheeseBook.id = 1
// 更新這個 id = 1 的 book
try! realm.write {
    realm.add(cheeseBook, update: true)
}複製代碼

若是這個主鍵值爲 「1」 的 Book 對象已經存在於數據庫當中 ,那麼該對象只會進行更新。若是不存在的話, 那麼一個全新的 Book 對象就會被建立出來,並被添加到數據庫當中。

您能夠經過傳遞一個子集,其中只包含打算更新的值, 從而對帶有主鍵的對象進行部分更新:

// 假設主鍵爲 `1` 的 "Book" 對象已經存在
try! realm.write {
    realm.create(Book.self, value: ["id": 1, "price": 9000.0], update: true)
    // book 對象的 `title` 屬性仍舊保持不變
}複製代碼

若是沒有定義主鍵,那麼最好不要對這類對象傳遞 update: true 參數。

請注意,對於可空屬性 而言, 在更新對象的時候,nil 仍會被視爲有效值。若是您提供了一個屬性值存在 nil 的字典,那麼這個設定會被應用到應用當中,而且這些屬性值也會被清空。 爲了確保不會出現意外的數據丟失, 在使用此方法以前請再三確認, 只提供了想要進行更新的屬性值。

刪除對象

在寫入事務中,將要刪除的對象傳遞給 Realm().delete(_:) 方法。

// cheeseBook 存儲在 Realm 數據庫中
// 在事務中刪除對象
try! realm.write {
    realm.delete(cheeseBook)
}
複製代碼

您一樣也能夠刪除存儲在 Realm 數據庫當中的全部數據。請注意,Realm 文件會保留在磁盤上所佔用的空間,從而爲之後的對象預留足夠的空間,從而實現快速存儲。

// 從 Realm 數據庫中刪除全部對象
try! realm.write {
    realm.deleteAll()
}複製代碼

查詢

查詢將會返回一個 Results 實例,其中包含了一組 Object 對象。Results 的接口與 Array 基本相同,而且可使用索引下標來訪問包含在 Results 當中的對象。與 Array 所不一樣的是,Results 只能持有一個 Object 子類類型。

全部的查詢操做(包括檢索和屬性訪問)在 Realm 中都是延遲加載的。只有當屬性被訪問時,數據纔會被讀取。

查詢結果並非數據的拷貝:(在寫入事務中)修改查詢結果會直接修改磁盤上的數據。與之相似,您能夠從 Results 當中的 Object 來直接遍歷關係圖。

除非對結果進行了訪問,不然查詢的執行將會被推遲(Lazy)。這意味着 將多個臨時 Results 關聯在一塊兒,而後對數據進行排序和條件檢索的操做, 並不會執行中間狀態處理之類的額外工做。

一旦執行了查詢,或者添加了通知模塊, 那麼 Results 將時刻與 Realm 數據庫當中的數據保持一致, 若有可能,會在後臺線程中執行再一次查詢操做。

從 Realm 數據庫中檢索對象的最基本方法是 Realm().objects(_:),這個方法將會返回 Object 子類類型在默認 Realm 數據庫當中的查詢到的全部數據,並以 Results 實例的形式返回。

let dogs = realm.objects(Dog.self) // 從默認的 Realm 數據庫中遍歷全部 Dog 對象複製代碼

條件查詢

若是您對 NSPredicate 有所瞭解的話,那麼您就已經掌握了在 Realm 中進行查詢的方法了。Objects、Realm、List 和 Results 均提供了相關的方法,從而只需傳遞 NSPredicate 實例、斷言字符串、或者斷言格式化字符串來查詢特定的 Object 實例,這與對 NSArray 進行查詢所相似。

例如,下面這個例子經過調用 Results().filter(_:...) 方法,從默認 Realm 數據庫中遍歷出全部棕黃色、名字以 「B」 開頭的狗狗:

// 使用斷言字符串來查詢
var tanDogs = realm.objects(Dog.self).filter("color = 'tan' AND name BEGINSWITH 'B'")

// 使用 NSPredicate 來查詢
let predicate = NSPredicate(format: "color = %@ AND name BEGINSWITH %@", "tan", "B")
tanDogs = realm.objects(Dog.self).filter(predicate)
複製代碼

參見 Apple 的斷言編程指南來獲取更多關於構建斷言的信息,此外還可使用咱們的 NSPredicate Cheatsheet。Realm 支持大多數常見的斷言:

  • 比較操做數能夠是屬性名,也能夠是常量。但至少要有一個操做數是屬性名;
  • 比較操做符 ==、<=、<、>=、>、!= 和 BETWEEN 支持 Int、Int八、Int1六、Int3二、Int6四、Float、Double 以及 Date 這幾種屬性類型,例如 age == 45;
  • 比較是否相同:== 和 !=,例如,Results().filter("company == %@", company);
  • 比較操做符 == 和 != 支持布爾屬性;
  • 對於 String 和 Data 屬性而言,支持使用 ==、!=、BEGINSWITH、CONTAINS 和 ENDSWITH 操做符,例如 name CONTAINS 'Ja';
  • 對於 String 屬性而言,LIKE 操做符能夠用來比較左端屬性和右端表達式:? 和 * 可用做通配符,其中 ? 能夠匹配任意一個字符,* 匹配 0 個及其以上的字符。例如:value LIKE '?bc*' 能夠匹配到諸如 「abcde」 和 「cbc」 之類的字符串;
  • 字符串的比較忽略大小寫,例如 name CONTAINS[c] 'Ja'。請注意,只有 「A-Z」 和 「a-z」 之間的字符大小寫會被忽略。[c] 修飾符能夠與 [d] 修飾符結合使用;
  • 字符串的比較忽略變音符號,例如 name BEGINSWITH[d] 'e' 可以匹配到 étoile。這個修飾符能夠與 [c] 修飾符結合使用。(這個修飾符只可以用於 Realm 所支持的字符串子集:參見當前的限制一節來了解詳細信息。)
  • Realm 支持如下組合操做符:「AND」、「OR」 和 「NOT」,例如 name BEGINSWITH 'J' AND age >= 32;
  • 包含操做符:IN,例如 name IN {'Lisa', 'Spike', 'Hachi'};
  • 空值比較:==、!=,例如 Results().filter("ceo == nil")。請注意,Realm 將 nil 視爲一種特殊值,而不是某種缺失值;這與 SQL 不一樣,nil 等同於自身;
  • ANY 比較,例如 ANY student.age < 21;
  • List 和 Results 屬性支持彙集表達式:@count、@min、@max、@sum 和 @avg,例如 realm.objects(Company.self).filter("employees.@count > 5") 可用以檢索全部擁有 5 名以上僱員的公司。
  • 支持子查詢,不過存在如下限制:
    • @count 是惟一一個能在 SUBQUERY 表達式當中使用的操做符;
    • SUBQUERY(…).@count 表達式只能與常量相比較;
    • 目前仍不支持關聯子查詢。

參見 Results().filter(_:...)

排序

Results 容許您指定一個排序標準,而後基於關鍵路徑、屬性或者多個排序描述符來進行排序。例如,下列代碼讓上述示例中返回的 Dog 對象按名字進行升序排序:

// 對顏色爲棕黃色、名字以 "B" 開頭的狗狗進行排序
let sortedDogs = realm.objects(Dog.self).filter("color = 'tan' AND name BEGINSWITH 'B'").sorted(byKeyPath: "name")
複製代碼

關鍵路徑一樣也能夠是某個多對一關係屬性。

class Person: Object {
    @objc dynamic var name = ""
    @objc dynamic var dog: Dog?
}
class Dog: Object {
    @objc dynamic var name = ""
    @objc dynamic var age = 0
}

let dogOwners = realm.objects(Person.self)
let ownersByDogAge = dogOwners.sorted(byKeyPath: "dog.age")
複製代碼

請注意,sorted(byKeyPath:) 和 sorted(byProperty:) 不支持 將多個屬性用做排序基準,此外也沒法鏈式排序(只有最後一個 sorted 調用會被使用)。 若是要對多個屬性進行排序,請使用 sorted(by:)方法,而後向其中輸入多個 SortDescriptor 對象。

欲瞭解更多信息,參見:

  • Results().sorted(_:)
  • Results().sorted(byKeyPath:ascending:)

注意,在對查詢進行排序的時候,只能保證 Results 的次序不變。 出於性能考量,插入次序將沒法保證。 若是您但願維護插入次序, 那麼能夠在這裏查看解決方案。

鏈式查詢

與傳統數據庫相比,Realm 查詢引擎的一個獨特特性就是:它可以用很小的事務開銷來實現鏈式查詢,而不是每條查詢都要連續不斷地分別去單獨訪問數據庫服務器。

若是您須要獲取一個棕黃色狗狗的結果集,而後在此基礎上再獲取名字以 ‘B’ 開頭的棕黃色狗狗,那麼您能夠像這樣將這兩個查詢鏈接起來:

let tanDogs = realm.objects(Dog.self).filter("color = 'tan'")
let tanDogsWithBNames = tanDogs.filter("name BEGINSWITH 'B'")
複製代碼

結果的自更新

Object 實例是底層數據的動態體現,其會自動進行更新,這意味着您無需去從新檢索結果。它們會直接映射出 Realm 數據庫在當前線程中的狀態,包括當前線程上的寫入事務。惟一的例外是,在使用 for...in 枚舉時,它會將剛開始遍歷時知足匹配條件的全部對象給遍歷完,即便在遍歷過程當中有對象被過濾器修改或者刪除。

let puppies = realm.objects(Dog.self).filter("age < 2")
puppies.count // => 0
try! realm.write {
    realm.create(Dog.self, value: ["name": "Fido", "age": 1])
}
puppies.count // => 1
複製代碼

全部的 Results 對象均有此特性,不管是匹配查詢出來的仍是鏈式查詢出來的。

Results 屬性不只讓 Realm 數據庫保證高速和高效,同時還讓代碼更爲簡潔、更加靈活。例如,若是視圖控制器基於查詢結果來實現,那麼您能夠將 Results 存儲在屬性當中,這樣每次訪問就不須要刷新以確保數據最新了。

您能夠訂閱 Realm 通知,以瞭解 Realm 數據什麼時候發生了更新,好比說能夠決定應用 UI 什麼時候進行刷新,而無需從新檢索 Results。 因爲結果是自動更新的,所以不要迷信下標索引和總數會保持不變。Results 不變的惟一狀況是在快速枚舉的時候,這樣就能夠在枚舉過程當中,對匹配條件的對象進行修改。

try! realm.write {
    for person in realm.objects(Person.self).filter("age == 10") {
        person.age += 1
    }
}
複製代碼

此外,還可使用鍵值編碼 來對 Results 執行相關操做。

限制查詢結果

大多數其餘數據庫技術都提供了從檢索中對結果進行「分頁」的能力(例如 SQLite 中的 「LIMIT」 關鍵字)。這一般是頗有必要的,能夠避免一次性從硬盤中讀取太多的數據,或者將太多查詢結果加載到內存當中。

因爲 Realm 中的檢索是惰性的,所以這行這種分頁行爲是沒有必要的。由於 Realm 只會在檢索到的結果被明確訪問時,纔會從其中加載對象。 若是因爲 UI 相關或者其餘代碼實現相關的緣由致使您須要從檢索中獲取一個特定的對象子集,這和獲取 Results 對象同樣簡單,只須要讀出您所須要的對象便可。

// 循環讀取出前 5 個 Dog 對象
// 從而限制從磁盤中讀取的對象數量
let dogs = try! Realm().objects(Dog.self)
for i in 0..<5 {
    let dog = dogs[i]
    // ...
}
複製代碼

數據遷移

本地遷移

經過設置 Realm.Configuration.schemaVersion 以及 Realm.Configuration.migrationBlock 能夠定義本地遷移。

// 此段代碼位於 application(application:didFinishLaunchingWithOptions:)

let config = Realm.Configuration(
    // 設置新的架構版本。必須大於以前所使用的
    // (若是以前從未設置過架構版本,那麼當前的架構版本爲 0)
    schemaVersion: 1,

    // 設置模塊,若是 Realm 的架構版本低於上面所定義的版本,
    // 那麼這段代碼就會自動調用
    migrationBlock: { migration, oldSchemaVersion in
        // 咱們目前還未執行過遷移,所以 oldSchemaVersion == 0
        if (oldSchemaVersion < 1) {
            // 沒有什麼要作的!
            // Realm 會自行檢測新增和被移除的屬性
            // 而後會自動更新磁盤上的架構
        }
    })

// 通知 Realm 爲默認的 Realm 數據庫使用這個新的配置對象
Realm.Configuration.defaultConfiguration = config

// 如今咱們已經通知了 Realm 如何處理架構變化,
// 打開文件將會自動執行遷移
let realm = try! Realm()
複製代碼

值的更新

// 此段代碼位於 application(application:didFinishLaunchingWithOptions:)

Realm.Configuration.defaultConfiguration = Realm.Configuration(
    schemaVersion: 1,
    migrationBlock: { migration, oldSchemaVersion in
        if (oldSchemaVersion < 1) {
            // enumerateObjects(ofType:_:) 方法將會遍歷
            // 全部存儲在 Realm 文件當中的 `Person` 對象
            migration.enumerateObjects(ofType: Person.className()) { oldObject, newObject in
                // 將兩個 name 合併到 fullName 當中
                let firstName = oldObject!["firstName"] as! String
                let lastName = oldObject!["lastName"] as! String
                newObject!["fullName"] = "\(firstName) \(lastName)"
            }
        }
    })
複製代碼

屬性重命名

// 此段代碼位於 application(application:didFinishLaunchingWithOptions:)

Realm.Configuration.defaultConfiguration = Realm.Configuration(
    schemaVersion: 1,
    migrationBlock: { migration, oldSchemaVersion in
        // 咱們目前還未執行過遷移,所以 oldSchemaVersion == 0
        if (oldSchemaVersion < 1) {
            // 重命名操做必需要在 `enumerateObjects(ofType: _:)` 調用以外進行
            migration.renameProperty(onType: Person.className(), from: "yearsSinceBirth", to: "age")
        }
    })
複製代碼

通知

當整個 Realm 數據庫發生變化時,就會發送 Realm 通知;若是隻有個別對象被修改、添加或者刪除,那麼就會發送集合通知。

通知只會在最初所註冊的註冊的線程中傳遞,而且該線程必須擁有一個正在運行的 Run Loop

Realm 通知

通知處理模塊能夠對整個 Realm 數據庫進行註冊。每次涉及到 Realm 的寫入事務提交以後,不管寫入事務發生在哪一個線程仍是進程中,通知處理模塊都會被激活:

// 獲取 Realm 通知
let token = realm.observe { notification, realm in
    viewController.updateUI()
}
// 隨後
token.invalidate()
複製代碼

集合通知

能夠經過傳遞到通知模塊當中的 RealmCollectionChange 參數來訪問這些變動。該對象存放了受刪除 (deletions)、插入 (insertions) 以及修改 (modifications) 所影響的索引信息。

對象通知

Realm 支持對象級別的通知。能夠在特定的 Realm 對象上進行通知的註冊,對象被刪除、修改時獲取相應的通知。

class StepCounter: Object {
    @objc dynamic var steps = 0
}

let stepCounter = StepCounter()
let realm = try! Realm()
try! realm.write {
    realm.add(stepCounter)
}
var token : NotificationToken?
token = stepCounter.observe { change in
    switch change {
    case .change(let properties):
        for property in properties {
            if property.name == "steps" && property.newValue as! Int > 1000 {
                print("Congratulations, you've exceeded 1000 steps.")
                token = nil
            }
        }
    case .error(let error):
        print("An error occurred: \(error)")
    case .deleted:
        print("The object was deleted.")
    }
}
複製代碼

跨線程使用 Realm 數據庫

在不一樣的線程中使用同一個 Realm 文件,必須每個線程初始化一個新的Realm 實例。

不支持跨線程共享Realm 實例。Realm 實例要訪問相同的 Realm 文件還必須使用相同的 Realm.Configuration。

JSON

Realm 沒有提供對 JSON 的直接支持,可使用 NSJSONSerialization.JSONObjectWithData(_:options:) 的輸出

常見限制

Realm 致力於平衡數據庫讀取的靈活性和性能。爲了實現這個目標,在 Realm 中所存儲的信息的各個方面都有基本的限制。例如:

  1. 類名稱的長度最大隻能存儲 57 個 UTF8 字符。
  2. 屬性名稱的長度最大隻能支持 63 個 UTF8 字符。
  3. Data 和 String 屬性不能保存超過 16 MB 大小的數據。若是要存儲大量的數據,可經過將其分解爲16MB 大小的塊,或者直接存儲在文件系統中,而後將文件路徑存儲在 Realm 中。若是您的應用試圖存儲一個大於 16MB 的單一屬性,系統將在運行時拋出異常。
  4. 每一個單獨的 Realm 文件大小沒法超過應用在 iOS 系統中所被容許使用的內存量——這個量對於每一個設備而言都是不一樣的,而且還取決於當時內存空間的碎片化狀況(關於此問題有一個相關的 Radar:rdar://17119975)。若是您須要存儲海量數據的話,那麼能夠選擇使用多個 Realm 文件並進行映射。
  5. 對字符串進行排序以及不區分大小寫查詢只支持「基礎拉丁字符集」、「拉丁字符補充集」、「拉丁文擴展字符集 A」 以及」拉丁文擴展字符集 B「(UTF-8 的範圍在 0~591 之間)。 線程

儘管 Realm 文件能夠被多個線程同時訪問,可是您不能直接跨線程傳遞 Realms、Realm 對象、查詢和查詢結果。若是您須要跨線程傳遞 Realm 對象的話,您可使用 ThreadSafeReference API。

模型

Setter 和 Getter:由於 Realm 在底層數據庫中重寫了 setters 和 getters 方法,因此您不能夠在您的對象上再對其進行重寫。一個簡單的替代方法就是:建立一個新的 Realm 忽略屬性,該屬性的訪問起能夠被重寫, 而且能夠調用其餘的 getter 和 setter 方法。

自動增加屬性:Realm 沒有線程且進程安全的自動增加屬性機制,而這在其餘數據庫中經常用來產生主鍵。然而,在絕大多數狀況下,對於主鍵來講,咱們須要的是一個惟一的、自動生成的值,所以沒有必要使用順序的、連續的、整數的 ID 做爲主鍵,所以一個獨一無二的字符串主鍵一般就能知足需求了。一個常見的模式是將默認的屬性值設置爲 NSUUID().UUIDString 以產生一個惟一的字符串 ID。

自動增加屬性另外一種常見的動機是爲了維持插入以後的順序。在某些狀況下,這能夠經過向某個 List中添加對象,或者使用 NSDate() 默認值的 createdAt 屬性。

Objective-C 中的屬性:若是您須要在 Objective‑C 中訪問 Realm Swift 模型的話,那麼注意全部 List以及 RealmOptional 屬性都不可用(就像其餘 Swift 獨有的數據類型同樣)——若是有必要的話,您能夠添加封裝的 getter 和 setter 方法,將其在 NSNumber 或者 NSArray 之間進行轉化。此外,早於 Xcode 7 Beta 5 以前的版本有一個 已知的Swift bug,它會致使自動生成的 Objective‑C 頭文件(-Swift.h)沒法經過編譯。您就必須將 List 類型的屬性設置爲 private 或者 internal。請前往 GitHub issue #1925瞭解更多信息。

Object 子類的自定義構造器:當您建立 Object 子類模型的時候,您或許會想要添加本身的構造器方法,以便增長便利性。

因爲 Swift 內省機制中現有的一些限制,咱們不能給這個類中添加指定構造器(designated initializer)。相反,它們須要被標記爲便利構造器(convenience initializer),使用相同名字的 Swift 關鍵詞:

class MyModel: Object {
    @objc dynamic var myValue = ""
    convenience init(myValue: String) {
        self.init() // 請注意這裏使用的是 'self' 而不是 'super'
        self.myValue = myValue
    }
}
複製代碼
相關文章
相關標籤/搜索