Realm Objective-C & Swift 2.2: スレッド間のオブジェクトを受け渡し、関連による並べ替えなどをサポート

これまでのRealmの設計が目指していたもののひとつに、一貫性があり、わかりやすいスレッドモデルを提供するということがありました。このたび、Realm Objective‑CおよびRealm Swift 2.2より、安全にスレッド間をまたいでオブジェクトを受け渡すことができる仕組みを用意しました。また関連のプロパティを並べ替えに使用することがこのバージョンからできるようになりました。その他、同期に関する改善と不具合の修正が含まれます。

スレッドに従う

本日より、Realmでは複数のスレッド間でオブジェクトを扱うことがさらに簡単になりました。これまでのマルチスレッドの対応は何年もの間の絶え間ない議論の果てに下された決断で、意図的なものです(なぜなら、マルチスレッドは本質的に難しいものだからです。)。

以前に公開した、「Threading Deep Dive」という記事において、Realmがオブジェクトグラフ全体を一貫して表示しつつ、ユーザーにロックやリソースの競合を解決するといった作業をさせないで済むように、いかにスレッドを取り扱っているかということを説明しました。特に、この設計は他のORMやデータフレームワークにおいては「フォールト」の概念を構築しています 💥。

公式ドキュメントのマルチスレッドセクションは、マルチスレッドでRealmを正しく使用する方法を真に理解するために必見の資料です。この資料に目を通すと、マルチスレッドとRealmを非常に生産的に使用できます。しかし、これまではオブジェクトはスレッド間で受け渡すことができませんでした。

Thread Confinement

Realmがスレッドセーフであるなら、なぜスレッドをまたいでオブジェクトを渡そうとすると例外が起こるのでしょうか!?

一貫性と安全性を保証するために、シンプルな制約を課しているためです。Realmインスタンス、オブジェクト、Resultsオブジェクト、およびListオブジェクトは、それが生成されたスレッド内でだけ利用することができる、という制約です。

「Thread Confinement」という制約は、Realmの内部構造や人為的な制約に基づく一時的な制限ではなく、正確なコードの記述を容易にする設計の重要な部分であることを理解することが重要です。

実際に、Realmオブジェクトをスレッド間で自由に受け渡せるようにすることを実装するのは非常に簡単ですが、正しく使用するのが非常に危険で難しく、予測不可能になるというトレードオフがあります。

Realmは、トランザクションベースのデータベースであり、データが中途半端な状態でディスクに永続化されることはありません(書き込みトランザクションの範囲でデータの整合性を保証できます。)。

記事の更新情報を受け取る

独立性(ACIDの「I」)を保証するために、別のトランザクションの変更は、トランザクションがコミットされるまで他のトランザクションからは見ることができません。そうでなければ、Realmはこの設計によって「フォールト」を読み込んでしまうでしょう。

この独立性を保証するために、特定のスレッドのRealmは同じ時点のデータを保持しています。(実際にはスレッドごとに1つのRealmしかありません)。 Realmやオブジェクト、クエリなどをスレッド間で自由に受け渡せてしまうと、異なる時点のデータを混在させることになり、非常に難しい結果につながります。 たとえば、オブジェクトを削除した後のスレッドにオブジェクトを渡すとクラッシュする可能性があります。また、スレッドをまたいで値が変更される可能性があります。別のトランザクションで異なる関連を持つこともできてしまいます。

データをスレッド間で受け渡すこれまでの(古い)やり方

これまでは、データをスレッド間で受け渡す際には、Realmが管理していないオブジェクトを渡さなければなりませんでした。

それはつまり、アンマネージド(Realmに永続化されていない)のオブジェクトを渡すことだったり、プライマリーキーを渡して、読み込み直すといったことだったりしました。

let realm = try! Realm()
let person = Person(name: "Jane", primaryKey: 123)
let pk = person.primaryKey
try! realm.write {
  realm.add(person)
}
DispatchQueue(label: "com.example.myApp.bg").async {
  let realm = try! Realm()
  guard let person = realm.object(ofType: Person.self,
                                  forPrimaryKey: pk) else {
    return // person was deleted
  }
  try! realm.write {
    person.name = "Jane Doe"
  }
}

しかし、オブジェクトがプライマリキーを持っていなかったら、この方法はうまくいきません。また、古いデータを使ってしまう可能性があります。ListResultsLinkingObjectsのようなRealmオブジェクト以外のものを渡すことも、このアプローチでは簡単に行えません。

スレッドセーフ参照を使用する

これからは、以前に「Thread Confinement」とされていたオブジェクトすべてに対して、スレッドセーフ参照を利用することができます。そしてよりシンプルに、次のような3ステップだけでスレッド間で受け渡すことができます。

  1. ThreadSafeReferenceを「Thread Confinement」なオブジェクトを用いて生成する。
  2. ThreadSafeReferenceを別のスレッドに渡す。
  3. 渡された参照を保存されているRealmインスタンスのRealm.resolve(_:)メソッドの引数に渡し、参照を解決する。このメソッドの戻り値として取得したオブジェクトは通常のオブジェクトと同じように使えます。

例:

let realm = try! Realm()
let person = Person(name: "Jane") // no primary key required
try! realm.write {
  realm.add(person)
}
let personRef = ThreadSafeReference(to: person)
DispatchQueue(label: "com.example.myApp.bg").async {
  let realm = try! Realm()
  guard let person = realm.resolve(personRef) else {
    return // person was deleted
  }
  try! realm.write {
    person.name = "Jane Doe"
  }
}

現実的な例 🌏

オープンソースとして公開されているRealmTasksアプリをご覧ください。iOS PR #374というプルリクエストによって、古いやり方でオブジェクトをスレッド間で受け渡していた箇所が、スレッドセーフ参照を使う方法に書き換えられています。

