LoginSignup
15
11

More than 5 years have passed since last update.

RealmSwiftでのちょっと複雑なマイグレーション(ネスト, List, LinkingObject)

Last updated at Posted at 2018-09-04

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)"
}

firstNamelastName という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
}

このブロックの中で、oldObjectnewObjectMigrationObject という型を持っています。

MigrationObjectoldObject!["lastName"] のように、subscriptでフィールド名を文字列として渡すと、値を取得することができます。このときAny型になります。

そしてこれをStringならStringに、oldObject!["lastName"] as? Stringとすればキャストできます。
じゃあ、これがネストした他のRealmオブジェクトだったりしたときにどうしたらいいのかなと、思うわけです。勘が良い人はもしかしたら気づいているかもしれないですが、僕は全然気づかず、かなり苦戦しました。

※マイグレーションはどんどん追加していったとしても順番に実行していきますが、
もしプロパティ名をkeyPathなどを使ってクラス定義から取得していると、将来そのプロパティを削除したときにコンパイルエラーになるので、文字列のまま使うのが良いと思っています。

Listオブジェクトの読み込み

考えあぐねていたところに、公式サンプルで以下のようなListオブジェクトのマイグレーションの例を見つけました。

let dogs = newObject?["pets"] as? List<MigrationObject>

ref: https://github.com/realm/realm-cocoa/blob/dbd9284440827f085108d5e12c03fdca1db4fd56/examples/ios/swift/Migration/AppDelegate.swift#L94

つまり、マイグレーション中の曖昧なオブジェクトはMigrationObjectにキャストできて、subscriptで階層的に読み込めるのでは?

ネストしたオブジェクトの読み込み

結論から言えば、上の予想のように、ネストしたオブジェクトは、 MigrationObject にキャストできます。

例えば以下のような構造を仮定します。

class Foo: Object {
    dynamic var bar: Bar
}
class Bar: Object {
    dynamic var baz: Baz
}
class Baz: Object {
    dynamic var qux: String
}

このとき、 Fooqux: 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
}

おまけ

MigrationObjectDynamicObject の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]
    }
}

15
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
11