mBaaSのデータストアはRDBMSとは使い方が異なります。細かな正規化を行うよりも、フィールドの中にオブジェクトや配列を入れてしまうと言った使い方がデータストア向きです。しかし、それでも構造化した方が良い場合に備えて、ポインターやリレーションと言ったデータ型も用意しています。
今回はさらにサブクエリの使い方を紹介します。これを学ぶことで、より柔軟なデータ取得が可能になるでしょう。
あるクラスの結果から別なクラスのデータを取得する
たとえば次のような2つのクラスがあったとします。フィールド名は分かりやすいように日本語にしています。
チームクラス
スポーツチームの情報をまとめたクラスです。
チーム名 | 勝 | 負 | 地域 |
---|---|---|---|
巨人 | 67 | 45 | 東京 |
阪神 | 60 | 53 | 兵庫 |
中日 | 60 | 55 | 愛知 |
DeNA | 56 | 58 | 神奈川 |
広島 | 52 | 56 | 広島 |
ヤクルト | 41 | 69 | 東京 |
対戦クラス
各チームの対戦結果をまとめたクラスです。
日程 | 勝チーム | 負チーム | 勝ち点 | 失点 |
---|---|---|---|---|
2020/08/01 | 巨人 | 広島 | 11 | 3 |
2020/08/01 | 中日 | ヤクルト | 3 | 1 |
2020/08/01 | DeNA | 阪神 | 7 | 3 |
2020/08/02 | 広島 | 巨人 | 9 | 2 |
2020/08/02 | 中日 | ヤクルト | 0 | 0 |
2020/08/02 | 阪神 | DeNA | 3 | 1 |
この時、データの持たせ方として2つの方法が考えられます。
- 勝チーム、負チームのチームをそれぞれチームクラスへのポインターとして持つ
- 勝チーム、負チームの名前を文字列で持つ
利点、欠点は次のようになります。これはデータの特性や更新頻度などに合わせて選べば良いでしょう。
ポインターを使った場合
- 利点:ポインターの場合、チームクラスの名前を変更しても対戦クラス側には影響しない
- 欠点:データ取得時にincludeで指定できるのは1つのフィールドだけ。管理画面上で分かりづらい
文字列を使った場合
- 利点:管理画面上で見やすい。データ量はポインターより少しだけ小さい(文字数次第)
- 欠点:チームクラスで名前を変更してしまうと情報の整合性がとれなくなる
対戦情報からチーム情報を取得する際の問題
ではここから本題です。対戦クラスは日々の対戦結果が記録されますので、データ量が増えていくのは間違いありません。そして、対戦クラスでデータを絞り込んで、該当するチーム情報を取得したいことはよくあります。
この時、APIリクエストが分かれてしまうのは想像が付きます。
- 対戦クラスを検索
- その結果からチームクラスを検索
このようにAPIリクエストを分けてしまうのは、次のような問題につながります。
- ネットワークリクエストが増える分、ユーザは結果表示を待たされてしまう
- APIリクエスト回数が増える
対戦クラスの結果をループ処理で回したりしていると、なおさら時間がかかります。改善として、チーム名を配列化して、in検索を利用する方法が考えられます。とはいえ、この場合でもAPIリクエストは2回必要です。
サブクエリーを使う
そこでお勧めしたいのがサブクエリーの利用です。サブクエリーは次の2種類があります。
- クエリー検索結果の特定のフィールドの値で検索する
- クエリー検索結果のポインターで検索する
値とポインター、どちらも利用できますので、入っている値に応じて使い分けてください。
値で検索する
値を使った場合です。今回はJavaScript SDKでの実装例です。分かりやすいように日本語を使っていますが、本来クラス名とフィールド名は英語のみです。
以下は60勝以上しているチームの対戦成績を抽出しています。
const Team = ncmb.DataStore('チームクラス'); Team.greaterThanOrEqualTo('勝', 60); // 60勝以上しているチームを対象 const Match = ncmb.DataStore('対戦クラス'); const matches = Match .select('勝チーム', 'チーム名', Team) .fetchAll();
ポインターで検索する
同様にポインターを使った場合です。
const Team = ncmb.DataStore('チームクラス'); Team.greaterThanOrEqualTo('勝', 60); // 60勝以上しているチームを対象 const Match = ncmb.DataStore('対戦クラス'); const matches = await Match .inQuery('勝ちチーム', Team) .fetchAll();
ループする場合
ちなみにループ処理で書くと次のようになるでしょう(以下は文字列でチーム名を持っている場合)。なおループ処理でasync/awaitを使うのはよくないので、Promise.allを使うのが基本です。今回の例でいえば、60勝以上しているチームが3チームなので、全部で4回APIリクエストが発生します。
const Team = ncmb.DataStore('チームクラス'); const teams = await Team .greaterThanOrEqualTo('勝', 60) // 60勝以上しているチームを対象 .fetchAll(); const promise = [] teams.forEach(async team => { const Match = ncmb.DataStore('対戦クラス'); promise.push(await Match .equalTo('勝ちチーム', team.get('name')) .fetchAll()); }); const matches = (await Promise.all(promises)).flat()
in検索を使った場合は次のようになります。この場合、APIリクエストは2回になります。
const Team = ncmb.DataStore('チームクラス'); const teams = await Team .greaterThanOrEqualTo('勝', 60) // 60勝以上しているチームを対象 .fetchAll(); const Match = ncmb.DataStore('対戦クラス'); const matches = await Match .in('勝ちチーム', teams.map(t => t.get('name'))) .fetchAll());
Tips
ポインターの場合はinQueryでサブクエリーの結果を利用できます。値の場合は select で、サブクエリーのどのフィールドを指定するかを指定します。
サブクエリーの利点
サブクエリーを使う利点はなんと言っても結果の高速性です。一度データを取得して、その結果からまた別な結果を取得するのに比べて高速に取得できます。また、APIリクエスト数を節約できるのも魅力でしょう。
まとめ
サブクエリーはデータストアの検索結果で別なクラスの検索につなげる時に利用できる可能性があります。サブクエリーを使うことで、より高速かつAPIリクエスト数を減らせる可能性がありますので、ぜひ使ってみてください。