クエリ
リアルタイムクエリサポートを備えた、複数のデータベースを横断するデータの結合を含む、任意のソースからのデータに対して詳細なクエリを作成します。
Squid はストリームとオブザーバブルの処理に RxJs を使用しています。RxJS やストリーミング更新の詳細については、RxJs documentation をご覧ください。
ドキュメントをクエリする際、スナップショットを一度だけ取得する方法と、スナップショットのストリームを消費する方法の選択肢があります。
- スナップショットを取得する場合、ドキュメントの最新バージョンが
Promiseとして返されます。 - スナップショットのストリームを取得する場合、クエリ結果が変化するたびに新しいスナップショットを発行する RxJs の
Observableが返されます。
Squid Client SDK のスナップショットおよびデータストリームの使用により、最小限のオーバーヘッドとセットアップでデータソースからリアルタイム更新を継続的に受信できます。
単一のドキュメントのクエリ
単一のドキュメントをクエリするには、document reference の snapshot または snapshots メソッドを呼び出してください。その後、data ゲッターを使用してドキュメント内のデータにアクセスできます:
const docRef = await squid.collection<User>('users').doc('user_id').snapshot();
if (docRef) {
console.log(docRef.data);
}
また、snapshots メソッドを使用してこのドキュメントの変更を subscribe することも可能です。ドキュメントが変化するたびに、observable は新しい値を発行します。
squid
.collection<User>('users')
.doc('user_id')
.snapshots()
.subscribe((docRef) => {
console.log(docRef.data);
});
Squid のバックエンドセキュリティルールでは、各クエリを実行できるユーザーを制御できます。これらのルールは、クエリを含む QueryContext をパラメータとして受け取ります。バックエンドセキュリティを設定して、特定のコレクションまたはデータベースコネクタへの読み取りアクセスを制限してください。データベースの読み書き権限を制限する方法については、docs on security rules をご覧ください。
コレクションからの複数ドキュメントのクエリ
コレクションからドキュメントをクエリする場合、query メソッドを使用してクエリを構築します。
Squid では、snapshot メソッドを使用して単一のクエリ結果を取得するか、snapshots メソッドを使用してクエリ結果のストリームを取得するかのオプションが用意されています。この方法でクエリ結果のストリームを取得する場合、クエリ結果が変化するたびに observable は新しい値を発行します。
以下は、18歳以上のすべての admin を返す単一のクエリスナップショットを取得する例です:
const users = await squid.collection<User>('users').query().gt('age', 18).eq('role', 'admin').snapshot();
次の例は、snapshots メソッドを使用してストリーミングクエリ結果を受信する例です:
const usersObs = squid.collection<User>('users').query().gt('age', 18).eq('role', 'admin').snapshots();
/* users observable を subscribe し、新しい値が受信されるたびにデータをログに出力する */
usersObs.subscribe((users) => {
console.log(
'Got new snapshot:',
users.map((user) => user.data)
);
});
Query は、changes メソッドを使用して変更のストリームを返すこともサポートしています。このメソッドによって返される observable には、コレクションに対して行われた変更を追跡する3種類の配列が含まれます:
inserts: コレクションへの新規挿入のドキュメントリファレンスを含みますupdates: コレクション内の更新のドキュメントリファレンスを含みますdeletes: 削除されたドキュメントのデータを含みます
const usersObs = squid.collection<User>('users').query().gt('age', 18).eq('role', 'admin').changes();
usersObs.subscribe((changes) => {
// 18歳以上の admin コレクションへの新規挿入をログに出力
console.log(
'Inserts:',
changes.inserts.map((user) => user.data)
);
// admin への更新をログに出力
console.log(
'Updates:',
changes.updates.map((user) => user.data)
);
// deletes 配列には、ドキュメントリファレンスを伴わない実際の削除データが含まれます
console.log('Deletes:', changes.deletes);
});
ドキュメントリファレンスのデリファレンス
コレクションからデータをクエリすると、ドキュメントリファレンスが返されます。ドキュメントからデータにアクセスするには、data ゲッターを呼び出してください。
const usersObs = squid
.collection<User>('users')
.query()
.snapshots()
.pipe(map((user) => user.data));
data ゲッターを呼び出すことなく直接ドキュメントデータを取得するには、dereference メソッドを呼び出します。
const usersDataObs = squid.collection<User>('users').query().dereference().snapshots();
コレクションおよびコネクタ間でのデータの結合
Squid では、複数のクエリを結合し、結果の変化を監視することができます。この機能は、異なるデータソースからデータを結合する能力によってさらに強力になります。
例えば、dept コレクションと employees コレクションをクエリで結合し、18歳以上のすべての従業員とその部署を返すことができます:
const departmentCollection = squid.collection<Dept>('dept');
const employeeCollection = squid.collection<Employee>('employees');
const employeeQuery = employeeCollection.query().gt('age', 18);
const joinObs = departmentCollection
.joinQuery('d')
.join(employeeQuery, 'e', {
left: 'id',
right: 'deptId',
})
.snapshots();
joinObs.subscribe((joinResult) => {
// ここで join 結果を利用
});
上記のコードでは、各クエリにエイリアスが割り当てられています。dept コレクションには d、employees コレクションには e が割り当てられており、ジョイン条件は employees コレクションの deptId フィールドと dept コレクションの id フィールドを結合しています。
この例は組み込みデータベースから2つのコレクションを結合する例ですが、コレクションリファレンスにコネクタ ID を指定することで、別々のデータベースコネクタからの結合も可能です。例えば、コネクタ ID が connectorA と connectorB の2つのデータベースコネクタがある場合、以下のようにコネクタ ID を追加することで同じ結合を実行できます:
const departmentCollection = squid.collection<Dept>('dept', 'integrationA');
const employeeCollection = squid.collection<Employee>('employees', 'integrationB');
デフォルトでは、Squid は left join を実行します。つまり、この例では、部署が空であっても全ての部署が結合結果に含まれます。例えば、部署 A には18歳以上の人が2人いるが、部署 B には18歳以上の人がいない場合、ジョインクエリの結果は次のようになります:
type ResultType = Array<{
d: DocumentReference<Dept>;
e: DocumentReference<Employee> | undefined;
}>;
joinResult ===
[
{ d: { data: { id: 'A' } }, e: { data: { id: 'employee1' } } },
{ d: { data: { id: 'A' } }, e: { data: { id: 'employee2' } } },
{ d: { data: { id: 'B' } }, e: undefined },
];
undefined な結果を除外するには inner join を実行してください。inner join を実行するには、join メソッドの第4パラメータとして { isInner: true } を渡します。以下の例では、部署 B は返されません:
const departmentCollection = squid.collection<Dept>('dept');
const employeeCollection = squid.collection<Employee>('employees');
const employeeQuery = employeeCollection.query().gt('age', 18);
const joinObs = departmentCollection
.joinQuery('d')
.join(
employeeQuery,
'e',
{
left: 'id',
right: 'deptId',
},
{
isInner: true,
}
)
.snapshots();
joinObs.subscribe((joinResult) => {
// ここで join 結果を利用
});
type ResultType = Array<{
d: DocumentReference<Dept>;
e: DocumentReference<Employee>; // `| undefined` は含まれません
}>;
joinResult ===
[
{ d: { data: { id: 'A' } }, e: { data: { id: 'employee1' } } },
{ d: { data: { id: 'A' } }, e: { data: { id: 'employee2' } } },
];
3つのコレクション間でジョインを行うには、クエリにさらに 1 つのジョインを追加します。例えば、employees、dept、company の各コレクションがある場合、次のようなジョインを実行できます:
const departmentCollection = squid.collection<Dept>('dept');
const employeeCollection = squid.collection<Employee>('employees');
const employeeQuery = employeeCollection.query().gt('age', 18);
const companyQuery = squid.collection<Company>('company').query();
const joinObs = departmentCollection
.joinQuery('d')
.join(employeeQuery, 'e', {
left: 'id',
right: 'deptId',
})
.join(companyQuery, 'c', {
left: 'companyId',
right: 'id',
})
.snapshots();
上記の例では、employees と dept を結合し、さらに dept と company を結合しています。結果のオブジェクトは以下の型となります:
type Result = Array<{
e: DocumentReference<Employee>;
d: DocumentReference<Dept> | undefined;
c: DocumentReference<Company> | undefined;
}>;
ジョインの左側を選択する
ジョインの左側を選択するには、join メソッドの第4パラメータ内の options オブジェクトで leftAlias を渡します。例えば、employee と dept、および employee と company をジョインする場合、以下のように company コレクションのジョイン左側を e に設定します:
const departmentCollection = squid.collection<Dept>('dept');
const employeeCollection = squid.collection<Employee>('employees');
const employeeQuery = employeeCollection.query().gt('age', 18);
const companyQuery = squid.collection<Company>('company').query();
const joinObs = departmentCollection
.joinQuery('d')
.join(employeeQuery, 'e', {
left: 'id',
right: 'deptId',
})
.join(companyQuery, 'c', {
left: 'companyId',
right: 'id'
},
{ leftAlias: 'e' }
)
.snapshots();
ジョイン結果のグループ化
繰り返しのエントリをまとめるために、ジョイン結果をグループ化するには、ジョインクエリで grouped() メソッドを呼び出します。例えば、employees と dept をジョインし、さらに dept と company をジョインする場合、grouped() を使用して各ユーザーにつき 1 つのエントリのみを受信します。
const departmentCollection = squid.collection<Dept>('dept');
const employeeCollection = squid.collection<Employee>('employees');
const employeeQuery = employeeCollection.query().gt('age', 18);
const companyQuery = squid.collection<Company>('company').query();
const joinObs = departmentCollection
.joinQuery('d')
.join(employeeQuery, 'e', {
left: 'id',
right: 'deptId',
})
.join(companyQuery, 'c', {
left: 'companyId',
right: 'id',
})
.grouped()
.snapshots();
grouped() メソッドを使用しない場合、このクエリは以下の型の結果を返します:
type Result = Array<{
// 同じユーザーが複数回返される可能性があります(各部署や会社ごとに)
e: DocumentReference<Employee>;
// 同じ部署が複数回返される可能性があります(各会社ごとに)
d: DocumentReference<Dept> | undefined;
c: DocumentReference<Company> | undefined;
}>;
grouped() メソッドを使用すると、クエリは以下の型の結果を返します:
type Result = Array<{
e: DocumentReference<Employee>;
d: Array<{
d: DocumentReference<Dept>;
c: Array<DocumentReference<Company>>;
}>;
}>;
また、grouped() メソッドを dereference() と組み合わせることで、DocumentReference を除いた結果データを取得できます。
const departmentCollection = squid.collection<Dept>('dept');
const employeeCollection = squid.collection<Employee>('employees');
const employeeQuery = employeeCollection.query().gt('age', 18);
const companyQuery = squid.collection<Company>('company').query();
const joinObs = departmentCollection
.joinQuery('d')
.join(employeeQuery, 'e', {
left: 'id',
right: 'deptId',
})
.join(companyQuery, 'c', {
left: 'companyId',
right: 'id',
})
.grouped()
.dereference()
.snapshots();
このクエリは以下の型の結果を返します:
type Result = Array<{
e: Employee;
d: Array<{
d: Dept;
c: Array<Company>;
}>;
}>;
制限とソート
Squid は、アプリケーションのパフォーマンス最適化やユーザーエクスペリエンス向上に役立つよう、クエリのソートおよび制限機能を提供しています。
クエリをソートするには、sortBy メソッドを使用し、ソート対象のフィールドと(オプションで)ソート順を指定します。ソート順が指定されない場合、クエリは昇順がデフォルトとなります。
以下は、年齢で降順にソートするクエリの例です:
const users = await squid.collection<User>('users').query().gt('age', 18).eq('role', 'admin').sortBy('age', false).snapshot();
クエリで返される結果の数を制限するには、limit メソッドを使用し、返される結果の最大数を指定します。制限が指定されない場合、クエリはデフォルトで 1000 を返し、これが最大値となります。
以下は、クエリを 10 件に制限する例です:
const users = await squid.collection<User>('users').query().gt('age', 18).eq('role', 'admin').limit(10).snapshot();
同じクエリ内でソートと制限を組み合わせることも可能です。以下はその例です:
const users = await squid.collection<User>('users').query().gt('age', 18).eq('role', 'admin').sortBy('age', false).limit(10).snapshot();
もう一つの機能として limitBy があります。これは、fields 内の各フィールドで同じ値を持つ最初の limit 件のドキュメントのみを返します。これにより、「各都市の最も若い 5 人のユーザーを返す」といったクエリが可能になります(例を参照)。
const users = await squid.collection<User>('users').query().sortBy('state').sortBy('city').sortBy('age').sortBy('name').limitBy(5, ['state', 'city']).snapshot();
返されるクエリは、各 state と city の組み合わせにつき最大 5 件のドキュメントとなります。つまり、このクエリは都市ごとに年齢が同点の場合は名前で順序付けられた最も若い 5 人のユーザーを返します。
limitBy 句にあるすべての fields は、クエリの最初の n 個の sortBy 句に現れる必要があります(n はフィールド数です)。上記の例では、state と city(limitBy に現れるもの)がクエリ内に sortBy() として存在し、それらの sortBy は age や name より前に記述されていなければなりません。
ページネーション
Squid は、クエリ上の paginate メソッドを通じて、クエリ結果をページネーションする強力な方法を提供します。
paginate メソッドは、以下のプロパティを含む PaginationOptions オブジェクトをパラメータとして受け取ります:
- pageSize: デフォルトが 100 の数値
- subscribe: クエリに対するリアルタイム更新の購読を行うかどうかを示すブール値。デフォルトは
trueです。
呼び出すと、paginate メソッドは以下のプロパティを持つ Pagination オブジェクトを返します:
- observeState: 現在のページネーション状態(
PaginationStateと定義)を発行する observable - next: 次のページ状態を返す promise を解決する関数
- prev: 前のページ状態を返す promise を解決する関数
- waitForData: 読み込みプロセスが完了した際に、現在のページネーション状態を返す promise を返す関数
- unsubscribe: ページネーションオブジェクトに対して、クエリの購読解除および内部状態のクリアを指示する関数
PaginationState オブジェクトは、以下のプロパティを含みます:
- data: 現在のページのデータを保持する配列
- hasNext: 次のページが存在するかどうかを示すブール値
- hasPrev: 前のページが存在するかどうかを示すブール値
- isLoading: ページネーションがデータの読み込み中であるかを示すブール値
以下はページネーションの使用例です:
const pagination = (this.query = squid.collection<User>('users').query().gt('age', 18).eq('role', 'admin').sortBy('age', false).dereference().paginate({ pageSize: 10 }));
let data = await pagination.waitForData();
console.log(data); // 最初のページのデータを出力
data = await pagination.next();
console.log(data); // 2ページ目のデータを出力
pagination.unsubscribe();
要求に応じて、ページネーションオブジェクトは最新状態を維持するためにクエリに対してリアルタイム更新の購読を行います。つまり、PaginationState オブジェクトはデータが変更されるたびに最新の状態に更新されます。
リアルタイム更新を維持したり、サーバー更新に起因する空のページの受信といったエッジケースに対応するため、ページネーションオブジェクトはページ表示の際に複数回クエリを実行する場合があります。
クエリヘルパー
ヘルパー関数を使用する以外にも、where 関数を使用してクエリを構築することができます。where 関数は、クエリするフィールド、使用するオペレーター、そして比較する値の 3 つのパラメータを受け取ります。
| Helper | Before | After | Explanation |
|---|---|---|---|
| eq | where('foo', '==', 'bar') | eq('foo', 'bar') | foo が bar と等しいかチェックします |
| neq | where('foo', '!=', 'bar') | neq('foo', 'bar') | foo が bar と等しくないかチェックします |
| in | where('foo', 'in', ['bar']) | in('foo', ['bar']) | foo が指定されたリストに含まれているかチェックします |
| nin | where('foo', 'not in', ['bar']) | nin('foo', ['bar']) | foo が指定されたリストに含まれていないかチェックします |
| gt | where('foo', '>', 'bar') | gt('foo', 'bar') | foo が bar より大きいかチェックします |
| gte | where('foo', '>=', 'bar') | gte('foo', 'bar') | foo が bar 以上かチェックします |
| lt | where('foo', '<', 'bar') | lt('foo', 'bar') | foo が bar より小さいかチェックします |
| lte | where('foo', '<=', 'bar') | lte('foo', 'bar') | foo が bar 以下かチェックします |
| like (case sensitive) | where('foo', 'like_cs', '%bar%') | like('foo', '%bar%') | (大文字小文字区別)foo がパターン %bar% と一致するかチェックします |
| like | where('foo', 'like', '%bar%') | like('foo', '%bar%', false) | (大文字小文字非区別)foo がパターン %bar% と一致するかチェックします |
| notLike (case sensitive) | where('foo', 'not like_cs', '%bar%') | notLike('foo', '%bar%') | (大文字小文字区別)foo がパターン %bar% と一致しないかチェックします |
| notLike | where('foo', 'not like', '%bar%') | notLike('foo', '%bar%', false) | (大文字小文字非区別)foo がパターン %bar% と一致しないかチェックします |
| arrayIncludesSome | where('foo', 'array_includes_some', ['bar']) | arrayIncludesSome('foo', ['bar']) | foo 配列が指定された値の一部を含むかチェックします |
| arrayIncludesAll | where('foo', 'array_includes_all', ['bar']) | arrayIncludesAll('foo', ['bar']) | foo 配列が指定された値を全て含むかチェックします |
| arrayNotIncludes | where('foo', 'array_not_includes', ['bar']) | arrayNotIncludes('foo', ['bar']) | foo 配列が指定された値を含まないかチェックします |