RealmSwift
简介
- Realm是由美国YCombinator孵化的创业团队历时几年打造,第一个专门针对移动平台设计的数据库
- Realm是一个跨平台的移动数据库引擎,目前支持iOS、Android平台,同时支持Objective-C、Swift、Java、React Native、Xamarin等多种编程语言
- Realm并不是对SQLite或者CoreData的简单封装, 是由核心数据引擎C++打造,是拥有独立的数据库存储引擎,可以方便、高效的完成数据库的各种操作
优势与亮点
- 开源
- 简单易用:使用用Realm,则可以极大地减少学习代价和学习时间
- 跨平台:使用Realm数据库,iOS和Android无需考虑内部数据的架构,调用Realm提供的API就可以完成数据的交换
- 线程安全。程序员无需对在不同线程中,对数据库的读取一致性做任何考虑,Realm会保证每次读取都得到一致的数据
使用cocoaPods安装
- Podfile中,使用
user_frameworks!
和pod 'RealmSwift'
- 执行命令
pod install
使用Realm Studio
- 为了配合Realm的使用,Realm还提供了一个轻量级的数据库查看工具Realm Studio,借助这个工具,开发者可以查看数据库当中的内容,并执行简单的插入和删除操作。
-
如果需要调试, 可以通过NSHomeDirectory()打印出Realm数据库地址, 找到对应的Realm文件, 然后用Realm Studio可视化工具打开即可
Realm Studio
使用Realm框架
1、Realm数据库
- 本地化数据库(我们主要使用这个,还有内存中数据库)
- 可同步数据库(使用 Realm 对象服务器 (Realm Object Server) 来实现其内容与其他设备之间的同步)
2、打开Realm数据库
- 首先初始化一个新的 Realm 对象
//默认defalut.realm
let realm = try! Realm()
try! realm.write {
realm.add(dog)
}
第一次创建Realm实例在资源受限的情况下可能会发生错误,使用swift内置的错误处理机制
do {
let realm = try Realm()
} catch let error as NSError {
// 错误处理
}
- 配置Realm,使用
Relam.Configuration()
var config = Realm.Configuration()
//设置某些类只能存储到当前realm数据库中
config.objectTypes = [MyClass.self, MyOtherClass.self]
// 使用默认的目录,但是请将文件名替换为用户名
config.fileURL = config.fileURL!.deletingLastPathComponent().appendingPathComponent("\(username).realm")
// 只读
config.readOnly = true
// 将该配置设置为默认 Realm 配置 设置后Realm()就是当前Realm对象
Realm.Configuration.defaultConfiguration = config
- 异步打开Realm数据库
如果打开Realm数据库的操作需要耗费大量时间,比如需要执行迁移、压缩等操作,建议使用asyncOpen API。
let config = Realm.Configuration(schemaVersion: 1, migrationBlock: { migration, oldSchemaVersion in
// 可能会进行冗长的数据迁移操作
})
Realm.asyncOpen(configuration: config) { realm, error in
if let realm = realm {
// 成功打开 Realm 数据库,迁移操作在后台线程中进行
} else if let error = error {
// 处理在打开 Realm 数据库期间所出现的错误
}
}
3、数据模型
创建数据模型
- 创建数据模型需要继承Object或某个已存在的Realm数据模型类
- Realm模型对象的绝大部分功能与其他swift对象相同。主要限制在于,您只能在对象被创建的线程中使用该对象。
支持的属性类型
- Bool、Int、Int8、Int16、Int32、Int64、Double、Float、String、Date 以及 Data。
- CGFloat 属性被取消了,因为它不具备平台独立性。
- String、Date 以及 Data 属性都是可空的。Object 属性必须可空。
属性特性
- 必须使用@objc dynamic 使swift具有oc动态特性
- 但三种例外属性:
LinkingObjects
、List
以及RealmOptional
,这些属性不能声明为动态类型,因为泛型无法在oc中正确表示。且这些属性应使用let进行声明
class Person: Object {
// 可空字符串属性,默认为 nil
@objc dynamic var name: String? = nil
// 可选 int 属性,默认为 nil
// RealmOption 属性应该始终用 `let` 进行声明,
// 因为直接对其进行赋值并不会起任何作用
let age = RealmOptional<Int>()
//一般不用可空数据类型,常规写法
@objc dynamic var num: Int = 0
let dogs = List<Dog>()
}
关系
- 多对一关系
class Dog: Object {
// ... 其余属性声明
@objc dynamic var owner: Person? // 对一关系必须设置为可空
}
let jam = Person()
let rex = Dog()
rex.owner = jim
- 多对多关系
通过List
属性
class Person: Object {
// ...其他属性声明
let dogs = List<Dog>()
}
您可以照常对 List 属性进行访问和赋值:
let someDogs = realm.objects(Dog.self).filter("name contains 'Fido'")
jim.dogs.append(objectsIn: someDogs)
jim.dogs.append(rex)
List
属性会确保其内部插入次序不会被打乱
注意,不支持包含原始类型的List
进行查询
- 双向关系
上面的例子,只是dog单向的拥有了owner属性,通过LinkingObjects
类型使dog和person拥有双向关系
class Dog: Object {
@objc dynamic var name = ""
@objc dynamic var age = 0
let owners = LinkingObjects(fromType: Person.self, property: "dogs")
}
主键、被忽略属性、索引属性
- 主键 重写
Object.primaryKey()
可以设置模型的主键 - 被忽略属性 不想某些字段保存在Realm数据库中,可以重写
Object.ignoreProperties()
- 索引属性 重写
Object.indexedProperties()
需要为某些特定情况优化读取性能的时候使用
import RealmSwift
// 狗狗的数据模型
class Dog: Object {
@objc dynamic var id: Int = 0
@objc dynamic var owner: Person? // 属性可以设置为可选
override static func primaryKey() -> String? {
return "id" //这里id 是模型类声明的名称
}
override static func ignoreProperties() -> [String] {
return ["owner"]
}
}
4、数据库基本操作
对象的所有更改(添加、修改和删除)都必须在写入事务内完成。
添加数据
//(1)创建Dog对象
let dog: Dog = Dog()
dog.name = "Wang"
dog.age = 18
dog.id = 1
// (2) 从字典中创建 Dog 对象
let myOtherDog = Dog(value: ["name" : "Pluto", "age": 3,"id":2])
// (3) 从数组中创建 Dog 对象
let myThirdDog = Dog(value: ["Fido", 5, 3])
try! realm!.write {
realm!.add(dog) //注意如果添加已有主键对象会崩溃
// realm!.add(dog, update: true)//有主键则更新,无则更新
}
更新数据
//更新
//通过主键更新
let dog: Dog = Dog()
dog.age = 19
dog.id = 1
dog.name = "Lala"
//当没有当前主键即增
try! realm!.write {
realm!.add(dog, update: true)
}
//直接更新
try! realm!.write {
dog.age = 20
}
//键值编码
//`Object`、`Result` 和 `List` 均允许使用 键值编码(KVC)
let persons = realm.objects(Person.self)
try! realm.write {
//第一个person对象的isFirst设置为true
persons.first?.setValue(true, forKeyPath: "isFirst")
// 将每个 person 对象的 planet 属性设置为 "Earth"
persons.setValue("Earth", forKeyPath: "planet")
}
查询数据
- 查询将会返回一个 Results实例,其中包含了一组 Object 对象
- 所有的查询操作(包括检索和属性访问)在 Realm 中都是延迟加载的。只有当属性被访问时,数据才会被读取。
- 查询结果并不是数据的拷贝,修改查询到的数据将会修改数据库
- 从 Realm 数据库中检索对象的最基本方法是
Realm().objects(_:)]
,这个方法将会返回 Object 子类类型
//最基本查询方法
let dogs = realm.objects(Dog.self)//从realm数据库查询所有dog对象
//条件查询
// 使用断言字符串来查询
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)
//链式查询:realm它能够用很小的事务开销来实现链式查询
let tanDogs = realm.objects(Dog.self).filter("color = 'tan'")
let tanDogsWithBNames = tanDogs.filter("name BEGINSWITH 'B'")
- 排序
Results 允许您指定一个排序标准,然后基于关键路径、属性或者多个排序描述符来进行排序。例如,下列代码让上述示例中返回的 Dog 对象按名字进行升序排序:
// 对颜色为棕黄色、名字以 "B" 开头的狗狗进行排序
let sortedDogs = realm.objects(Dog.self).filter("color = 'tan' AND name BEGINSWITH 'B'").sorted(byKeyPath: "name")
请注意,sorted(byKeyPath:)
和sorted(byProperty:)
不支持 将多个属性用作排序基准,此外也无法链式排序(只有最后一个 sorted 调用会被使用)如果要对多个属性进行排序,请使用 sorted(by:)
方法,然后向其中输入多个 SortDescriptor 对象。
- 限制查询结果
大多数其他数据库技术都提供了从检索中对结果进行“分页”的能力(例如 SQLite 中的 “LIMIT” 关键字)。这通常是很有必要的,可以避免一次性从硬盘中读取太多的数据,或者将太多查询结果加载到内存当中。
由于 Realm 中的检索是惰性的,因此这行这种分页行为是没有必要的。因为 Realm 只会在检索到的结果被明确访问时,才会从其中加载对象。
// 循环读取出前 5 个 Dog 对象
// 从而限制从磁盘中读取的对象数量
let dogs = try! Realm().objects(Dog.self)
for i in 0..<5 {
let dog = dogs[i]
// ...
}
- 结果自更新
查询后结果修改会自动更新,但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
删除对象
// 在事务中删除对象
try! realm.write {
realm.delete(cheeseBook)
}
// 从 Realm 数据库中删除所有对象
try! realm.write {
realm.deleteAll()
}
数据迁移(更新数据库)
假设原有模型类
class Person: Object {
@objc dynamic var firstName = ""
@objc dynamic var lastName = ""
@objc dynamic var age = 0
}
添加fullname属性,去掉firstName和lastName
class Person: Object {
@objc dynamic var fullName = ""
@objc dynamic var age = 0
}
本地迁移
通过设置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()
值的更新
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)"
}
}
})
还有属性重命名、线性迁移
通知
- 通知只会在最初所注册的注册的线程中传递,并且该线程必须拥有一个正在运行的 Run Loop。
- 无论写入事务是在哪个线程或者进程中发生的,一旦提交了相关的写入事务,那么通知处理模块就会被异步调用。
- 如果某个写入事务当中包含了 Realm 的版本升级操作,那么通知处理模块很可能会被同步调用。这种情况只会在 Realm 升级到最新版本的时候发生,会抛出异常。可以使用Realm.isInWriteTransaction 来确定是否正处于写入事务当中。
- 由于通知的传递是通过 Run Loop 进行的,因此 Run Loop 中的其他活动可能会延迟通知的传递。如果通知无法立即发送,那么来自多个写入事务的更改可能会合并到一个通知当中。
Realm通知
当整个 Realm 数据库发生变化时,就会发送通知
// 注册 Realm 通知
let token = realm.observe { notification, realm in
viewController.updateUI()
}
// 随后
token.invalidate()
集合通知
通过传递到通知模块当中的 RealmCollectionChange
参数来访问这些变更。该对象存放了受删除 (deletions)、插入 (insertions) 以及修改 (modifications) 所影响的索引信息
class ViewController: UITableViewController {
var notificationToken: NotificationToken? = nil
override func viewDidLoad() {
super.viewDidLoad()
let realm = try! Realm()
let results = realm.objects(Person.self).filter("age > 5")
// 订阅 Results 通知
notificationToken = results.observe { [weak self] (changes: RealmCollectionChange) in
guard let tableView = self?.tableView else { return }
switch changes {
case .initial:
// Results 现在已经填充完数据,无需阻塞 UI 便可直接访问
tableView.reloadData()
case .update(_, let deletions, let insertions, let modifications):
// 检索结果发生改变,将其应用到 UITableView
tableView.beginUpdates()
tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}),
with: .automatic)
tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.endUpdates()
case .error(let error):
// 在后台工作线程中打开 Realm 文件发生了错误
fatalError("\(error)")
}
}
}
deinit {
notificationToken?.invalidate()
}
}
对象通知
您可以在特定的 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 的通知总是以异步的方式进行传递,因此这些操作永远不会阻塞主 UI 线程,也不会导致应用卡顿
- 有时我们需要在主线程进行同步传递,并能够立即反映在 UI 的时候Realm提供了
Realm.commitWrite(withoutNotifying:)
// 添加细粒化通知模块
token = collection.observe { changes in
switch changes {
case .initial:
tableView.reloadData()
case .update(_, let deletions, let insertions, let modifications):
// 检索结果发生改变,将其应用到 UITableView
tableView.beginUpdates()
tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}),
with: .automatic)
tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.endUpdates()
case .error(let error):
// 处理错误
()
}
}
func insertItem() throws {
// 在主线程执行界面驱动更新:
collection.realm!.beginWrite()
collection.insert(Item(), at: 0)
// 随后立即将其同步到 UI 当中
tableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
// 确保变更通知不会再次响应变更
try collection.realm!.commitWrite(withoutNotifying: [token])
}
键值观察
- Realm 对象的大多数属性都遵从键值观察机制。所有
Object
子类的持久化存储(未被忽略)的属性都是遵循 KVO 机制的,并且Object
以及List
中的无效
属性也同样遵循(然而LinkingObjects
属性并不能使用 KVO 进行观察)。
加密
- Realm 支持在创建 Realm 数据库时采用64位的密钥对数据库文件进行 AES-256+SHA2 加密。
// 生成随机秘钥
var key = Data(count: 64)
_ = key.withUnsafeMutableBytes { bytes in
SecRandomCopyBytes(kSecRandomDefault, 64, bytes)
}
// 打开已加密的 Realm 文件
let config = Realm.Configuration(encryptionKey: key)
do {
let realm = try Realm(configuration: config)
// 照常使用 Realm 数据库
let dogs = realm.objects(Dog.self).filter("name contains 'Fido'")
} catch let error as NSError {
// 如果秘钥错误,`error` 会提示数据库无法访问
fatalError("Error opening realm: \(error)")
}
线程
- 单个线程中无需考虑并行或多线程处理问题。
- Realm 通过确保每个线程始终拥有 Realm 的一个快照,以便让并发运行变得十分轻松。
- 不能让多个线程都持有同一个 Realm 对象的实例
检视其他线程上的变化
- 在主 UI 线程中(或者任何一个位于 runloop 中的线程),对象会在 runloop 的每次循环过程中自行获取其他线程造成的更改
- Realm 会自每个 runloop 循环的开始自动进行刷新,除非 Realm 的 autorefresh 属性设置为 NO。如果某个线程没有 runloop 的话(通常是因为它们被放到了后台进程当中),那么 Realm.refresh() 方法必须手动调用,以确保让事务维持在最新的状态当中。
跨线程传递实例
跨线程使用 Realm 数据库
JSON
Realm 没有提供对 JSON 的直接支持,但是您可以使用 NSJSONSerialization.JSONObjectWithData(_:options:)
的输出,实现将 JSON 添加到 Object
的操作。