Realm-Swift使用入门

7,535 阅读18分钟

Realm适用iOS和Android平台,本身相比sqlite、CoreData操作简单,在这里记录下使用方式; (Swift4.2)

安装

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 重绘以前,您根本无需进行数据刷新或者重新检索。

对象存储

对象的所有更改(添加、修改和删除)都必须在写入事务内完成。

Realm 对象可以被实例化,还可作为未管理对象使用(例如,还未添加到 Realm 数据库),并且使用方式与其它正常 Swift 对象无异。然而,如果要在线程之间共享对象,或者在应用启动后反复使用,那么您必须将这些对象添加到 Realm 数据库中。向 Realm 数据库中添加对象必须在写入事务内完成。由于写入事务将会产生无法忽略的性能消耗,因此您应当检视您的代码,以确保尽可能减少写入事务的数量。

warning:Realm 的写入操作是同步以及阻塞进行的,它并不会异步执行。如果线程 A 开始进行写入操作,然后线程 B 在线程 A 结束之前,对相同的 Realm 数据库也执行了写入操作,那么线程 A 必须要在线程 B 的写入操作发生之前,结束并提交其事务。写入事务会在 beginWrite() 执行时自动刷新,因此重复写入并不会产生竞争条件。

更新对象

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、Int8、Int16、Int32、Int64、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
    }
}