iOS数据持久化之-Realm使用深入详解篇
原创 2019-11-21
相信关于Realm的基本使用介绍,在很多文章都已经介绍过了,其中访问比较多的有:
Realm在iOS中的简单使用
Realm数据库 从入门到“放弃”
官方也讲解得比较详细:官方文档
在此篇文章中,我只深入的介绍或者说比那些基础篇更加综合的验证官方文档所提及到的关于【线程】【观察】【事务】的操作
首先我们创建两个实例,以此来作为Object讲解对象
class Book: Object {
@objc dynamic var id = 0
@objc dynamic var name: String?
let owners = LinkingObjects<Student>(fromType: Student.self, property: "books")
override static func primaryKey() -> String {
return "id"
}
}
class Student: Object {
@objc dynamic var idCard: String = "20191110"
@objc dynamic var name: String? = "wang"
@objc dynamic var age: Int = 0
let books = List<Book>()
override static func primaryKey() -> String {
return "idCard"
}
}
在进一步说明之前,还是先来上一段关于增删改查的操作
/// 初始化一个Realm实例,将采用默认配置
var realm = try! Realm()
/// add 添加/更新一个对象
let student = Student()
student.idCard = "20191120"
try? realm.write {
// 当update=true时,必须要求Object的subclass实现override static func primaryKey() -> String
realm.add(student, update: true)
}
// or
realm.beginWrite()
realm.add(student, update: true)
try? realm.commitWrite()
/// 查询
let results = realm.objects(Student.self)
print("Basisc query results: \(results.count)")
// 还可以进行链式操作
let results0 = results.filter("idCard=%@", "20191120")
// 如果设置了主键还可以查询指定的对象
let queryStudent = realm.object(ofType: Student.self, forPrimaryKey: "20191120")
print("Basisc query object: \(queryStudent)")
/// 删除一个对象或者一系列对象
let student1 = Student()
student1.idCard = "20191121"
try? realm.write {
realm.add(student1, update: true)
}
print("Basisc delete before results count: \(results.count)")
let results1 = realm.objects(Student.self).filter("idCard=%@", "20191121")
try? realm.write {
realm.delete(results1)
}
print("Basisc delete after results count: \(results.count)") // 可以看出results结果实现了自更新
// 当然你也可以
//try? realm.write {
// realm.delete(student1)
//}
/// 多对多的使用,多对一或一对一
/// 请看Student的:books属性
let book = Book()
book.id = 1
book.name = "语文"
let book1 = Book()
book1.id = 2
book.name = "数学"
try? realm.write {
realm.add(book, update: true)
realm.add(book1, update: true)
student.books.append(book)
student.books.append(book1)
}
print("Basisc Many-to-Many Many-to-One: \(student.books)")
/// inverse 关系,什么时候我们会用到这种反转的关系呢?当一个东西拥有多个关联持有者是,我们希望知道他的持有者是谁,那么这个时候就有必要
/// 请看Book的:owners 属性
DispatchQueue.global().async {
let book = (try! Realm()).object(ofType: Book.self, forPrimaryKey: "1")
print("Basisc Many-to-Many Many-to-One inverse: \(book.owners.first)")
}
通过上诉总结了如下几点:
1、在进行任何已被管理的对象操作时都必须满足 [Object].isInvalidated == false
2、相同线程下的不同Realm实例事务操作不能够嵌套,因为这个时候即使新建Realm实例,但其还是处于transition中,不同线程下的不同Realm实例是可以嵌套的
3、Realm不支持自增key
4、Realm查询到Results仅当真正访问的时候才会加载到内存当中,故Realm查询并不支持limit,并且对数据加载到内存更友好
5、Realm查询结果是自更新的,亦即意味着在任意线程更新了数据,那么都将会自动更新到查询结果
这是基本上用到Realm数据库需要了解到的基本
接下来说说在跨线程中的操作及结果验证
- Realm实例对象是否可以跨线程访问?答案否!
let realm = try! Realm()
print("Thread isMain: \(Thread.isMainThread)")
/// 这里我们验证一个打开的realm是否可以跨线程
/// 错误示例,Realm实例不能够跨线程
DispatchQueue.global().async {
let student = Student()
try? realm.write {
realm.add(student, update: true)
}
}
- Object子类是否可以跨线程访问?答案否!
let student = Student()
try? realm.write {
realm.add(student, update: true)
}
/// 错误示例,已经被Realm示例managed的对象不能够跨线程
/// 但是处于unmanaged的对象就可以当成一般的对象使用,是可以跨线程访问的,可以尝试将上述add屏蔽掉再来看看结果
DispatchQueue.global().async {
let realm = try! Realm()
try? realm.write {
realm.add(student, update: true)
}
}
- Results 查询结果是否可以跨线程访问?答案否!
let results = realm.objects(Student.self)
DispatchQueue.global().async {
print("Thread Results access before")
print("Thread Results access \(results.count)") // 这里会出错
print("Thread Results access after")
}
所以【注意且重要】 Realm、Object、Results 或者 List 受管理实例皆受到线程的限制,这意味着它们只能够在被创建的线程上使用,否则就会抛出异常。这是 Realm 强制事务版本隔离的一种方法。否则,在不同事务版本中的线程间,通过潜在泛关系图 (potentially extensive relationship graph) 来确定何时传递对象将不可能实现。
那么综上我们是否就没有办法跨线程访问Realm实例对象了呢?当然不是Realm给我们提供了一个ThreadSafeReference,来方便我们跨线程访问
- 针对Object跨线程访问
let studentRef = ThreadSafeReference<Student>(to: student)
DispatchQueue.global().async {
let realm = try! Realm()
guard let studentCopy = realm.resolve(studentRef) else {
return
}
print("Thread Object ThreadSafeReference: \(studentCopy.idCard)") // 可以看到结果正常输出
}
- 针对Results跨线程访问
let resultsRef = ThreadSafeReference<Results<Student>>(to: results)
DispatchQueue.global().async {
let realm = try! Realm()
guard let resultsCopy = realm.resolve(resultsRef) else {
return
}
print("Thread Results ThreadSafeReference: \(resultsCopy.count)") // 可以看到结果正常输出
// 那么我们是否可以进一步访问resultsCopy中的结果呢? 从输出结果看,答案是可以的
print("Thread Results ThreadSafeReference Object update before: \(resultsCopy.first)")
try? realm.write {
resultsCopy.first?.name = "ThreadSafeReference"
}
print("Thread Results ThreadSafeReference Object update after: \(resultsCopy.first)")
}
当然你也可以验证其他List、LinkingObjects,在我的Demo中已经提供了这些的所有验证,在文末我将会贴上Demo地址
故关于Realm线程方面的总结如下:
1、Realm、Object、Results 或者 List 被管理实例皆受到线程的限制,只能够在被创建且被管理的实例线程中使用
2、Object、Results、List也可以通过ThreadSafeReference来跨线程安全访问,这意味着当我们不确定某个被管理对象或者已经确定某个被管理对象在其他线程使用,开始新线程访问前我们都都可以通过线程安全引用使其能够跨线程访问
3、如果一个ThreadSafeReference被一个Realm实例resolve后,那么ThreadSafeReference所指向的那个对象的管理Realm也将会变更到当前实例
再来说说Realm数据库的观察(observe)操作
Realm数据库支持realm实例全局observe,Results结果集observe,以及Object对象实例观察
- Realm实例观察
let realmToken = realm.observe { (notification, realm) in
print("Observe Realm: notification => \(notification), realm => \(realm)")
}
- Object实例观察
let animal = Animal()
print("Observe Object is managed before: \(animal.realm != nil)")
try? realm.write {
realm.add(animal, update: true)
let realm0 = try! Realm()
let animal0 = Animal()
animal0.id = Int.max
// 相同线程的不同Realm实例事务是不能够嵌套的
// try? realm0.write {
// realm0.add(animal0, update: true)
// }
DispatchQueue.global().asyncAfter(deadline: .now(), execute: {
let animal1 = Animal()
animal1.id = 1
let realm1 = try! Realm()
// 不同的线程Realm实例事务是可以嵌套的
try? realm1.write {
realm1.add(animal1, update: true)
}
/// 注意这里即使在不同的线程不同Realm实例下,只要在一个事务中,都不能够被观察
// _ = animal1.observe({ (change) in
// print("Observe Object 在不同的Realm实例的事务中观察")
// })
})
}
print("Observe Object is managed after: \(animal.realm != nil)")
// 注意所有的观察都不能够在相同Realm实例的事务中操作!!!!
// 错误做法
//try? realm.write {
// _ = animal.observe({ (_) in
// // do something
// })
//}
// 正确做法
let animatToken = animal.observe { (change) in
switch change {
case .error(let error):
print("Observe Object error: \(String(describing: error))")
case .change(let propertyChanges):
for propertyChange in propertyChanges {
print("Observe Object propertyChange: name => \(propertyChange.name), oldValue => \(propertyChange.oldValue), newValue => \(propertyChange.newValue)")
}
default:
print("Observe Object deleted")
}
}
- Results结果集观察
let resultToken = results.observe { (change: RealmCollectionChange<Results<Animal>>) in
switch change {
case .initial(let results):
print("Observe Result initial: \(results.count)")
break
case .update(let results, let deletions, let insertions, let modifications):
print("Observe Result update: \(results.count) deletions => \(deletions) insertions => \(insertions) modifications => \(modifications)")
default:
print("Observe Result error")
}
}
故关于Realm实例observe方面的总结如下:
1、Realm/Object/Results/List都可被观察,并且当数据发生变化不论是在哪个进程或者线程都会被通知到
2、所有的观察不可以在Realm的实例事务中操作,不管被管理对象是否归属于当前Realm实例managed
3、相同线程下的不同Realm实例事务操作不能够嵌套,因为这个时候即使新建Realm实例,但其还是处于transition中,不同线程下的不同Realm实例是可以嵌套的
更多的详解及错误示例,请查看RealmStudy demo
在此篇文章的基础之上,我还封装了一个对外看似以单例操作数据库的manager,使用非常简单:
// 初始化
XRealm.default.initialize(withUID: "xxx")
// 跨线程写入
XRealm.default.write {
// 任何数据库操作
}
// 新增
XRealm.default.add(object, true/false, true)
// 删除
XRealm.default.delete(object)
XRealm.default.delete(results)
XRealm.default.delete(sequence)
// 查询
XRealm.default.object(Object.self)
// 事务中实现观察
var token: NotificationToken?
let realm = try! Realm()
let book = Book()
book.id = 0
book.name = "语文"
try? realm.write {
realm.add(book, update: true)
}
try? realm.write {
book.name = "数学"
XRealm.default.observe(book, { (change) in
print("fuck 观察变更 \(change)")
}, { [weak self] (token, error) in
self?.token = token
print("fuck 观察 \(token) \(error)")
})
}
其处理了:
1、对数据增删改的数据是否invalidate的验证
2、事务嵌套操作的规避,即如果已经开启了事务那么将不会在begin一个事务
3、关于如何规避在事务中执行observe造成的崩溃问题
关于如何处理的可以查看XRealm源码