Murayama blog.

AIの民主化。

Parse - Relations

Relations Guide

リレーションには3つの種類があります。One-to-oneリレーションは1つのオブジェクトを他のオブジェクトに関連付けます。One-to-manyリレーションは1つのオブジェクトに複数のオブジェクトに関連付けます。many-to-manyリレーションは複数のオブジェクト間の複雑な関連を表します。

Relationships in Parse

Parseでリレーションを定義するには4つの方法があります。

  • Pointers (one-to-one、one-to-manyリレーション向け)
  • Arrays (one-to-many、many-to-manyリレーション向け)
  • Parse Relations (many-to-manyリレーション向け)
  • Join Tables (many-to-manyリレーション向け)

One-to-Many Relationships

one-to-manyリレーションのPointers、Arraysを検討する場合はいくつかの検討事項があります。まずリレーションにどれだけの数のオブジェクトが関連するかを検討します。リレーションの"many"サイドにたくさんの数(100件を超えるような)のオブジェクトを含むなら、Pointersを選択してください。100件より少ない数のオブジェクトを扱うならArraysは便利です。とくに、親オブジェクトの取得と同時にすべての子オブジェクトを扱いたいケースで役に立つでしょう。

Using Pointers for One-to-Many Relationships

ゲームアプリケーションを開発しているとしましょう。ゲームではプレイするごとにスコアや結果を記録します。Parse上ではGameオブジェクトの中にこのデータを保存します。ゲームが上手くいくと、個々のプレイヤーはシステム内に数千のGameオブジェクトを格納します。このように、大きな数のリレーションが発生する可能性がある状況においては、Pointersを選択すると良いでしょう。

このゲームアプリケーションでは、Parse UserにすべてのGameオブジェクトを関連付けるものとします。次のように実装できます。

ParseObject game = new ParseObject("Game");
game.put("createdBy", ParseUser.getCurrentUser());

Parse Userによって生成されたGameオブジェクトを取得するクエリは次のようになります。

ParseQuery<ParseObject> gameQuery = ParseQuery.getQuery("Game");
gameQuery.whereEqualTo("createdBy", ParseUser.getCurrentUser());

特定のGameオブジェクトを生成したParse Userを取得したい場合は、createdByキーでルックアップします。

// say we have a Game object
ParseObject game = ...
 
// getting the user who created the Game
ParseUser createdBy = game.getUser("createdBy");

多くの場合において、Pointersはone-to-manyリレーションを実現するための良い選択肢となるでしょう。

Using Arrays for One-to-Many Relations

Arraysはone-to-manyリレーションにおいて、オブジェクトの数が少ないとわかっている場合に有効です。Arraysは、includeKeyパラメータを使うことでメリットが生まれます。パラメータを適用すると"one-to-many"リレーションにおける"one"オブジェクトを取得する際に、"many"に該当するすべてのオブジェクトを取得するこができます。ただし、関連するオブジェクトの数が多くなるとレスポンスタイムがかかってしまいます。

ゲームアプリケーションの場合、プレイヤーが手に入れた武器を記録する必要があるでしょう。この武器の数は1ダース程度に収まるものとします。このようなケースでは、武器の数が極端に多くならないことがわかっています。また、プレイヤーは画面に表示する武器の並び順を指定できるとしましょう。扱う配列の数が少ないとわかっていて、プレイするごとにユーザが並び順を設定したいような場合においてArraysは上手く動作します。

Parse UserオブジェクトにweaponsListフィールドを作成してみましょう。

// let's say we have four weapons
ParseObject scimitar = ...
ParseObject plasmaRifle = ...
ParseObject grenade = ...
ParseObject bunnyRabbit = ...
 
// stick the objects in an array
ArrayList<ParseObject> weapons = new ArrayList<ParseObject>();
weapons.add(scimitar);
weapons.add(plasmaRifle);
weapons.add(grenade);
weapons.add(bunnyRabbit);
 
// store the weapons for the user
ParseUser.getCurrentUser().put("weaponsList", weapons);

Weaponオブジェクトのリストを取得する場合は次のようになります。

ArrayList<ParseObject> weapons = ParseUser.getCurrentUser().get("weaponsList");

one-to-manyリレーションにおいて、"one"オブジェクトをフェッチする際に"many"オブジェクトをすべてフェッチしたいケースもあるでしょう。
Parseはこのようなケースを想定して、ParseQueryにincludeKeyパラメータ(Androidの場合はincludeメソッド)を提供しています。これにより、Parse User取得時にWeaponオブジェクトのリストをフェッチできます。

