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源码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,313评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,369评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,916评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,333评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,425评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,481评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,491评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,268评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,719评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,004评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,179评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,832评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,510评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,153评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,402评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,045评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,071评论 2 352

推荐阅读更多精彩内容