tl;dr
Realmのマイグレーション中のオブジェクトはプリミティブ型かMigrationObject型にキャストして値を読み込む。
モチベ
Realmのマイグレーションについて調べると、簡単な例が載っている。
- プロパティを消す、モデルを追加する場合は、migration blockを定義するだけで良い
- プロパティをリネームするときはmigration.renamePropertyを使うとはやい
- enumerateObjectsでオブジェクトを列挙して、それぞれに対して値を書き込める
3つ目の列挙について、載っているサンプルはこんな感じ。
migration.enumerateObjects(ofType: Person.className()) { oldObject, newObject in
// combine name fields into a single field
let firstName = oldObject!["firstName"] as! String
let lastName = oldObject!["lastName"] as! String
newObject!["fullName"] = "\(firstName) \(lastName)"
}
firstName
と lastName
という2つのプロパティを読み込んで、fullName
というプロパティの初期値にする方法です。
でもこれはかなり単純な例で、List
の時、ネストしたオブジェクトの時、LinkingObject
の時、にどうすればよいのかわかりませんでした。
が、試行錯誤の末、やっと方法がわかったのでご紹介します
そもそもなぜ困るのか
例えばサンプルと同じPersonというクラスについて、以下のようにフィールドを変化させる場合を考えます。
// old
class Person: Object {
dynamic var firstName: String = ""
dynamic var lastName: String = ""
}
// new
class Person: Object {
dynamic var fullName: String = ""
}
マイグレーションを実行する段階では新しいバージョンになっているので、 Person
クラスは fullName
というプロパティのみ持っています。
DB内部では firstName
, lastName
のみを持っている状態のままなので、これを Person
として読み出すことはできません。
つまり、マイグレーションが終わるまではRealmの型は基本的にどれも使えないことになります。
MigrationObject
migration.enumerateObjects(ofType: Person.className()) { oldObject, newObject in
}
このブロックの中で、oldObject
と newObject
は MigrationObject
という型を持っています。
MigrationObject
はoldObject!["lastName"]
のように、subscriptでフィールド名を文字列として渡すと、値を取得することができます。このときAny型になります。
そしてこれをStringならStringに、oldObject!["lastName"] as? String
とすればキャストできます。
じゃあ、これがネストした他のRealmオブジェクトだったりしたときにどうしたらいいのかなと、思うわけです。勘が良い人はもしかしたら気づいているかもしれないですが、僕は全然気づかず、かなり苦戦しました。
※マイグレーションはどんどん追加していったとしても順番に実行していきますが、
もしプロパティ名をkeyPathなどを使ってクラス定義から取得していると、将来そのプロパティを削除したときにコンパイルエラーになるので、文字列のまま使うのが良いと思っています。
Listオブジェクトの読み込み
考えあぐねていたところに、公式サンプルで以下のようなListオブジェクトのマイグレーションの例を見つけました。
let dogs = newObject?["pets"] as? List<MigrationObject>
つまり、マイグレーション中の曖昧なオブジェクトはMigrationObjectにキャストできて、subscriptで階層的に読み込めるのでは?
ネストしたオブジェクトの読み込み
結論から言えば、上の予想のように、ネストしたオブジェクトは、 MigrationObject
にキャストできます。
例えば以下のような構造を仮定します。
class Foo: Object {
dynamic var bar: Bar
}
class Bar: Object {
dynamic var baz: Baz
}
class Baz: Object {
dynamic var qux: String
}
このとき、 Foo
に qux: String
というプロパティを付け替えたいと思ったら、以下のようにします。
migration.enumerateObjects(ofType: Foo.className()) { oldObject, newObject in
let oldQux = oldObject
.flatMap { $0["bar"] as? MigrationObject }
.flatMap { $0["baz"] as? MigrationObject }
.flatMap { $0["qux"] as? String }
newObject?["qux"] = oldQux ?? ""
}
LinkingObjectの読み込み
以下のような構造を想定して、Dogに hasOwner: Bool
というプロパティを追加する場合を考えます。これの初期値は owners
を読み込んで、その数から決定したいです。
※Personをマイグレーションする場合はListなので、上の章で書いたとおりです。
class Person: Object {
// ... other property declarations
let dogs = List<Dog>()
}
class Dog: Object {
// ... other property declarations
let owners = LinkingObjects(fromType: Person.self, property: "dogs")
}
migration.enumerateObjects(ofType: Dog.className()) { oldObject, newObject in
// migrations
}
このとき、 oldObject?["owners"]
を読み込もうとするとクラッシュします。
が、 newObject?["owners"]
は読み込むことができます。
そしてその型は、 RLMResults<MigrationObject>
になります。 RLMResult型を使うためには RealmSwift
だけでなく Realm
もimportする必要があります。
よって、以下のように書くことができます。
migration.enumerateObjects(ofType: Dog.className()) { oldObject, newObject in
// migrations
let ownersCount = (newObject?["owners"] as? RLMResults<MigrationObject>)?.count
newObject?["hasOwner"] = ownersCount > 0
}
おまけ
MigrationObject
は DynamicObject
のtypealiasです。(ref: Migration.swift#L34)
また、 DynamicObject
の定義はこちらです。
中では RLMDynamicGetByName
を読んでいますが、Objective-Cでの実装なので、 エラーをキャッチしてません。(ref: RLMAccessor.mm#L563-L579)
よって、以下のように先に存在を確認してから取得すると良いです。
private extension DynamicObject {
func tryGetValue(for key: String) throws -> Any? {
guard let property = objectSchema[key] else {
throw NSError()
}
return self[property.name]
}
}