// set up our query for a User object
ParseQuery<ParseUser> userQuery = ParseUser.getQuery();
 
// configure any constraints on your query...
// for example, you may want users who are also playing with or against you
// tell the query to fetch all of the Weapon objects along with the user
// get the "many" at the same time that you're getting the "one"
userQuery.include("weaponsList");
 
// execute the query
userQuery.findInBackground(new FindCallback<ParseUser>() {
  public void done(List<ParseUser> userList, ParseException e) {
    // userList contains all of the User objects, and their associated Weapon objects, too
  }
});

one-to-manyリレーションの"many"側から、"one"オブジェクトを取得できます。たとえば、特定のWeaponを持っているParseUserを見つけたいなら、次のようにクエリに制約を実装します。

// add a constraint to query for whenever a specific Weapon is in an array
userQuery.whereEqualTo("weaponsList", scimitar);
 
// or query using an array of Weapon objects...
userQuery.whereEqualTo("weaponsList", arrayOfWeapons);

Many-to-Many Relationships

続いてmany-to-manyリレーションシップを見てみましょう。読書アプリケーションを開発しているとして、Book ObjectsとAuthor Objectsのようなモデルがあるとします。Authorは複数のBookを記述し、Bookは複数のAuthorを持ちます。このようなmany-to-manyシナリオでは、Arrays、Parse Relations、Join Tableの生成といった選択肢があります。

これらの選択のポイントは、2つのエンティティ間のリレーションシップにメタデータを保持するかどうかです。メタデータを保持しないなら、Parse RelationやArraysを使うことが好ましいでしょう。一般的に配列を使うと、パフォーマンスが高くなり、より少ないクエリで済むようになります。many-to-manyリレーションシップのいずれかの配列要素数が100を超える場合は、Pointersと同じ理由でParse RelationやJoin Tablesを使う方が好ましいでしょう。

一方、リレーションにメタデータを保持する場合は、セパレートテーブル("Join Table")を用意して両方のリレーションを格納するようにします。これはリレーションについての情報であり、片方のリレーションのオブジェクトについての情報ではないという点に注意してください。メタデータとして、Join Tableに定義するものには次のようなものがあります。

  • リレーションが作成された日時
  • リレーションを作成したユーザ
  • リレーションが参照された回数

Parse Relations

Parse Relationsを使うと、BookといくつかのAuthorオブジェクトの間にリレーションを定義できます。Data Browserでは、Bookオブジェクトにauthorsという名前のリレーション型の列を作成できます。

その後、BookオブジェクトにいくつかのAuthorsオブジェクトを関連付けます。

// let’s say we have a few objects representing Author objects
ParseObject authorOne =
ParseObject authorTwo =
ParseObject authorThree =
 
// now we create a book object
ParseObject book = new ParseObject("Book");
 
// now let’s associate the authors with the book
// remember, we created a "authors" relation on Book
ParseRelation<ParseObject> relation = book.getRelation("authors");
relation.add(authorOne);
relation.add(authorTwo);
relation.add(authorThree);
 
// now save the book object
book.saveInBackground();

あるBookを書いたAuthorのリストを取得するクエリは次のようになります。

// suppose we have a book object
ParseObject book = ...
 
// create a relation based on the authors key
ParseRelation relation = book.getRelation("authors");
 
// generate a query based on that relation
ParseQuery query = relation.getQuery();
 
// now execute the query

あるAuthorの寄稿したBookの一覧を取得したいこともあるでしょう。リレーションの逆を取得するクエリを作成します。

// suppose we have a author object, for which we want to get all books
ParseObject author = ...
 
// first we will create a query on the Book object
ParseQuery<ParseObject> query = ParseQuery.getQuery("Book");
 
// now we will query the authors relation to see if the author object we have 
// is contained therein
query.whereEqualTo("authors", author);

Using Join Tables

リレーションについてもっと知りたくなるようなケースもあるでしょう。たとえば、userが他のuserをフォローするようなuser間のfollowing/followerのリレーションです。これはソーシャルネットワークでポピュラーなものです。私たちのアプリケーションでは、User AがUserBをフォローしたという情報だけでなく、いつフォローを開始したかという情報も知りたいとしましょう。このような情報は、Parse Relationに含むことができません。このような情報を保存するためには、リレーションシップが記録されるセパレートテーブルを作成しなければなりません。このテーブルをFlollowテーブルと呼びましょう。Followテーブルにはfromカラムとtoカラムがあり、これらはParse Userへのポインタを保持します。これらのリレーションと並んで、dateという名前のDate型のカラムを追加します。

