WebサービスのAPIを利用してクライアント側のアプリを作る際に、「まぁRailsとかだと当たり前なんだろうけど、これはやめてほしいなー」と思うようなレスポンスを返してくるAPIを時々見かけます。
あくまでrealm-java観点でなのですが、もしも社内で新規にWebサービスのAPIとアプリを同時に開発する機会があれば、ぜひWebサービス側の開発者に意識して欲しいポイントを幾つか紹介します。
前提
APIのレスポンスを自前でパースしてDBに保存する、という愚直な実装をするのであれば、おそらくこの記事のターゲットではありません。
この記事の狙いとしては「realm-javaで幸せライフを過ごす」ことです。
ん?realm-javaで幸せライフ?なにそれ?
というのを最初に明確にしておきますね。
Realm#createOrUpdateFromJson
と Realm#createOrUpdateAllFromJson
をフル活用したい
realm-javaの便利ポイントとして、JSONオブジェクトを使って差分アップデートができるという点が挙げられます。今回はそれをフル活用することを「realm-javaで幸せライフ」と呼ぶことにしましょう。大げさですか?そうですか。まぁいいでしょう。
とりあえず、差分アップデート?なにそれ?という方むけに少し説明しますと、
たとえばこんな感じのメッセンジャーっぽいアプリを想定しましょう。
上の絵で、「みたよ。ごめんごめん」というメッセージを送信すると同時に、スレッド一覧のほうのサマリも書き換わってほしい、とします。
たとえばMessageThreadのid=9 に紐づく last_messageを、 Message(id:1243) から Message(id:1246) のものに差し替える、ということをするために、
SQLiteだと、そこそこ面倒なクエリを書かないといけないと思いますが、Realmでは
JSONObject obj = new JSONObject("{'id':9,'last_message':{'id':1246, 'text':'みたよ。ごめんごめん'}}");
realm.createOrUpdateObjectFromJson(MessageThread.class, obj)
のように、書き換えたい部分だけをJSONObjectに詰めてcreateOrUpdateすることで、サラッと実現することができます。
ポイントとしては、createOrUpdateなので、
- すでに1246のMessageレコードがあればそれを上書き(textが作られてなかったらtextを代入)する形で紐付け
- まだ1246のレコードがない状態であれば、新規にそのMessageレコードを作った上で紐付け
というのが自動的に行われることです。
一覧/詳細 系のAPIと組むときには、この差分アップデートってむちゃくちゃ便利なんです。
Web API担当者に是非知っておいて欲しい、real-java側の制約
さて、ちょっと前提が長くなってしまいましたが、ここからが本題です。
差分アップデートを使って幸せライフを過ごすためには、 realm-javaの制約に則ったJSONオブジェクトをAPIが返してくれる必要があります。
もちろん、完全にかっちり用意してもらう必要はなくて、多少であれば、APIのレスポンスをアプリ側でちょろっと書きかえて(前処理をして)から Realm#createOrUpdate...FromJson
すればいいのですが、「多少は...」「多少は...」と積み重ねていくと段々と不幸せになっていきますw
なるべくアプリ側でJSONのパースや前処理しないようにするためのポイントを5つ紹介します。
Primary Keyの無いオブジェクトは差分アップデートができない!
{
"id": 1234,
"text": "了解です",
"sender": {
"name": "John Doe",
"icon": "http://example.com/john.doe.png"
}
}
画面に密結合な感じのAPIを作っているとよくあるのですが、プライマリキーがないオブジェクトがあると、差分アップデートが効きません。
たとえば、上記のようなレコードを差分アップデートすると、Userのレコード(name:John Doe, icon:http://example.com/john.doe.png) が新規に作られてそれをメッセージに紐付けるような挙動となります。
そう、**APIを実行するたびにUserのレコードが1つずつ増える(=無限増加するw)**ヤバイ挙動になってしまうのです!
ということで、WebAPIをレビューする際には、「このJSONオブジェクト、IDはないの?」というのを徹底して詰めてあげましょうw
APIによってJSONオブジェクトの構造が異なるとつらい
あんまりこういう例はないとは思うんですが、たとえば
一覧APIでは
{
"id": 9,
"users": [
{
"id": 3,
"name": "JohnDoe"
},
{
"id": 1,
"name": "YusukeIwaki"
}
],
"last_message": {
"id": 1234,
"text": "了解です",
"sender_id": 3
}
}
なのに、詳細APIでは
{
"id": 1234,
"text": "了解です",
"sender": {
"id": 3,
"name": "JohnDoe"
}
}
というように、おなじMessageという型が別の構造(上記の例だと、sender_idだったりsenderだったり)を持っていると、適切に差分アップデートをするためには、アプリ側で前処理が必要になります。
Web APIをレビューする際には「一覧に乗ってくるMessageオブジェクト、こっちのAPIで使われてるやつと構造違っててパースできんのやけど?」と詰めてあげましょう。
(大抵のWebフレームワークにはパーシャルレイアウトとかの仕組みがあり、構造を共通化したほうがメンテナンス工数は減らせることが多いはずなので、アプリだけでなくWebAPI側にもメリットがあります)
ほげほげ_id:xxx
よりも ほげほげ:{id:xxx}
Message has_one User as sender
をどう表現するかという問題なのですが、
たとえば、以下のようなJSONで
{
"id": 9,
"last_message": {
"id": 1234,
"text": "了解です",
"sender_id": 3
}
}
それに対応するモデルを
class Message extends RealmObject {
@PrimaryKey private long id;
private String text;
private long sender_id;
}
のように定義してしまうと、Message送信者の名前やアイコンを取得するたびに、(それらの情報はUserモデルにあるので)
realm.where(User.class).equalsTo("id", message.getSenderId()).findFirst()
のようなクエリを叩かないといけなくなってしまいます。
SQLだとIDを持たせてる方が正規化されてていいし、INNER JOINてきな操作で簡単にUserをあわせて取得ができますが、RealmにはINNER JOINに相当するオペレーションがないので、オブジェクトデータベースを意識した設計にしないといけないのです!
じゃあどうすればいいの?というと、Messageモデルが
class Message extends RealmObject {
@PrimaryKey private long id;
private String text;
private User sender;
}
こんな感じになっていれば、
- Messageから送信者の名前を取得するには
message.getSender().getName()
- 送信者が自分のメッセージを探すには
realm.where(Message.class).equalsTo("sender.id", currentUserId).findAll()
のように、直感的に書けます。ので、Realmてきには
{
"id": 9,
"last_message": {
"id": 1234,
"text": "了解です",
"sender": {
"id": 3
}
}
}
こんな感じのJSONが返ってくると、 なんとか_id
があるよりも100倍くらい嬉しいのです!
List<String>
や List<Integer>
は絶対にやめてー・・・
プライマリキーが必要だ、に通ずるところなのですが、
たとえば User has_many emails
, User has_many users as friends
な関係があったとして、
{
users:[
{
"id": 1,
"emails": [
"yusukeiwaki@example.com"
],
"friend_ids": [
2
]
},
{
"id": 2,
"emails": [],
"friend_ids": []
},
{
"id": 3,
"emails": [
"john.doe@example.com",
"jd@example2.com"
],
"friend_ids": [1, 2]
}
]
}
こんな感じで返されたとします。これ、実はすっごいrealm-javaでは扱いにくいんです。
realm-javaでは、 RealmObjectを継承したクラスをhas_manyで持つことはできますが、そうじゃないものをhas_manyで持つことができません。
class Avatar extends RealmObject {
@PrimaryKey private String url;
private String mime_type;
}
class User extends RealmObject {
@PrimaryKey private long id;
private RealmList<Avatar> avatars; //これはできる
private RealmList<String> emails; //できない! ビルドエラーになる
private RealmList<Long> friend_ids; //できない! ビルドエラーになる
}
なので、 List<String>
とか List<Integer>
なJSONが来ると、必然的に、アプリ側で前処理が必要になってしまいます。
(もしくは、JSONArrayを文字列にしたものをRealmに保持する、とか・・・)
ということで、「 ほげほげ_ids
は絶対にやめて下さい。 ほげほげ:[{id:xx}, {id:yy}]
にしてください」と会話をしましょう。
APIによってJSONオブジェクトの値の意味が異なってもつらい
JSON構造がRealmに親切な設計になっていても、「異なる意味のモノが同じカラム名に入っている」と、少し不都合です。
たとえば一覧APIでは
{
"id": 9,
"last_message": {
"id": 1234,
"text": "メッセージ見た?ちゃん...",
"sender": {
"id": 3,
"name": "JohnDoe"
}
}
}
詳細APIでは
{
"id": 1234,
"text": "メッセージ見た?ちゃんと来てよ?",
"sender": {
"id": 3,
"name": "JohnDoe"
}
}
のように、 textに入っているものがサマリだったり文章全体だったりすると、
たとえば、詳細をcreateOrUpdateしたあとに一覧APIをcreateOrUpdateすると、せっかく取得した文章全体が、サマリに上書きされてしまいます。
サマリはsummary_text、文章全体はtext のように、似て非なるものはカラムを分けましょう。
まとめ
ということで、Realmを使いたいアプリ側には「こんなJSON構造はいやだ」みたいなのが結構あることがわかりましたね!
重要なのは、「うちはこんなのは嫌だ」ってアプリ側の事情だけを押し付けるのではなく、ちゃんと会話することです。API側も、realm-javaを使いたいアプリだけがターゲットとは限りませんので、やっぱり「ほげほげ_idが使いたい」と主張してきたら、適度に折れて、アプリ側で前処理をすることも考えなければなりません。
みなさまぜひWeb API側と何か組むときには、これらのことを会話の上で、快適な realm-javaライフをお過ごしくださいませ。