先日の「Realm Tech Talk with JP Simard #2」で質問した挙動のまとめです。
Swiftで作っているアプリで、REST APIのレスポンスをキャッシュしてローカルでDBとして使いたくなって、でもCore Dataも大袈裟でめんどくさいなぁと思っていました。そこで出会ったのが、今話題のRealmです。ドキュメントにある「REST APIs」をみて「これだ!」と思いました。
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つのモデルを定義しました。
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.
は電話番号を公開していないのでレスポンスに含まれていません。
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
を含めないようにしてみました。
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もまったく含まれていないレスポンスからの追加です。
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:のドキュメントには入れ子になったオブジェクトがプライマリキーを持っている場合は、同じようにcreateOrUpdateInRealm:withObject:
が呼ばれると書いてありますが、そうではないのでしょうか…?
プライマリキーを持たない子オブジェクトの更新
id = 0
であるBill Gates
のlocation
を更新してみます。
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:
は使わずに、プロパティひとつずつセットしていくことになる気もします。