下記は関連するコードの一部です。このコードは、バックグラウンドスレッドでListプロパティの重複を取り除く、という処理をしています。

realm.addNotificationBlock { _, realm in
  let items = realm.objects(TaskListList.self).first!.items
  guard items.count > 1 && !realm.isInWriteTransaction else { return }
  let itemsReference = ThreadSafeReference(to: items)
  DispatchQueue(label: "io.realm.RealmTasks.bg").async {
    let realm = try! Realm()
    guard let items = realm.resolve(itemsReference), items.count > 1 else {
      return
    }
    realm.beginWrite()
    let listReferenceIDs = NSCountedSet(array: items.map { $0.id })
    for id in listReferenceIDs where listReferenceIDs.count(for: id) > 1 {
      let id = id as! String
      let indexesToRemove = items.enumerated().flatMap { index, element in
        return element.id == id ? index : nil
      }
      indexesToRemove.dropFirst().reversed().forEach(items.remove(objectAtIndex:))
    }
    try! realm.commitWrite()
  }
}

関連のプロパティを用いて並べ替える

これまでは直接のプロパティを使うことでしか、Realmコレクションを並べ替えることはできませんでした。

Realm 2.2より、Realmコレクションを1対1の関連のプロパティによって並べ替えることができます。

例えば、PersonクラスのRealmコレクションを関連であるdogプロパティのageの値によって並べ替えるとします。その場合は、dogOwners.sorted(byKeyPath: "dog.age")のように書きます。

class Person: Object {
  dynamic var name = ""
  dynamic var dog: Dog?
}
class Dog: Object {
  dynamic var name = ""
  dynamic var age = 0
}

realm.beginWrite()

let lucy = realm.create(Dog.self, value: ["Lucy", 7])
let freyja = realm.create(Dog.self, value: ["Freyja", 6])
let ziggy = realm.create(Dog.self, value: ["Ziggy", 9])

let mark = realm.create(Person.self, value: ["Mark", freyja])
let diane = realm.create(Person.self, value: ["Diane", lucy])
let hannah = realm.create(Person.self, value: ["Hannah"])
let don = realm.create(Person.self, value: ["Don", ziggy])
let diane_sr = realm.create(Person.self, value: ["Diane Sr", ziggy])

let dogOwners = realm.objects(Person.self)
print(dogOwners.sorted(byKeyPath: "dog.age").map({ $0.name }))
// Prints: ["Mark", "Diane", "Don", "Diane Sr", "Hannah"]

以前のバージョンのRealmで、同様の動作を実現するには、Personオブジェクトにdog.ageと同じ内容のプロパティを格納するか、Resultsを使わずにRealmの機能を使わずにソートする必要がありました。その場合はResultsのメリットは失われてしまいます。

下記はAPIの変更点です。今回の改善により、「プロパティ」ではなくより正しい「キーパス」という用語を使用するように変更しました。

  • 下記のObjective-C APIは非推奨となります。新しいAPIを使用してください。
Deprecated API New API
-[RLMArray sortedResultsUsingProperty:] -[RLMArray sortedResultsUsingKeyPath:]
-[RLMCollection sortedResultsUsingProperty:] -[RLMCollection sortedResultsUsingKeyPath:]
-[RLMResults sortedResultsUsingProperty:] -[RLMResults sortedResultsUsingKeyPath:]
+[RLMSortDescriptor sortDescriptorWithProperty:​ascending] +[RLMSortDescriptor sortDescriptorWithKeyPath:​ascending:]
RLMSortDescriptor​.property RLMSortDescriptor​.keyPath
  • 下記のSwift APIは非推奨となります。新しいAPIを使用してください。
Deprecated API New API
LinkingObjects​.sorted(byProperty:​ascending:) LinkingObjects​.sorted(byKeyPath:​ascending:)
List.sorted(byProperty:​ascending:) List.sorted(byKeyPath:​ascending:)
RealmCollection.sorted(byProperty:​ascending:) RealmCollection.sorted(byKeyPath:​ascending:)
Results.sorted(byProperty:​ascending:) Results.sorted(byKeyPath:​ascending:)
SortDescriptor(property:​ascending:) SortDescriptor(keyPath:​ascending:)
SortDescriptor​.property SortDescriptor​.keyPath

Realmのバージョニングはセマンティックバージョニングに準拠しています。そのため、上記の非推奨となったメソッドはRealmがバージョン3.xになるまではそのまま残され、使用できます。

その他の変更

同期に関する非互換の変更(ベータ版のため)

  • 内部で使用している同期エンジンのバージョンをBETA-6.5にバージョンアップしました。
  • 同期に関連するエラー通知の挙動が変更されました。特定のユーザーまたはセッションに関連しないエラーは、同期エンジンによって「致命的」と分類された場合にのみ通知されます。

不具合の修正

  • deleteRealmIfMigrationNeededを設定すると、Realm 0.xで作られたファイルを1.x、または1.xのファイルを2.xで開く場合など、ファイルフォーマットの移行が必要な場合にもRealmファイルが削除されるようになりました。
  • ネストされたサブクエリを含むクエリを解釈できない問題を修正しました。
  • 古いスレッドからのRLMRealmインスタンスがまだ存在している間に、スレッドIDが再使用されたときに誤った例外が発生する問題を修正しました。

古いSwiftバージョンのサポートについて

Xcode 7.3.1とSwift 2.2のサポートはできる限り長い間続ける予定ですが、できるだけ早くXcode 8に移行することをおすすめします。


お読みいただきありがとうございます。 Realm で素晴らしいアプリケーションを作りましょう!お困りの際はStack Overflow(日本語)Slack(日本語)Twitter(日本語)GitHub(英語)でご相談ください。


Realm Cocoa Team