2人のユーザのフォロー関係を保存する場合、from、to列、date列を適切に入力して、Followテーブルに行を追加します。

// suppose we have a user we want to follow
ParseUser otherUser = ...
 
// create an entry in the Follow table
ParseObject follow = new ParseObject("Follow");
follow.put("from", ParseUser.getCurrentUser());
follow.put("to", otherUser);
follow.put("date", Date());
follow.saveInBackground();

フォローしている全員を取得したいなら、Followテーブルに次のようなクエリを実行します。

// set up the query on the Follow table
ParseQuery<ParseObject> query = ParseQuery.getQuery("Follow");
query.whereEqualTo("from", ParseUser.getCurrentUser());
 
// execute the query
query.findInBackground(newFindCallback<ParseObject>() {
    public void done(List<ParseObject> followList, ParseException e) {
 
    }
});

同様にフォロワーを取得する場合も簡単です。

// set up the query on the Follow table
ParseQuery<ParseObject> query = ParseQuery.getQuery("Follow");
query.whereEqualTo("to", ParseUser.getCurrentUser());
 
// execute the query
query.findInBackground(newFindCallback<ParseObject>() {
    public void done(List<ParseObject> followList, ParseException e) {
 
    }
});

Using Arrays for Many-to-Many Relations

Arraysは、One-to-Manyリレーションで使用したように、Many-to-Manyリレーションでも使用できます。リレーションの片方のすべてのオブジェクトは、Array型のカラムを持ち、リレーションの他方のいくつかのオブジェクトを保持します。

読書アプリケーションにおけるBookとAuthorオブジェクトを考えてみましょう。Bookオブジェクトは、AuthowオブジェクトのArrayを含むでしょう(authorsのようなカラム名で)。このシナリオではArraysは有効に働きます。なぜなら、Authorオブジェクトが100件を超える可能性が非常に低いからです。このような理由からBookオブジェクトにArrayを定義します。結果的に、Authorは100冊以上のBookと関連付けることもできます。*1

BookとAuthor間のリレーションを保存する例を見てみましょう。

// let's say we have an author
ParseObject author = ...
 
// and let's also say we have an book
ParseObject book = ...
 
// add the song to the song list for the album
book.put("authors", author);

AuthorリストはArrayであるため、Bookのフェッチ時にincludeKey(Androidの場合はincludeメソッド)を使うことができます。そうするとbookのフェッチ時にauthorsも合わせて取得できます。

// set up our query for the Book object
ParseQuery bookQuery = ParseQuery.getQuery("Book");
 
// configure any constraints on your query...
// tell the query to fetch all of the Author objects along with the Book
bookQuery.include("authors");
 
// execute the query
bookQuery.findInBackground(newFindCallback<ParseObject>() {
    public void done(List<ParseObject> bookList, ParseException e) {
    }
});

この時点で、BookオブジェクトからすべてのAuthor Objectsを取得できます。

ArrayList<ParseObject> authorList = book.getList("authors");

最後に、あなたはAuthorを知っていて、そのAuthorの寄稿したすべてのBookオブジェクトを取得したいとします。次のようにクエリに制約をかけます。

// set up our query for the Book object
ParseQuery bookQuery = ParseQuery.getQuery("Book");
 
// configure any constraints on your query...
booKQuery.whereEqualTo("authors", author);
 
// tell the query to fetch all of the Author objects along with the Book
bookQuery.include("authors");
 
// execute the query
bookQuery.findInBackground(newFindCallback<ParseObject>() {
    public void done(List<ParseObject> bookList, ParseException e) {
 
    }
});

One-to-One Relationships

Parseでは、one-to-oneリレーションは、あるオブジェクトを2つのオブジェクトに分離したいケースに使用します。このようなケースは稀ですが、次のようなケースが考えられます。

  • ユーザデータの可視性に制限をかけたい場合。このようなケースでは、他のユーザに公開しても構わないユーザ情報と、他のユーザに公開したくないプライベートな情報に分離します(ACLを使ってプロテクトします)。
  • サイズによるオブジェクトの分離。オブジェクトの最大サイズである128Kを超えてしまうような場合は、データの一部を分離するためにセカンダリオブジェクトを定義します。データを分離しなければならないような大規模なオブジェクトを設計するのは避けるべきでしょう。このような問題を回避できない場合は、Parse Fileによる大規模データの保存を検討してください。


ここまで読んでくれてありがとう。このように複雑になってしまうことは申し訳なく思っています。一般的にデータモデリングは複雑になりがちです。良く言えば、人間関係に比べればまだマシでしょうか。*2

*1:この一文は要るのかな。。

*2:これも要らんかも。。