Realm の createOrUpdateInRealm:withObject: の挙動調査

先日の「[Realm Tech Talk with JP Simard #2](http://realm.connpass.com/event/11815/)」で質問した挙動のまとめです。

Swiftで作っているアプリで、REST APIのレスポンスをキャッシュしてローカルでDBとして使いたくなって、でもCore Dataも大袈裟でめんどくさいなぁと思っていました。そこで出会ったのが、今話題の[Realm](http://realm.io)です。ドキュメントにある「[REST APIs](http://realm.io/docs/cocoa/0.90.6/#rest-apis)」をみて「これだ！」と思いました。

[createOrUpdateInRealm:withObject:](http://realm.io/docs/cocoa/0.90.6/api/Classes/RLMObject.html#//api/name/createOrUpdateInRealm:withObject:)を使うと、入れ子になったJSONをリレーションを含めDBのレコードにしてくれるようです。以下、その挙動調査です。

# 環境

||バージョン|
|---|---|
|Realm |0.90.6|
|Xcode |6.1.1|
|Deployment Target |iOS 8以上|

Realmはバージョンが0.01上がっただけで大幅に機能が変わったりするらしいので要注意です → http://www.slideshare.net/ryugoo/impression-of-realm-java

# 検証用モデル

検証用にプライマリキーを持つCompany、プライマリキーを持たないLocation、プライマリキーを持ちCompanyとLocationとリレーションがあるPersonの3つのモデルを定義しました。

```swift
class Company: RLMObject {
    dynamic var id = 0
    dynamic var name = ""
    dynamic var phone = ""
   
    override class func primaryKey() -> String! {
        return "id"
    }
}

class Location: RLMObject {
    dynamic var latitude = 0.0
    dynamic var longitude = 0.0
}

class Person: RLMObject {
    dynamic var id = 0
    dynamic var name = ""
    dynamic var company: Company?
    dynamic var location: Location?
   
    override class func primaryKey() -> String! {
        return "id"
    }
}
```

# 検証

どんな挙動をするか、どんな使い方ができるかを調べるために、現実的ではない検証もあります。モバイルアプリ向けのAPIだと1画面1APIなことが多くて、そういう場合は一部の属性が返ってこなかったり、リレーション先まで含めて一回で取得したりするので、そんなケースも想定したりしています。

## 単純な追加

まずは単純にCompanyの追加をやってみます。`withObject:`に指定しているDictionayがJSONをデジリアライズした結果だと思ってください。`Apple`には電話番号がありますが、`Mercari, Inc.`は電話番号を公開していないのでレスポンスに含まれていません。

```swift
let realm = RLMRealm.defaultRealm()

realm.beginWriteTransaction()
Company.createOrUpdateInRealm(realm, withObject: ["id": 0, "name": "Apple", "phone": "1-800-MY-APPLE"])
Company.createOrUpdateInRealm(realm, withObject: ["id": 1, "name": "Mercari, Inc."])
realm.commitWriteTransaction()

println(Company.allObjectsInRealm(realm))
```

結果

```
RLMResults <0x7ff48a4d2450> (
	[0] Company {
		id = 0;
		name = Apple;
		phone = 1-800-MY-APPLE;
	},
	[1] Company {
		id = 1;
		name = Mercari, Inc.;
		phone = ;
	}
)
```

Companyが2つ作られました。`Mercari, Inc.`の電話番号は`phone`を指定していないのでデフォルト値の空文字列になっています。

## 単純な更新

`Apple`の名前が`Apple Inc.`に変わったので、それを受け取ってCompanyを更新してみます。ついでにレスポンスに`phone`を含めないようにしてみました。

```swift
realm.beginWriteTransaction()
Company.createOrUpdateInRealm(realm, withObject: ["id": 0, "name": "Apple Inc."])
realm.commitWriteTransaction()

println(Company.allObjectsInRealm(realm))
```

結果
       
```
RLMResults <0x7fd8f05d6d00> (
	[0] Company {
		id = 0;
		name = Apple Inc.;
		phone = 1-800-MY-APPLE;
	},
	[1] Company {
		id = 1;
		name = Mercari, Inc.;
		phone = ;
	}
)
```

`name`が更新されました。一方`phone`はそのままです。なるほど、SQLでいうUPDATE文みたいな動きをするようです。

## 入れ子になったオブジェクトの追加・更新

Personを追加して、同時にCompanyやLocationを追加・更新してみます。1つ目はすべての情報がそろったレスポンス、2つ目はCompanyの情報が一部ないレスポンス、3つ目はCompanyもLocationもまったく含まれていないレスポンスからの追加です。

```swift
realm.beginWriteTransaction()
Person.createOrUpdateInRealm(realm, withObject: ["id": 0, "name": "Bill Gates", "company": ["id": 2, "name": "Microsoft"], "location": ["latitude": 100, "longitude": 100]])
Person.createOrUpdateInRealm(realm, withObject: ["id": 1, "name": "Steve Jobs", "company": ["id": 0, "name": "Apple Inc."]])
Person.createOrUpdateInRealm(realm, withObject: ["id": 2, "name": "Shintaro Yamada"])
realm.commitWriteTransaction()

println(Person.allObjectsInRealm(realm))
println(Company.allObjectsInRealm(realm))
println(Location.allObjectsInRealm(realm))
```

結果

```
RLMResults <0x7fa879d49790> (
	[0] Person {
		id = 0;
		name = Bill Gates;
		company = Company {
		id = 2;
		name = Microsoft;
		phone = ;
	};
		location = Location {
		latitude = 100;
		longitude = 100;
	};
	},
	[1] Person {
		id = 1;
		name = Steve Jobs;
		company = Company {
		id = 0;
		name = Apple Inc.;
		phone = ;
	};
		location = (null);
	},
	[2] Person {
		id = 2;
		name = Shintaro Yamada;
		company = (null);
		location = (null);
	}
)
RLMResults <0x7fa879d4b4f0> (
	[0] Company {
		id = 0;
		name = Apple Inc.;
		phone = ;
	},
	[1] Company {
		id = 1;
		name = Mercari, Inc.;
		phone = ;
	},
	[2] Company {
		id = 2;
		name = Microsoft;
		phone = ;
	}
)
RLMResults <0x7fa879d4c080> (
	[0] Location {
		latitude = 100;
		longitude = 100;
	}
)
```

結果をみると、`Bill Gates`の追加にともないCompanyやLocationもちゃんと追加されていますが、プライマリキーを持つCompanyのオブジェクトである`Apple Inc.`は`phone`が空文字列になってしまいました。`name`に別の値を指定した場合だと、`name`は指定通り更新されますが、やはり`phone`は空文字列になってしまいます。これはSQLでいうREPLACE文みたいな動きですね。

うむー、[createOrUpdateInRealm:withObject:のドキュメント](http://realm.io/docs/cocoa/0.90.6/api/Classes/RLMObject.html#//api/name/createOrUpdateInRealm:withObject:)には入れ子になったオブジェクトがプライマリキーを持っている場合は、同じように`createOrUpdateInRealm:withObject:`が呼ばれると書いてありますが、そうではないのでしょうか…？

## プライマリキーを持たない子オブジェクトの更新

`id = 0`である`Bill Gates`の`location`を更新してみます。

```swift
realm.beginWriteTransaction()
Person.createOrUpdateInRealm(realm, withObject: ["id": 0, "location": ["latitude": 1234, "longitude": 1234]])
realm.commitWriteTransaction()

println(Person.objectsInRealm(realm, "id = 0"))
println(Location.allObjectsInRealm(realm))
```



結果

```
RLMResults <0x7f89a8d4a4c0> (
	[0] Person {
		id = 0;
		name = Bill Gates;
		company = Company {
		id = 2;
		name = Microsoft;
		phone = ;
	};
		location = Location {
		latitude = 1234;
		longitude = 1234;
	};
	}
)
RLMResults <0x7f89a8d4bd00> (
	[0] Location {
		latitude = 100;
		longitude = 100;
	},
	[1] Location {
		latitude = 1234;
		longitude = 1234;
	}
)
```

この結果は予想できたのですが、以前のLocationが残ったままになっています。こういうのは自動で削除されるとうれしいですね。

# 感想

これらの結果から、`createOrUpdateInRealm:withObject:`を入れ子になったJSONに使うのは、この挙動を考慮したJSONになっていない場合は色々面倒な気がします。マスター的なデータや初期データをさくっとDB化するのには便利そうです。

UPDATE文でもREPLACE文でもどちらの挙動でもよいのですが、子オブジェクトに対しても同じ挙動をしてくれるとまだ使いやすいのになぁと思いました。

まあでも、JSONのキーはスネークケース、Objective-CやSwiftのプロパティはキャメルケースであることが多いと思うので、現実的には`createOrUpdateInRealm:withObject:`は使わずに、プロパティひとつずつセットしていくことになる気もします。