クエリ
リアルタイムクエリをサポートし、複数データベース間のデータ結合も含め、あらゆるソースのデータに対して細粒度のクエリを作成できます。
クエリを使う理由
データベースから特定のサブセットを取得したい、条件でフィルタしたい、結果をソートしたい、コレクション間でデータを結合したい、あるいはリアルタイム更新を購読したいことがあります。クエリAPIは、チェーン可能で型安全なインターフェースでこれらすべてを実現します。
概要
Squid はストリームと observable の処理に RxJs を使用します。RxJS とストリーミング更新については、 RxJs ドキュメントで詳しく学べます。
ドキュメントをクエリする際、単一のスナップショット、またはスナップショットのストリームを消費するかを選べます。
- スナップショットを消費する場合、ドキュメントの最新バージョンを
Promiseとして受け取ります。 - スナップショットのストリームを消費する場合、結果は RxJs の
Observableとなり、クエリ結果が変化するたびに新しいスナップショットが emit されます。
Squid Client SDK におけるスナップショットおよびデータストリームの仕組みにより、最小限のオーバーヘッドとセットアップで データソースからリアルタイム更新を継続的に受け取れます。
クイックスタート
collection reference に対してフィルタメソッドをチェーンしてクエリを構築し、snapshot() を呼び出して結果を取得します。
interface User {
id: string;
name: string;
age: number;
role: string;
}
// Query all admin users over 18
const admins = await squid
.collection<User>('users')
.query()
.gt('age', 18)
.eq('role', 'admin')
.snapshot();
// Access data from each result
for (const userRef of admins) {
console.log(userRef.data.name);
}
コアコンセプト
単一ドキュメントのクエリ
単一ドキュメントをクエリするには、document reference に対して snapshot または snapshots メソッドを呼び出します。結果はドキュメントデータそのもの(型 T | undefined)です。
const user = await squid.collection<User>('users').doc('user_id').snapshot();
if (user) {
console.log(user.name);
}
または、snapshots メソッドを使ってこのドキュメントの変更を subscribe できます。ドキュメントが変化するたびに、observable が最新データを emit します。
squid
.collection<User>('users')
.doc('user_id')
.snapshots()
.subscribe((user) => {
if (user) {
console.log(user.name);
}
});
Squid の backend security rules により、どのユーザーがどの特定クエリを実行できるかを制御できます。これらのルールは
パラメータとして QueryContext(クエリを含む)を受け取ります。特定コレクションや database connector に対する読み取りアクセスを制限するように backend security を設定してください。データベースの読み書き権限を制限する方法は、security rules のドキュメントを参照してください。
コレクションから複数ドキュメントをクエリする
コレクションからドキュメントをクエリする場合、query メソッドでクエリを構築します。
Squid には、snapshot メソッドで単一のクエリ結果を消費する方法と、snapshots メソッドでクエリ結果のストリームを消費する方法があります。この方法でストリームを消費すると、クエリ結果が変わるたびに observable が新しい値を emit します。
以下は、年齢が 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();
/* Subscribe to the users observable, and log the data each time a new value is received */
usersObs.subscribe((users) => {
console.log(
'Got new snapshot:',
users.map((user) => user.data)
);
});
Query は changes メソッドによって変更のストリームを返すこともサポートします。このメソッドが返す observable には、コレクションに対する変更を追跡する 3 種類の配列が含まれます。
inserts: コレクションに新規挿入されたドキュメントの document reference を含みますupdates: コレクションで更新されたドキュメントの document reference を含みますdeletes: 削除されたドキュメントのデータを含みます
const usersObs = squid
.collection<User>('users')
.query()
.gt('age', 18)
.eq('role', 'admin')
.changes();
usersObs.subscribe((changes) => {
// Logs all new insertions into the collection of admins who are 18+
console.log(
'Inserts:',
changes.inserts.map((user) => user.data)
);
// Logs new updates in the collection where the user is now an admin who is 18+
console.log(
'Updates:',
changes.updates.map((user) => user.data)
);
// The deletes array contains the actual deleted data without a doc reference
console.log('Deletes:', changes.deletes);
});
document reference のデリファレンス(参照解決)
コレクションからデータをクエリすると、document reference を受け取ります。ドキュメントのデータには、data getter を呼び出してアクセスできます。
const usersObs = squid
.collection<User>('users')
.query()
.snapshots()
.pipe(map((user) => user.data));
data getter を呼ばずにドキュメントデータを直接受け取るには、dereference メソッドを呼び出します。
const usersDataObs = squid
.collection<User>('users')
.query()
.dereference()
.snapshots();
コレクションとコネクタをまたいだデータ結合(Join)
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) => {
// Use the join result here
});
上記コードでは、それぞれのクエリにエイリアスを割り当てています。dept コレクションには d、employees コレクションには e です。結合条件は、employees コレクションの deptId フィールドと dept コレクションの id フィールドを結合します。
この例では組み込みデータベース内の 2 つのコレクションを結合していますが、collection reference に connector ID を指定すれば、別々の database connector 間でも join できます。たとえば connector ID が connectorA と connectorB の 2 つの database connector がある場合、次のように connector ID を追加することで同じ join を実行できます。
const departmentCollection = squid.collection<Dept>('dept', 'connectorA');
const employeeCollection = squid.collection<Employee>('employees', 'connectorB');
デフォルトでは、Squid は left join を実行します。つまり、この例では空の部署も含め、すべての部署が join 結果に含まれます。たとえば、部署 A に 18 歳超の人が 2 人いるが、部署 B には 18 歳超の人がいないとします。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' } } },
{ 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) => {
// Use the join result here
});
type ResultType = Array<{
d: DocumentReference<Dept>;
e: DocumentReference<Employee>; // Note no `| undefined`
}>;
joinResult ===
[
{ d: { data: { id: 'A' } }, e: { data: { id: 'employee1' } } },
{ d: { data: { id: 'A' } }, e: { data: { id: 'employee2' } } },
];
3 つのコレクション間で join を書くには、クエリに join をもう 1 つ追加します。たとえば employees、dept、company のコレクションがある場合、次の join を実行できます。
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 の左側を選ぶ
join の左側を選ぶには、join メソッドの第 4 引数である options オブジェクトに leftAlias を渡します。たとえば employee と dept、および employee と company を結合するには、次のように company コレクションの join における左側を選びます。
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();
join 結果のグルーピング
join 結果をグルーピングして重複エントリをまとめるには、join クエリに対して 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<{
// The same user may return more than once (one for each department and company)
e: DocumentReference<Employee>;
// The same department may return more than once (one for each company)
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>;
}>;
}>;
OR クエリ
同じコレクションに対する複数クエリを、collection reference の or() メソッドで結合します。結果は重複排除され、最初のクエリのソート順でソートされます。
const usersCollection = squid.collection<User>('users');
const adminQuery = usersCollection.query().eq('role', 'admin');
const recentQuery = usersCollection.query().gt('createdAt', '2024-01-01');
// Returns users who are admins OR were created after 2024-01-01
const results = await usersCollection.or(adminQuery, recentQuery).snapshot();
or() に渡すクエリはすべて同一のソート順である必要があります。少なくとも 1 つのクエリを指定する必要があります。
制限(Limit)とソート(Sorting)
Squid ではクエリのソートと制限が可能で、アプリケーションのパフォーマンス最適化やユーザー体験の改善に役立ちます。
クエリをソートするには sortBy メソッドを使用し、ソート対象フィールドと、必要に応じてソート順を指定します。ソート順が指定されない場合、クエリはデフォルトで昇順になります。
以下は、年齢で降順にソートする例です。
const users = await squid
.collection<User>('users')
.query()
.gt('age', 18)
.eq('role', 'admin')
.sortBy('age', false)
.snapshot();
クエリの返却件数を制限するには limit メソッドを使用し、返す最大件数を指定します。limit を指定しない場合はデフォルトで 1000 となり、これが許可される最大値でもあります。1000 件を超えて取得するには、pagination を使用してください。
以下は、結果を 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();
もう 1 つの機能として 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 人返します(年齢が同じ場合は name でタイブレーク)。
limitBy 句の fields はすべて、クエリの先頭 n 個の sortBy 句に登場している必要があります(n はフィールド数)。上記例では、state と city(limitBy に登場)がクエリ内で sortBy() されている必要があり、さらにそれらの sortBy は age や name より前でなければなりません。
フィールド投影(Field projection)
projectFields メソッドを使うと、クエリから返すフィールドを指定でき、必要なデータだけを取得することで帯域を削減しパフォーマンスを向上できます。
const users = await squid
.collection<User>('users')
.query()
.projectFields(['name', 'age'])
.dereference()
.snapshot();
// Results contain only the projected fields: name and age
フィールド投影は、リアルタイム購読、ドット記法によるネストフィールド、その他すべてのクエリメソッドと併用できます。検証ルール、購読時の挙動、エラーハンドリングを含む完全なドキュメントは Field projection を参照してください。
ページネーション(Pagination)
Squid は、クエリ上の paginate メソッドによってクエリ結果をページ分割する強力な方法を提供します。
paginate メソッドは PaginationOptions オブジェクトを引数に取り、次のプロパティを含みます。
pageSize: デフォルト 100 の数値。subscribe: クエリのリアルタイム更新を購読するかどうかを示す boolean。デフォルトはtrue。
呼び出すと、paginate は次のプロパティを持つ Pagination オブジェクトを返します。
observeState: 現在のページネーション状態(PaginationState)を emit する observable。next: 次ページの状態を promise で解決する関数。prev: 前ページの状態を promise で解決する関数。waitForData: ローディング完了後、現在のページネーション状態を promise で返す関数。unsubscribe: クエリの購読を解除し、内部状態をクリアするようページネーションオブジェクトに指示する関数。
PaginationState オブジェクトは次のプロパティを含みます。
data: 現在ページのデータを保持する配列。hasNext: 次ページがあるかどうかを示す boolean。hasPrev: 前ページがあるかどうかを示す boolean。isLoading: ページネーションがデータを読み込み中かどうかを示す boolean。
以下はページネーションの使用例です。
const pagination = 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); // Outputs the first page of data
data = await pagination.next();
console.log(data); // Outputs the second page of data
pagination.unsubscribe();
要求された場合、ページネーションオブジェクトは最新状態を維持するために、クエリのリアルタイム更新を能動的に購読します。つまり、データが変化すると PaginationState が最新データで更新されます。
リアルタイム更新を維持するため、またはサーバー更新により空ページを受け取るといったエッジケースに対応するために、ページを表示するまでにページネーションオブジェクトが 1 回を超えてクエリを実行する場合があります。
クエリヘルパー
ヘルパー関数を使う以外にも、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% に一致するかをチェック (CS) |
| like | where('foo', 'like', '%bar%') | like('foo', '%bar%', false) | foo がパターン %bar% に一致するかをチェック (CI) |
| notLike (case sensitive) | where('foo', 'not like_cs', '%bar%') | notLike('foo', '%bar%') | foo がパターン %bar% に一致しないかをチェック (CS) |
| notLike | where('foo', 'not like', '%bar%') | notLike('foo', '%bar%', false) | foo がパターン %bar% に一致しないかをチェック (CI) |
| 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 配列がどの値も含まないかをチェック |
エラーハンドリング
| Error | Cause | Solution |
|---|---|---|
| Invalid field name | コレクションに存在しないフィールドでフィルタまたはソートしている | フィールド名がコレクションスキーマと TypeScript interface に一致するか確認する |
| Limit exceeded | 最大 1000 件を超える結果を要求している | limit(1000) 以下にするか、大きな結果セットには pagination を使う |
limitBy sort mismatch | limitBy 内のフィールドが先頭の sortBy 句に現れない | すべての limitBy フィールドに対応する sortBy 句が正しい順序であることを確認する |
| Empty query result | フィルタ条件に一致するドキュメントがない | フィルタ値を確認し、想定コレクションにデータが存在するか確認する |
| Security rule rejection | クエリが security rules によりブロックされた | ユーザーにそのコレクションの読み取り権限があることを確認する |
ベストプラクティス
-
一度きりの読み取りには
snapshot()を使い、リアルタイム更新が必要な場合にのみsnapshots()を使ってください。不必要な購読はリソースを消費します。 -
アプリケーションコードですべて取得してから絞り込むのではなく、サーバー側でフィルタを適用してください。
Client code// Recommended: server-side filtering
const activeUsers = await squid
.collection<User>('users')
.query()
.eq('status', 'active')
.snapshot();
// Avoid: fetching everything and filtering client-side
const allUsers = await squid
.collection<User>('users')
.query()
.snapshot();
const activeUsers = allUsers.filter((u) => u.data.status === 'active'); -
予期せず大きな結果セットになるのを避けるため、クエリには明示的な limit を設定してください。デフォルトの limit は 1000 です。
-
document reference ではなくデータだけが必要な場合、後続コードを簡潔にするために
dereference()を使用してください。 -
必要なフィールドが一部だけの場合、帯域を削減するために field projection を使用してください。
-
メモリリークを防ぐため、コンポーネントやリスナーが破棄されるときに observable の購読を解除してください。
Client codeconst subscription = squid
.collection<User>('users')
.query()
.snapshots()
.subscribe((users) => {
// handle updates
});
// When done:
subscription.unsubscribe();