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
    }
}
复制代码
相关文章
相关标签/搜索