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

# tl;dr

Realmのマイグレーション中のオブジェクトはプリミティブ型かMigrationObject型にキャストして値を読み込む。

# モチベ

Realmのマイグレーションについて調べると、簡単な例が載っている。

https://realm.io/docs/swift/latest/#migrations

- プロパティを消す、モデルを追加する場合は、migration blockを定義するだけで良い
- プロパティをリネームするときはmigration.renamePropertyを使うとはやい
- enumerateObjectsでオブジェクトを列挙して、それぞれに対して値を書き込める

３つ目の列挙について、載っているサンプルはこんな感じ。

```swift
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というクラスについて、以下のようにフィールドを変化させる場合を考えます。

```swift
// 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

```swift
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オブジェクトのマイグレーションの例を見つけました。

```swift
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` にキャストできます。

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

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

このとき、 `Foo` に `qux: String` というプロパティを付け替えたいと思ったら、以下のようにします。

```swift
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なので、上の章で書いたとおりです。

```swift
class Person: Object {
    // ... other property declarations
    let dogs = List<Dog>()
}
class Dog: Object {
    // ... other property declarations
    let owners = LinkingObjects(fromType: Person.self, property: "dogs")
}
```

```swift
migration.enumerateObjects(ofType: Dog.className()) { oldObject, newObject in
    // migrations
}
```

このとき、 `oldObject?["owners"]` を読み込もうとするとクラッシュします。
が、 `newObject?["owners"]` は読み込むことができます。

そしてその型は、 `RLMResults<MigrationObject>` になります。 RLMResult型を使うためには `RealmSwift` だけでなく `Realm` もimportする必要があります。

よって、以下のように書くことができます。

```swift
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](https://github.com/realm/realm-cocoa/blob/dbd9284440827f085108d5e12c03fdca1db4fd56/RealmSwift/Migration.swift#L34))

また、 `DynamicObject` の定義は[こちら](https://github.com/realm/realm-cocoa/blob/dbd9284440827f085108d5e12c03fdca1db4fd56/RealmSwift/Object.swift#L359)です。

中では `RLMDynamicGetByName` を読んでいますが、Objective-Cでの実装なので、 エラーをキャッチしてません。(ref: [RLMAccessor.mm#L563-L579](https://github.com/realm/realm-cocoa/blob/dbd9284440827f085108d5e12c03fdca1db4fd56/Realm/RLMAccessor.mm#L563-L579))

よって、以下のように先に存在を確認してから取得すると良いです。

```swift
private extension DynamicObject {
    func tryGetValue(for key: String) throws -> Any? {
        guard let property = objectSchema[key] else {
            throw NSError()
        }
        return self[property.name]
    }
}

```