@[TOC](IOS DB存储之Realm.swift 使用详解)
1. Realm简介
1.1 什么是Realm
Realm是一个跨平台的移动数据库引擎,于2014 年7月发布,是一个跨平台的移动数据库引擎,专门为移动应用的数据持久化而生。其目的是要取代 Core Data 和 SQLite。目前支持iOS、Android平台,同时支持Objective-C、Swift、Java、React Native、Xamarin等多种编程语言* Realm并不是对SQLite或者CoreData的简单封装, 是由核心数据引擎C++打造,是拥有独立的数据库存储引擎,可以方便、高效的完成数据库的各种操作.
Realm官网
Realm官方文档
Realm GitHub
1.2 Realm的优点
- 跨平台:现在很多应用都是要兼顾iOS和Android两个平台同时开发。如果两个平台都能使用相同的数据库,那就不用考虑内部数据的架构不同,使用Realm提供的API,可以使数据持久化层在两个平台上无差异化的转换。
- 简单易用:Core Data 和 SQLite 冗余、繁杂的知识和代码足以吓退绝大多数刚入门的开发者,而换用 Realm,则可以极大地减少学习成本,立即学会本地化存储的方法。毫不吹嘘的说,把官方最新文档完整看一遍,就完全可以上手开发了。
- 可视化:Realm 还提供了一个轻量级的数据库查看工具,在Mac Appstore 可以下载“Realm Browser”这个工具,开发者可以查看数据库当中的内容,执行简单的插入和删除数据的操作。毕竟,很多时候,开发者使用数据库的理由是因为要提供一些所谓的“知识库”。
1.3 Realm支持的类型
- Realm 支持以下的属性类型:Bool、Int8、Int16、Int32、Int64、Double、Float、String、Date(精度到秒)以及Data.
- 也可以使用
List
和Object
来建立诸如一对多、一对一之类的关系模型,此外Object
的子类也支持此功能。
2. Realm安装配置
2.1 Cocospod安装
pod 'RealmSwift'
2.2 手动集成
(1)先去 Realm 的官网去下载最新框架:http://static.realm.io/downloads/swift/latest
(2)拖拽 RealmSwift.framework 和 Realm.framework 文件到”Embedded Binaries”选项中。选中 Copy items if needed 并点击 Finish
2.3 配置 Realm
2.3.1 配置 Realm 数据库
- 在打开
Realm
数据库之前,可以对其进行配置。 通过创建一个Realm.Configuration
的对象实例,然后配置相应的属性。 通过创建并自定义相关的配置值,使得您可以实现个性化的设置,包括如下方面:
- 对于本地 Realm 数据库而言,可以配置 Realm 文件在磁盘上的路径;
- 对于可同步 Realm 数据库而言,可以配置管理该 Realm 数据库的用户,以及 Realm 数据库在 Realm 对象服务器上的远程路径;
- 对于架构版本之间发生变化的 Realm 数据库而言,可以通过迁移功能来控制旧架构的 Realm 数据该如何更新到最新的架构。
- 对于存储的数据量过大、或者数据频繁发生变化的 Realm 数据库而言,可以通过压缩功能来控制 Realm 文件该如何实现压缩,从而确保能高效地利用磁盘空间。
- 要应用配置,可以在每次需要获取
Realm
实例的时候,通过向Realm(configuration: config)
方法传递该配置对象,或者通过Realm.Configuration.defaultConfiguration = config
方法,将默认Realm
数据库的默认配置设置为我们所需的配置。 - 例如,假设有一个应用要求用户必须要登录到 Web 后端服务器中,并且需要支持账户快速切换功能的话。 那么您可以通过以下代码,来为每个账户提供一个独立的
Realm
数据库,并且当前账户所使用的数据库将作为默认Realm
数据库来使用:
func setDefaultRealmForUser(username: String) {
var config = Realm.Configuration()
// 使用默认的目录,但是请将文件名替换为用户名
config.fileURL = config.fileURL!.deletingLastPathComponent().appendingPathComponent("\(username).realm")
// 将该配置设置为默认 Realm 配置
Realm.Configuration.defaultConfiguration = config
}
2.3.1.1 打包进项目里的数据库的使用
- 如果需要将应用的某些数据(比如配置信息,初始化信息等)打包到一个 Realm 文件中,作为主要 Realm 数据库的扩展,操作如下:
let config = Realm.Configuration(
// 获取需要打包文件的 URL 路径
fileURL: Bundle.main.url(forResource: "MyBundledData", withExtension: "realm"),
// 以只读模式打开文件,因为应用数据包并不可写
readOnly: ``true``)`
// 通过配置打开 Realm 数据库
let realm = try! Realm(configuration: config)
// 通过配置打开 Realm 数据库
let results = realm.objects(Dog.self).filter("age > 5")
2.3.2 同步打开 Realm 数据库
-
Realm
对象服务器上的 Realm 数据库同样也可以使用Realm.Configuration
和相关的工厂方法进行配置,这与之前创建本地Realm
数据库的做法基本类似,只不过在Realm.Configuration
中,需要将syncConfiguration
属性设置为SyncConfiguration
。 可同步Realm
数据库 (synchronized Realm
) 可通过 URL 地址来进行定位。
// 创建配置
let syncServerURL = URL(string: "realm://localhost:9080/~/userRealm")!
let config = Realm.Configuration(syncConfiguration: SyncConfiguration(user: user, realmURL: syncServerURL))
// 打开远程 Realm 数据库
let realm = try! Realm(configuration: config)
// 任何对此 Realm 数据库所做的操作,都会同步到所有设备上!
2.3.3 异步打开 Realm 数据库
- 如果打开
Realm
数据库的操作需要耗费大量时间的话,比如说需要执行迁移、压缩 或者需要从可同步Realm
数据库下载远程内容,那么建议使用asyncOpen API
。 这使得您可以在调度到指定队列之前,在后台线程中执行任意的初始化工作。 当可同步Realm
数据库只能以只读权限打开的时候,那么必须使用asyncOpen
。
2.3.4 内存中 Realm 数据库
- 通过配置
Realm.Configuration
中的inMemoryIdentifier
属性,而不是fileURL
属性,这样就能够创建一个完全在内存中运行的 Realm 数据库 (in-memory Realm
),它将不会存储在磁盘当中。 设置inMemoryIdentifier
会将fileURL
置为nil
(反之亦然)。
let realm = try! Realm(configuration: Realm.Configuration(inMemoryIdentifier: "MyInMemoryRealm"))
2.3.5 删除 Realm 文件
在某些情况下,例如清除缓存、或者重置整个数据集之类的操作,那么就可能需要从磁盘中将
Realm
文件给完全删除掉。除非必要,否则
Realm
数据库会避免将数据复制到内存中,因此由Realm
管理的所有对象都会包含磁盘上文件的引用,并且必须在文件被安全删除之前完成释放。 这包括从Realm
读取到(或者添加到 Realm 中)的所有对象,包括所有List
、Results
和ThreadSafeReference
对象,以及Realm
本身。实际上,这意味着对
Realm
文件的删除,要么在应用启动时、在打开Realm
数据库之前完成,要么只在显式声明的自动释放池 中打开Realm
数据库,然后在自动释放池后面进行删除。 这样才能够确保所有的Realm
对象都能够成功释放。最后,尽管不是必须的,不过您应当将
Realm
辅助文件连同Realm
文件一起删除,以完全清除所有的相关文件。
autoreleasepool {
// 在这里进行所有的 Realm 操作
}
let realmURL = Realm.Configuration.defaultConfiguration.fileURL!
let realmURLs = [
realmURL,
realmURL.appendingPathExtension("lock"),
realmURL.appendingPathExtension("note"),
realmURL.appendingPathExtension("management")
]
for URL in realmURLs {
do {
try FileManager.default.removeItem(at: URL)
} catch {
// 错误处理
}
}
3. Realm数据迁移
3.1 为何要迁移
- 举个例子:
- 比如原来有如下
Person
模型:
class Person: Object {
@objc dynamic var firstName = ""
@objc dynamic var lastName = ""
@objc dynamic var age = 0
}
- 假如我们想要更新数据模型,给它添加一个
fullname
属性, 而不是将“姓”和“名”分离开来。
class Person: Object {
@objc dynamic var fullName = ""
@objc dynamic var age = 0
}
- 在这个时候如果您在数据模型更新之前就已经保存了数据的话,那么
Realm
就会注意到代码和硬盘上数据不匹配。 每当这时,您必须进行数据迁移,否则当您试图打开这个文件的话Realm
就会抛出错误。
3.2 如何进行数据迁移
3.2.1 修改数据表结构
- 假设我们想要把上面所声明 Person 数据模型进行迁移。如下所示是最简单的数据迁移的必需流程
// 在(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()
- 虽然这个迁移操作是最精简的了,但是我们需要让这个闭包能够自行计算新的属性(这里指的是
fullName
),这样才有意义。 - 在迁移闭包中,我们能够调用
Migration().enumerateObjects(_:_:)
来枚举特定类型的每个Object
对象,然后执行必要的迁移逻辑。注意,对枚举中每个已存在的Object
实例来说,应该是通过访问oldObject
对象进行访问,而更新之后的实例应该通过newObject
进行访问:
// 在 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
// 将名字进行合并,存放在 fullName 域中
let firstName = oldObject!["firstName"] as! String
let lastName = oldObject!["lastName"] as! String
newObject!["fullName"] = "\(firstName) \(lastName)"
}
}
})
3.2.2 重命名表字段
- 如果要重命名某张数据库表的某个字段,可以直接通过修改模型来处理。例如将
Person
类中的yearsSinceBirth
属性重命名为age
,修改配置生效后,对应的表字段也会改过来。
// Inside your application(application:didFinishLaunchingWithOptions:)
Realm.Configuration.defaultConfiguration = Realm.Configuration(
schemaVersion: 1,
migrationBlock: { migration, oldSchemaVersion in
// We haven’t migrated anything yet, so oldSchemaVersion == 0
if (oldSchemaVersion < 1) {
// The renaming operation should be done outside of calls to `enumerateObjects(ofType: _:)`.
migration.renameProperty(onType: Person.className(), from: "yearsSinceBirth", to: "age")
}
})
4. Realm加密
(1)加密后的 Realm文件不能跨平台使用(因为 NSFileProtection 只有 iOS 才可以使用)
(2)Realm 文件不能在没有密码保护的 iOS 设备中进行加密。为了避免这些问题(或者您想构建一个 OS X 的应用),可以使用 Realm 提供的加密方法。
(3)加密过的 Realm 只会带来很少的额外资源占用(通常最多只会比平常慢10%)。
- 加密Demo实例下载:realm-cocoa
-
Realm
支持在创建 Realm 数据库时采用64位的密钥对数据库文件进行AES-256+SHA2
加密。
/***** 在创建 Realm 数据库时采用64位的密钥对数据库文件进行 AES-256+SHA2 加密 ****/
// 产生随机密钥
var key = Data(count: 64)
_ = key.withUnsafeMutableBytes { bytes in
SecRandomCopyBytes(kSecRandomDefault, 64, bytes)
}
// 打开加密文件
let config = Realm.Configuration(encryptionKey: key)
let realm:Realm
do {
realm = try Realm(configuration: config)
} catch let error as NSError {
// 如果密钥错误,error 会提示数据库不可访问
fatalError("Error opening realm: \(error)")
}
// 和往常一样使用 Realm 即可
let dogs = realm.objects(Book.self).filter("name contains Fido")
- 这样硬盘上的数据都能都采用
AES-256
来进行加密和解密,并用SHA-2 HMAC
来进行验证。 每次您要获取一个 Realm 实例时,您都需要提供一次相同的密钥。 - 加密过的
Realm
只会带来很少的额外资源占用(通常最多只会比平常慢10%)
5. Realm基本使用
5.1 增,删,改,查操作
5.1.1 新增
- Realm 的Object对象通过ORM于DB的表做了映射,插入数据都是通过add object来实现。
let realm = try! Realm()
try! realm.write {
realm.add(myDog)
}
- 对象的所有更改(添加、修改和删除)都必须在写入事务内完成。
-
Realm
对象可以被实例化,还可作为未管理对象使用(例如,还未添加到 Realm 数据库),并且使用方式与其它正常 Swift 对象无异。然而,如果要在线程之间共享对象,或者在应用启动后反复使用,那么您必须将这些对象添加到 Realm 数据库中。向 Realm 数据库中添加对象必须在写入事务内完成。由于写入事务将会产生无法忽略的性能消耗,因此您应当检视您的代码,以确保尽可能减少写入事务的数量。
注意:Realm 的写入操作是同步以及阻塞进行的,它并不会异步执行。如果线程 A 开始进行写入操作,然后线程 B 在线程 A 结束之前,对相同的 Realm 数据库也执行了写入操作,那么线程 A 必须要在线程 B 的写入操作发生之前,结束并提交其事务。写入事务会在 beginWrite() 执行时自动刷新,因此重复写入并不会产生竞争条件。
- 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 重绘以前,您根本无需进行数据刷新或者重新检索。
5.1.1.1 数据插入实例
- 这里以个人消费记录为例,我们先定义消费类别类,和具体消费记录类
import Foundation
import RealmSwift
//消费类型
class ConsumeType: Object {
//类型名
@objc dynamic var name = ""
}
//消费条目
class ConsumeItem: Object {
//条目名
@objc dynamic var name = ""
//金额
@objc dynamic var cost = 0.00
//时间
@objc dynamic var date = Date()
//所属消费类别
@objc dynamic var type: ConsumeType?
}
- 判断数据库记录是否为空,空的话则插入数据库(这里以默认数据库为例)
import UIKit
import RealmSwift
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
//使用默认的数据库`
let realm = try! Realm()
//查询所有的消费记录
let items = realm.objects(ConsumeItem.self)
//已经有记录的话就不插入了
if items.count>0 {
return
}
//创建两个消费类型
let type1 = ConsumeType()
type1.name = "购物"
let type2 = ConsumeType()
type2.name = "娱乐"
//创建三个消费记录
let item1 = ConsumeItem(value: ["买一台电脑", 5999.00, Date(), type1]) //可使用数组创建`
let item2 = ConsumeItem()
item2.name = "看一场电影"
item2.cost = 30.00
item2.date = Date(timeIntervalSinceNow: -36000)
item2.type = type2
let item3 = ConsumeItem()
item3.name = "买一包泡面"
item3.cost = 2.50
item3.date = Date(timeIntervalSinceNow: -72000)
item3.type = type1
// 数据持久化操作(类型记录也会自动添加的)`
try! realm.write {
realm.add(item1)
realm.add(item2)
realm.add(item3)
}
//打印出数据库地址
print(realm.configuration.fileURL ?? "")
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
5.1.2 更新
- Realm 提供了一系列更新对象的方法,根据使用场景的不同, 每个方法都有各自的优缺点。
5.1.2.1 直接更新
- 您可以在写入事务中,通过设置对象的属性从而完成更新。
// 在事务中更新对象
try! realm.write {
author.name = "Thomas Pynchon"
}
5.1.2.2 键值编码更新
-
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")
}
5.1.2.3 通过主键更新
- 如果数据模型类中包含了主键,那么 可以使用
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 的字典,那么这个设定会被应用到应用当中,并且这些属性值也会被清空。 为了确保不会出现意外的数据丢失, 在使用此方法之前请再三确认, 只提供了想要进行更新的属性值。
5.1.3 删除
- 在写入事务中,将要删除的对象传递给
Realm().delete(_:)
方法。
// cheeseBook 存储在 Realm 数据库中
// 在事务中删除对象
try! realm.write {
realm.delete(cheeseBook)
}
- 您同样也可以删除存储在
Realm
数据库当中的所有数据。请注意,Realm
文件会保留在磁盘上所占用的空间,从而为以后的对象预留足够的空间,从而实现快速存储。
// 从 Realm 数据库中删除所有对象
try! realm.write {
realm.deleteAll()
}
5.1.2 查询
5.1.2.1 简单查询
- 查询将会返回一个
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 对象
5.1.2.2 条件查询
- 如果您对
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)
5.1.2.3 查询排序
-
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
对象。
5.1.2.4 链式查询
- 与传统数据库相比,Realm 查询引擎的一个独特特性就是:它能够用很小的事务开销来实现链式查询,而不是每条查询都要接二连三地分别去单独访问数据库服务器。
- 如果您需要获取一个棕黄色狗狗的结果集,然后在此基础上再获取名字以 ‘B’ 开头的棕黄色狗狗,那么您可以像这样将这两个查询连接起来:
let tanDogs = realm.objects(Dog.self).filter("color = 'tan'")
let tanDogsWithBNames = tanDogs.filter("name BEGINSWITH 'B'")
5.1.2.5 查询结果的自更新
-
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
}
}
5.1.2.6 限制查询结果
- 大多数其他数据库技术都提供了从检索中对结果进行“分页”的能力(例如
SQLite
中的 “LIMIT” 关键字)。这通常是很有必要的,可以避免一次性从硬盘中读取太多的数据,或者将太多查询结果加载到内存当中。 - 由于
Realm
中的检索是惰性的,因此这行这种分页行为是没有必要的。因为Realm
只会在检索到的结果被明确访问时,才会从其中加载对象。 - 如果由于 UI 相关或者其他代码实现相关的原因导致您需要从检索中获取一个特定的对象子集,这和获取
Results
对象一样简单,只需要读出您所需要的对象即可。
// 循环读取出前 5 个 Dog 对象
// 从而限制从磁盘中读取的对象数量
let dogs = try! Realm().objects(Dog.self)
for i in 0..<5 {
let dog = dogs[i]
// ...
}
5.2 模型定义
5.2.1 属性的Setter 和 Getter
- Setter 和 Getter:因为
Realm
在底层数据库中重写了setters
和getters
方法,所以您不可以在您的对象上再对其进行重写。 - 一个简单的替代方法就是:创建一个新的
Realm
忽略属性,该属性的访问起可以被重写, 并且可以调用其他的getter
和setter
方法。
5.2.2 自动增长属性
自动增长属性:
Realm
没有线程且进程安全的自动增长属性机制,而这在其他数据库中常常用来产生主键。然而,在绝大多数情况下,对于主键来说,我们需要的是一个唯一的、自动生成的值,因此没有必要使用顺序的、连续的、整数的 ID 作为主键,因此一个独一无二的字符串主键通常就能满足需求了。一个常见的模式是将默认的属性值设置为NSUUID().UUIDString
以产生一个唯一的字符串 ID。自动增长属性另一种常见的动机是为了维持插入之后的顺序。在某些情况下,这可以通过向某个
List
中添加对象,或者使用NSDate()
默认值的createdAt
属性。
5.2.3 计算型属性
- 计算属性将被自动忽略
- 只读属性会被自动忽略
- 被忽略的属性,不会通过ORM映射到数据库表。
5.2.4 忽略属性(不会映射到DB)
- 重写
Object.ignoredProperties()
可以防止Realm
存储数据模型的某个属性。Realm 将不会干涉这些属性的常规操作,它们将由成员变量(var)提供支持,并且您能够轻易重写它们的setter
和getter
。
class Person: Object {
@objc dynamic var tmpID = 0
var name: String { // 计算属性将被自动忽略
return "\(firstName) \(lastName)"
}
@objc dynamic var firstName = ""
@objc dynamic var lastName = ""
override static func ignoredProperties() -> [String] {
return ["tmpID"]
}
}
5.2.5 添加主键(Primary Keys)
- 重写
Object.primaryKey()
可以设置模型的主键。 - 声明主键之后,对象将被允许查询,更新速度更加高效,并且要求每个对象保持唯一性。
- 一旦带有主键的对象被添加到
Realm
之后,该对象的主键将不可修改。
class Person: Object {
@objc dynamic var id = 0
@objc dynamic var name = ""
override static func primaryKey() -> String? {
return "id"
}
}
5.2.6 添加索引属性(Indexed Properties)
- 重写
Object.indexedProperties()
方法可以为数据模型中需要添加索引的属性建立索引:
class Book: Object {
@objc dynamic var price = 0
@objc dynamic var title = ""
override static func indexedProperties() -> [String] {
return ["title"]
}
}
5.2.7 反向关系(Inverse Relationship)
- 通过反向关系(也被称为反向链接(backlink)),您可以通过一个特定的属性获取和给定对象有关系的所有对象。 Realm 提供了“链接对象 (linking objects)” 属性来表示这些反向关系。借助链接对象属性,您可以通过指定的属性来获取所有链接到指定对象的对象。
- 例如,一个 Dog 对象可以拥有一个名为 owners 的链接对象属性,这个属性中包含了某些 Person 对象,而这些 Person 对象在其 dogs 属性中包含了这一个确定的 Dog 对象。您可以将 owners 属性设置为 LinkingObjects 类型,然后指定其关系,说明其当中包含了 Person 对象。
class Dog: Object {
@objc dynamic var name = ""
@objc dynamic var age = 0
// Realm 并不会存储这个属性,因为这个属性只定义了 getter
// 定义“owners”,和 Person.dogs 建立反向关系
let owners = LinkingObjects(fromType: Person.self, property: "dogs")
}
5.2.8 使用List实现一对多关系
-
List
中可以包含简单类型的Object
,表面上和可变的Array
非常类似。
注意:List
只能够包含Object
类型,不能包含诸如String
之类的基础类型。 - 如果打算给我们的 Person 数据模型添加一个“dogs”属性,以便能够和多个“dogs”建立关系,也就是表明一个 Person 可以有多个 Dog,那么我们可以声明一个List类型的属性:
class Person: Object {
... // 其余的属性声明
let dogs = List()
}
// 这里我们就可以使用已存在的狗狗对象来完成初始化
let aPerson = Person(value: ["李四", 30, [aDog, anotherDog]])
// 还可以使用多重嵌套
let aPerson = Person(value: ["李四", 30, [["小黑", 5], ["旺财", 6]]])
- 可以和之前一样,对 List 属性进行访问和赋值:
let someDogs = realm.objects(Dog.self).filter("name contains '小白'")
ZhangSan.dogs.append(objectsIn: someDogs)
ZhangSan.dogs.append(dahuang)
5.2.9 Objective-C 中的属性
- 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。
5.2.10 Object 子类的自定义构造器
- 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
}
}
5.3 通知处理
- 当整个
Realm
数据库发生变化时,就会发送Realm
通知;如果只有个别对象被修改、添加或者删除,那么就会发送集合通知。 - 通知只会在最初所注册的注册的线程中传递,并且该线程必须拥有一个正在运行的
Run Loop
5.3.1 Realm 通知
- 通知处理模块可以对整个 Realm 数据库进行注册。每次涉及到 Realm 的写入事务提交之后,无论写入事务发生在哪个线程还是进程中,通知处理模块都会被激活:
// 获取 Realm 通知
let token = realm.observe { notification, realm in
viewController.updateUI()
}
// 随后
token.invalidate()
5.3.2 集合通知
- 可以通过传递到通知模块当中的
RealmCollectionChange
参数来访问这些变更。该对象存放了受删除 (deletions
)、插入 (insertions
) 以及修改 (modifications
) 所影响的索引信息。
5.3.3 对象通知
-
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.")
}
}
5.4 JSON 数据处理
5.4.1 JSON数据处理实例
- 我们将从 网络 获取一组 JSON 格式的频道数据,然后将它以 Realm Objects 的形式储存到默认的 Realm 数据库里。
- json数据格式如下:
{
"channels": [
{
"name_en": "Personal Radio",
"seq_id": 0,
"abbr_en": "My",
"name": "私人兆赫",
"channel_id": 0
},
{
"name": "华语",
"seq_id": 0,
"abbr_en": "",
"channel_id": "1",
"name_en": ""
},
{
"name": "欧美",
"seq_id": 1,
"abbr_en": "",
"channel_id": "2",
"name_en": ""
}
]
}
- 我们将直接把 Dictionary 插入到 Realm 中,然后让 Realm 自行快速地将其映射到 Object 上。
- 为了确保示例能够成功,我们需要一个所有属性完全匹配 JSON 键结构的 Object 结构体。如果 JSON 的键结构不匹配 Object 结构体属性结构的话,那么就会在插入时被忽略。
- 定义Object对象如下:
class DoubanChannel:Object {
//频道id
@objc dynamic var channel_id = ""
//频道名称
@objc dynamic var name = ""
//频道英文名称
@objc dynamic var name_en = ""
//排序
@objc dynamic var seq_id = 0
@objc dynamic var abbr_en = ""
//设置主键
override static func primaryKey() -> String? {
return "channel_id"
}
}
- JSON数据转Object,插入数据代码如下:
import UIKit
import RealmSwift
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 调用API
let url = URL(string: "http://www.douban.com/j/app/radio/channels")!
let response = try! Data(contentsOf: url)
// 对 JSON 的回应数据进行反序列化操作
let json = try! JSONSerialization.jsonObject(with: response,
options: .allowFragments) as! [String:Any]
let channels = json["channels"] as! [[String:Any]]
let realm = try! Realm()
try! realm.write {
// 为数组中的每个元素保存一个对象(以及其依赖对象)
for channel in channels {
if channel["seq_id"] as! Int == 0 {continue} //第一个频道数据有问题,丢弃掉
realm.create(DoubanChannel.self, value: channel, update: true)
}
}
print(realm.configuration.fileURL ?? "")
}
}
6. Realm使用注意
6.1 对字符长度的限制
- Realm 致力于平衡数据库读取的灵活性和性能。为了实现这个目标,在 Realm 中所存储的信息的各个方面都有基本的限制。例如:
类名称
的长度最大只能存储57
个 UTF8 字符。属性名称
的长度最大只能支持63
个 UTF8 字符。- Data 和 String 属性不能保存超过 16 MB 大小的数据。如果要存储大量的数据,可通过将其分解为16MB 大小的块,或者直接存储在文件系统中,然后将文件路径存储在 Realm 中。如果您的应用试图存储一个大于 16MB 的单一属性,系统将在运行时抛出异常。
- 对字符串进行排序以及不区分大小写查询只支持“基础拉丁字符集”、“拉丁字符补充集”、“拉丁文扩展字符集 A” 以及”拉丁文扩展字符集 B“(UTF-8 的范围在 0~591 之间)。
- 每个单独的 Realm 文件大小无法超过应用在 iOS 系统中所被允许使用的内存量——这个量对于每个设备而言都是不同的,并且还取决于当时内存空间的碎片化情况(关于此问题有一个相关的 Radar:rdar://17119975)。如果您需要存储海量数据的话,那么可以选择使用多个 Realm 文件并进行映射。
6.2 Realm使用注意点
6.2.1 多线程问题
6.2.1.1 跨线程使用 Realm 数据库
- 在不同的线程中使用同一个
Realm
文件,必须每一个线程初始化一个新的Realm
实例。- 不支持跨线程共享
Realm
实例。Realm
实例要访问相同的Realm
文件还必须使用相同的Realm.Configuration
- 尽管 Realm 文件可以被多个线程同时访问,但是您不能直接跨线程传递 Realms、Realm 对象、查询和查询结果。如果您需要跨线程传递 Realm 对象的话,您可以使用 ThreadSafeReference API。(这个其实也不算是个问题,我们在多线程中新建新的Realm对象就可以解决)
6.2.1.1 跨线程访问数据库,Realm对象一定需要新建一个
*** Terminating app due to uncaught exception 'RLMException', reason: 'Realm accessed from incorrect thread.'******* First throw call stack:****(**** 0 CoreFoundation 0x000000011479f34b __exceptionPreprocess + 171**** 1 libobjc.A.dylib 0x00000001164
如果程序崩溃了,出现以上错误,那就是因为你访问Realm数据的时候,使用的Realm对象所在的线程和当前线程不一致。
- 解决办法: 就是在当前线程重新获取最新的Realm,即可。
6.2.1.2 .查询也不能跨线程查询
- 列如:
RLMResults * results = [self selectUserWithAccid:bhUser.accid]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
RLMRealm *realm = [RLMRealm defaultRealm];
[realm transactionWithBlock:^{
[realm addOrUpdateObject:results[0]];
}];
});
- 由于查询是在子线程外查询的,所以跨线程也会出错,出错信息如下:
***** Terminating app due to uncaught exception 'RLMException', reason: 'Realm accessed from incorrect thread'******* First throw call stack:****(**** 0 CoreFoundation 0x000000011517a34b __exceptionPreprocess + 171**** 1 libobjc.A.dylib 0x0000000116
6.2.2 Realm对象的 Setters & Getters 不能被重载
- 因为 Realm 在底层数据库中重写了 setters 和 getters 方法,所以您不可以在您的对象上再对其进行重写。一个简单的替代方法就是:创建一个新的 Realm 忽略属性,该属性的访问起可以被重写, 并且可以调用其他的 getter 和 setter 方法。
6.2.3 文件大小 & 版本跟踪
一般来说 Realm 数据库比 SQLite 数据库在硬盘上占用的空间更少。如果您的 Realm 文件大小超出了您的想象,这可能是因为您数据库中的 RLMRealm中包含了旧版本数据。
为了使您的数据有相同的显示方式,Realm 只在循环迭代开始的时候才更新数据版本。这意味着,如果您从 Realm 读取了一些数据并进行了在一个锁定的线程中进行长时间的运行,然后在其他线程进行读写 Realm 数据库的话,那么版本将不会被更新,Realm 将保存中间版本的数据,但是这些数据已经没有用了,这导致了文件大小的增长。这部分空间会在下次写入操作时被重复利用。这些操作可以通过调用writeCopyToPath:error:来实现。
- 解决办法:
通过调用invalidate,来告诉 Realm 您不再需要那些拷贝到 Realm 的数据了。这可以使我们不必跟踪这些对象的中间版本。在下次出现新版本时,再进行版本更新。
您可能在 Realm 使用Grand Central Dispatch时也发现了这个问题。在 dispatch 结束后自动释放调度队列(dispatch queue)时,调度队列(dispatch queue)没有随着程序释放。这造成了直到
RLMRealm 对象被释放后,Realm 中间版本的数据空间才会被再利用。为了避免这个问题,您应该在 dispatch 队列中,使用一个显式的自动调度队列(dispatch queue)。
6.2.4 Realm 没有自动增长属性
Realm 没有线程/进程安全的自动增长属性机制,这在其他数据库中常常用来产生主键。然而,在绝大多数情况下,对于主键来说,我们需要的是一个唯一的、自动生成的值,因此没有必要使用顺序的、连续的、整数的 ID 作为主键。
- 解决办法:
在这种情况下,一个独一无二的字符串主键通常就能满足需求了。一个常见的模式是将默认的属性值设置为 [[NSUUID UUID] UUIDString]
以产生一个唯一的字符串 ID。
自动增长属性另一种常见的动机是为了维持插入之后的顺序。在某些情况下,这可以通过向某个 RLMArray中添加对象,或者使用 [NSDate date]默认值的createdAt属性。
6.2.5 所有的数据模型必须直接继承自RealmObject
- 这阻碍我们利用数据模型中的任意类型的继承
这一点也不算问题,我们只要自己在建立一个model就可以解决这个问题。自己建立的model可以自己随意去继承,这个model专门用来接收网络数据,然后把自己的这个model转换成要存储到表里面的model,即RLMObject对象。这样这个问题也可以解决了。
Realm 允许模型能够生成更多的子类,也允许跨模型进行代码复用,但是由于某些 Cocoa 特性使得运行时中丰富的类多态无法使用。以下是可以完成的操作:
父类中的类方法,实例方法和属性可以被它的子类所继承
子类中可以在方法以及函数中使用父类作为参数
以下是不能完成的:
多态类之间的转换(例如子类转换成子类,子类转换成父类,父类转换成子类等)
同时对多个类进行检索
多类容器 (RLMArray以及 RLMResults)
6.2.6 Realm不支持集合类型
这一点也是比较蛋疼。
Realm支持以下的属性类型:BOOL、bool、int、NSInteger、long、long long、float、double、NSString、NSDate、NSData以及 被特殊类型标记的NSNumber。CGFloat属性的支持被取消了,因为它不具备平台独立性。
这里就是不支持集合,比如说NSArray,NSMutableArray,NSDictionary,NSMutableDictionary,NSSet,NSMutableSet。如果服务器传来的一个字典,key是一个字符串,对应的value就是一个数组,这时候就想存储这个数组就比较困难了。当然Realm里面是有集合的,就是RLMArray,这里面装的都是RLMObject。
所以我们想解决这个问题,就需要把数据里面的东西都取出来,如果是model,就先自己接收一下,然后转换成RLMObject的model,再存储到RLMArray里面去,这样转换一遍,还是可以的做到的。
6.2.7 建议每个model都需要设置主键,这样可以方便add和update
如果能设置主键,请尽量设置主键,因为这样方便我们更新数据,我们可以很方便的调用addOrUpdateObject:
或者 createOrUpdateInRealm:withValue:
方法进行更新。这样就不需要先根据主键,查询出数据,然后再去更新。有了主键以后,这两步操作可以一步完成。
6.2.8 事务处理注意点
6.2.8.1 transactionWithBlock 已经处于一个写的事务中,事务之间不能嵌套
- transactionWithBlock 已经处于一个写的事务中,如果还在block里面再写一个commitWriteTransaction,就会出错,写事务是不能嵌套的。
例如:
[realm transactionWithBlock:^{
[self.realm beginWriteTransaction];
[self convertToRLMUserWith:bhUser To:[self convertToRLMUserWith:bhUser To:nil]];
[self.realm commitWriteTransaction];
}];
出错信息如下:
*** Terminating app due to uncaught exception 'RLMException', reason: 'The Realm is already in a write transaction'******* First throw call stack:****(**** 0 CoreFoundation 0x0000000112e2d34b __exceptionPreprocess + 171**** 1 libobjc.A.dylib 0x00000001
6.2.9 自己封装一个Realm全局实例单例是没啥作用的
很多开发者应该都会对
Core Data
和Sqlite3
或者FMDB
,自己封装一个类似Helper
的单例。于是我也在这里封装了一个单例,在新建完Realm数据库的时候strong持有一个Realm的对象。然后之后的访问中只需要读取这个单例持有的Realm对象就可以拿到数据库了。想法是好的,但是同一个
Realm
对象是不支持跨线程操作realm
数据库的。Realm
通过确保每个线程始终拥有Realm
的一个快照,以便让并发运行变得十分轻松。你可以同时有任意数目的线程访问同一个Realm
文件,并且由于每个线程都有对应的快照,因此线程之间绝不会产生影响。需要注意的一件事情就是不能让多个线程都持有同一个Realm
对象的 实例 。如果多个线程需要访问同一个对象,那么它们分别会获取自己所需要的实例(否则在一个线程上发生的更改就会造成其他线程得到不完整或者不一致的数据)。其实
let realm = try! Realm(configuration: config)
(OC代码:RLMRealm *realm = [RLMRealm defaultRealm];
) 这句话就是获取了当前realm
对象的一个实例,其实实现就是拿到单例。所以我们每次在子线程里面不要再去读取我们自己封装持有的realm
实例了,直接调用系统的这个方法即可,能保证访问不出错。