Realm accessed from incorrect thread.

#はじめに
去年からずっと悩んでいたRealmSwiftのエラーが最近、解決しました。

#原因
タイトルにもあるように`不正なスレッドからレルムにアクセスしたこと`が原因です。

Realmのデータベース(以下、DB)では、異なるスレッド間でのアクセスは保証されていますが、スレッドを跨いでアクセスするのは禁止されています。

```swift
let realm = Realm() // メインスレッドで生成

DispatchQueue.global().async {
  // メインスレッドとは異なるスレッド

  realm.write {} // Realm accessed from incorrect thread.
}
```

#沼った部分
自分はずっとRealmインスタンスだけが、スレッド間を跨いでアクセスしなければOKだと思っていて、その思い込みのせいで沼にハマってしまいました。

解決の糸口は、[Realm入門](https://www.amazon.co.jp/%E8%BB%BD%E9%87%8F%E3%83%BB%E9%AB%98%E9%80%9F%E3%83%A2%E3%83%90%E3%82%A4%E3%83%AB%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9Realm%E5%85%A5%E9%96%80-%E8%8F%85%E5%8E%9F-%E7%A5%90/dp/4774188484)という書籍にありました。

#Realmの制約
Realmインスタンスは初期化した際に`スレッドID`を内部で保持し、DBのアクセス時に初期化の時のスレッドIDと現在のスレッドIDを比較して異なるスレッドIDならエラーを吐く仕様になっています。

でこれはなにもRealmインスタンスに限った話ではありませんでした。

Realmでは`マネージドオブジェクト・アンマネージドオブジェクト`があって、Realmインスタンスやモデル定義(テーブル定義)する際に継承する[Object](https://docs.mongodb.com/realm-legacy/docs/swift/latest/index.html#models)、クエリが実行された時に返ってくる[Results](https://docs.mongodb.com/realm-legacy/docs/swift/latest/index.html#queries)等がマネージドオブジェクトに該当します。

このマネージドオブジェクトでは必ず、内部でRealmインスタンスを持っており、値にアクセスする時は`スレッドチェック`が行われます。

そして、注意点としてObjectを継承したモデルオブジェクトは元々、アンマネージドオブジェクトで`Realm.add()する事によってマネージドオブジェクトになります`。

```swift
// モデル定義
class HogeObject: Object {
  // 何らかの値
}

let realm = try! Realm() // メインスレッドで生成
let hogeObj = HogeObject() // この時点ではアンマネージドオブジェクト

try! realm.write {
// メインスレッドのRealmに保存している
realm.add(hogeObj) // add()することによってマネージドオブジェクトになる
}

let results = realm.objects(HogeObject.self) // メインスレッドのrealmから生成

DispatchQueue.global().async {
  // メインスレッドとは異なるスレッド

  // 各インスタンスはメインスレッドで生成しているので
  // ここでアクセスするとエラーが発生する

  print(hogeObj) // Realm accessed from incorrect thread.
  print(results) // Realm accessed from incorrect thread.
  realm.write { } // Realm accessed from incorrect thread.
} 
```
以上を踏まえて自分のコードを読んでみると`マネージドオブジェクトになったモデルオブジェクトを別スレッドで使っていた`ことが判明しました。

だからずっとエラーが消えなかったんですね。

因みに`アンマネージドオブジェクトはスレッドを跨いでもOK`です。

#解決方法①
Realmでは、異なるスレッド間でオブジェクトを渡せるように[ThreadSafeReference](https://docs.mongodb.com/realm-legacy/docs/swift/latest/index.html#threading)が用意されていて本来であれば、これで解決します。

```swift
let realm = try! Realm() // メインスレッドで生成
let hogeObj = HogeObject()

try! realm.write {
  realm.add(hogeObj)
}

// マネージドオブジェクトのhogeObjを渡し、
// ThreadSafeReferenceインスタンスを生成する

let ref = ThreadSafeReference(to: hogeObj) // hogeObjへのスレッドセーフな参照を持つ

DispatchQueue.global().async {
  // メインスレッドとは異なるスレッド

  let realm = try! Realm() // サブスレッドで生成

  // 定数refはhogeObjへのスレッドセーフな参照を持つ
  // resolve()によって、サブスレッドのrealmインスタンスからhogeObjへの参照を解決(取り出す)ことが出来る
  guard let resolved = realm.resolve(ref) else {
    return
  }
  // マネージドオブジェクトにアクセスできるようになる
  print(resolved) // HogeObject
}
```

ただ今回は特殊ケースで、`保存後・削除後のモデルオブジェクトを一時的に保持してそれを別スレッドで使います`。

保存後のモデルオブジェクトであれば、ThreadSafeReferenceでどうにかなりましたが、削除後のモデルオブジェクトは`DBから削除されて無効なものになり、モデル定義したプロパティにアクセスするとエラーになります`。

なのでThreadSafeReferenceは参照できません。

```swift
let realm = try! Realm()
let hogeObj = HogeObject()

try! realm.write {
  realm.delete(hogeObj)
}
// 無効なオブジェクトで参照できないのでエラーになる
let ref = ThreadSafeReference(to: hogeObj) // Cannot construct reference to invalidated object
```
しかも、無効になった後もマネージドオブジェクトのままです。

#解決方法②
ではどうしたのかというと、会社の先輩を頼りました。

どうやって解決されたかというと、`元々あるモデルオブジェクトとは別にObjectを継承していないモデルオブジェクトを定義して`この問題を回避していました。

```swift
// Objectを継承していないモデルオブジェクト
struct HogeObject {
  var hoge: String

  init() {
    let obj = HogeRealmObject()
    self.hoge = obj.hoge
  }

  // Realmオブジェクトに変換する
  func toRealmObject() -> HogeRealmObject {
    let obj = HogeRealmObject()
    obj.hoge = self.hoge
    return obj
  }

// Objectを継承しているモデルオブジェクト
  class HogeRealmObject: Object {
    @objc dynamic var hoge: String = ""
  }
}

let realm = try! Realm()
var hogeObj = HogeObject()
hogeObj.hoge = "hogehoge"

// Realmのオブジェクトに変換してDBに追加する
let realmObj = hogeObj.toRealmObject()

try! realm.write {
  realm.add(realmObj)
}

DispatchQueue.global().async {
  // マネージドオブジェクトではないので別スレッドでもアクセス可能
  print(hogeObj.hoge) // "hogehoge"
}
```
以上のやり方だと、保存後・削除後とか関係なく値にアクセスできるし、かつスレッド間を跨ぐことも出来ます。

#終わりに
とりあえず、年明け前の悩みが消えて良かったです。