メインコンテンツまでスキップ

クエリ

リアルタイムクエリのサポートにより、複数のデータベース間のデータ結合も含め、あらゆるソースのデータに対して細かなクエリを作成できます。

クエリを使用する理由

データベースから特定のデータのサブセットを取得したり、条件でフィルタしたり、結果をソートしたり、コレクション間でデータを結合したり、リアルタイム更新を購読したりする必要があります。Query API を使うと、チェーン可能で型安全なインターフェースでこれらすべてを実行できます。

概要

Note

Squid はストリームと Observable の処理に RxJs を使用します。RxJS とストリーミング更新の詳細は RxJs documentation を参照してください。

ドキュメントをクエリする際、単一のスナップショット、またはスナップショットのストリームのどちらかを利用できます。

  • スナップショットを利用する場合、ドキュメント(群)の最新バージョンを Promise として受け取ります。
  • スナップショットのストリームを利用する場合、結果は RxJsObservable となり、クエリ結果が変化するたびに新しいスナップショットを emit します。

Squid Client SDK におけるスナップショットとデータストリームの利用により、最小限のオーバーヘッドとセットアップで、 データソースからリアルタイム更新を継続的に受け取れます。

クイックスタート

collection reference に対してフィルタメソッドをチェーンしてクエリを構築し、snapshot() を呼び出して結果を取得します。

Client code
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)です。

Client code
const user = await squid.collection<User>('users').doc('user_id').snapshot();
if (user) {
console.log(user.name);
}

または、snapshots メソッドを使ってこのドキュメントの変更を subscribe できます。ドキュメントが変更されるたびに、Observable が最新データを emit します。

Client code
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 を設定してください。データベースに対する read/write 権限の制限方法については、security rules の docs を参照してください。

コレクションから複数ドキュメントをクエリ

コレクションからドキュメントをクエリする場合、query メソッドを使ってクエリを構築します。

Squid は、snapshot メソッドによる単一のクエリ結果、または snapshots メソッドによるクエリ結果ストリームのどちらかを利用する選択肢を提供します。この方法でクエリ結果ストリームを利用すると、クエリ結果が変化するたびに Observable が新しい値を emit します。

以下は、18歳より上の admin をすべて返す単一のクエリスナップショットを取得する例です。

Client code
const users = await squid
.collection<User>('users')
.query()
.gt('age', 18)
.eq('role', 'admin')
.snapshot();

次の例は、snapshots メソッドを使ってストリーミングのクエリ結果を受け取ります。

Client code
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: 削除されたドキュメントのデータを含みます
Client code
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 を呼び出すことで、 ドキュメントのデータにアクセスできます。

Client code
const usersObs = squid
.collection<User>('users')
.query()
.snapshots()
.pipe(map((user) => user.data));

data getter を呼ばずにドキュメントデータを直接受け取りたい場合は、dereference メソッドを呼び出します。

Client code
const usersDataObs = squid
.collection<User>('users')
.query()
.dereference()
.snapshots();

コレクションと connector をまたいだデータ結合

Squid では、複数のクエリを join し、結果の変化を監視できます。この機能は、異なるデータソースのデータを join できることで、さらに強力になります。

たとえば、dept コレクションと employees コレクションを join し、18歳より上の従業員を部署情報とともに返すクエリを作成できます。

Client code
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 コレクションは demployees コレクションは e です。 join 条件は、employees コレクションの deptId フィールドと dept コレクションの id フィールドを結合します。

この例は組み込みデータベースの 2 つのコレクションの join を示していますが、コレクション参照に connector ID を指定することで、別々の database connector 間でも join できます。たとえば、connector ID が connectorAconnectorB の 2 つの database connector がある場合、次のように connector ID を追加して同じ join を実行できます。

Client code
const departmentCollection = squid.collection<Dept>('dept', 'connectorA');
const employeeCollection = squid.collection<Employee>('employees', 'connectorB');

デフォルトでは、Squid は left join を実行します。つまりこの例では、空の部署を含め、すべての部署が join 結果に含まれます。たとえば、部署 A には 18 歳より上の人が 2 人いるが、部署 B には 18 歳より上の人が 0 人だとします。join クエリを実行すると、結果は次のようになります。

Client code
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 は返されません。

Client code
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 を書くには、クエリにもう 1 つ join を追加します。たとえば、employeesdeptcompany の各コレクションがある場合、次の join を実行できます。

Client code
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();

上の例では、employeesdept、そして deptcompany を join しています。結果オブジェクトは次の型になります。

Client code
type Result = Array<{
e: DocumentReference<Employee>;
d: DocumentReference<Dept> | undefined;
c: DocumentReference<Company> | undefined;
}>;

join の左側を選ぶ

join の左側を選ぶには、join メソッドの第 4 引数として options オブジェクトに leftAlias を渡します。たとえば、employeedept を join し、さらに employeecompany を join する場合、company コレクションの join の左側を次のように選びます。

Client code
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() メソッドを呼び出します。 たとえば、employeesdept を join し、さらに deptcompany を join する場合、grouped を使うことでユーザーごとに 1 件だけのエントリを受け取れます。

Client code
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() メソッドがない場合、このクエリは次の型の結果を返します。

Client code
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() メソッドがある場合、クエリは次の型の結果を返します。

Client code
type Result = Array<{
e: DocumentReference<Employee>;
d: Array<{
d: DocumentReference<Dept>;
c: Array<DocumentReference<Company>>;
}>;
}>;

grouped()dereference() と組み合わせて、DocumentReference なしで結果データを取得できます。

Client code
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();

このクエリは次の型の結果を返します。

Client code
type Result = Array<{
e: Employee;
d: Array<{
d: Dept;
c: Array<Company>;
}>;
}>;

OR クエリ

collection referenceor() メソッドを使って、同じコレクションに対する複数のクエリを組み合わせます。結果は重複排除され、最初のクエリのソート順でソートされます。

Client code
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)とソート

Squid はクエリのソートと制限(limit)を提供しており、アプリケーションのパフォーマンス最適化や UX(ユーザー体験)の向上に役立ちます。

クエリをソートするには sortBy メソッドを使用し、ソート対象フィールドと、任意でソート順を指定します。ソート順が指定されない場合、クエリはデフォルトで昇順になります。

以下は、年齢で降順にソートする例です。

Client code
const users = await squid
.collection<User>('users')
.query()
.gt('age', 18)
.eq('role', 'admin')
.sortBy('age', false)
.snapshot();

クエリが返す結果数を制限するには limit メソッドを使用し、返す最大件数を指定します。limit を指定しない場合、クエリはデフォルトで 1000 になり、これが許可される最大値でもあります。1000 件を超えて取得するには、pagination を使用してください。

以下は、クエリ結果を 10 件に制限する例です。

Client code
const users = await squid
.collection<User>('users')
.query()
.gt('age', 18)
.eq('role', 'admin')
.limit(10)
.snapshot();

次の例のように、同一クエリでソートと制限を併用することもできます。

Client code
const users = await squid
.collection<User>('users')
.query()
.gt('age', 18)
.eq('role', 'admin')
.sortBy('age', false)
.limit(10)
.snapshot();

もう一つの機能として limitBy があります。これは、fields 内の各フィールドが同じ値を持つ最初の limit 件のドキュメントだけを返します。これにより、「各都市で最も若いユーザーを 5 人返す」のようなクエリが可能になります(例を参照)。

Client code
const users = await squid
.collection<User>('users')
.query()
.sortBy('state')
.sortBy('city')
.sortBy('age')
.sortBy('name')
.limitBy(5, ['state', 'city'])
.snapshot();

返されるクエリは、statecity の組み合わせごとに最大 5 ドキュメントになります。実質的に、このクエリは各都市で最も若いユーザーを 5 人返します(年齢が同じ場合は name でタイブレークされます)。

Note

limitBy 句のすべての fields は、クエリの最初の n 個の sortBy 句(n はフィールド数)に含まれていなければなりません。上の例では、statecitylimitBy に出てくるフィールド)にはクエリ内で sortBy() が必要であり、かつ agename より前に sortBy されている必要があります。

フィールド投影(Field projection)

projectFields メソッドを使うと、クエリで返すフィールドを指定でき、必要なデータだけを取得することで帯域を削減し、パフォーマンスを改善できます。

Client code
const users = await squid
.collection<User>('users')
.query()
.projectFields(['name', 'age'])
.dereference()
.snapshot();

// Results contain only the projected fields: name and age

フィールド投影は、リアルタイム購読、ドット記法によるネストフィールド、その他すべてのクエリメソッドと併用できます。検証ルール、購読の挙動、エラーハンドリングを含む完全なドキュメントは、Field projection を参照してください。

ページネーション

Squid は、クエリの paginate メソッドによってクエリ結果をページングする強力な方法を提供します。 paginate メソッドは PaginationOptions オブジェクトをパラメータとして受け取り、次のプロパティを含みます。

  • pageSize: デフォルトが 100 の数値。
  • subscribe: クエリのリアルタイム更新を購読するかどうかを示す boolean。デフォルトは true

呼び出すと、paginate は次のプロパティを持つ Pagination オブジェクトを返します。

  • observeState: 現在のページネーション状態(PaginationState として定義)を emit する Observable。
  • next: 次ページの状態で resolve する Promise を返す関数。
  • prev: 前ページの状態で resolve する Promise を返す関数。
  • waitForData: ロード処理が完了したら、現在のページネーション状態で resolve する Promise を返す関数。
  • unsubscribe: ページネーションオブジェクトがクエリの購読を解除し、内部状態をクリアするよう指示する関数。

PaginationState オブジェクトには次のプロパティが含まれます。

  • data: 現在ページのデータを保持する配列。
  • hasNext: 次ページが利用可能かを示す boolean。
  • hasPrev: 前ページが利用可能かを示す boolean。
  • isLoading: ページネーションがデータ読み込み中かどうかを示す boolean。

以下はページネーションの使用例です。

Client code
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 オブジェクトは最新データで更新されます。

Note

リアルタイム更新を維持するため、またはサーバー更新により空ページを受け取るといったエッジケースに対応するために、 ページネーションオブジェクトは 1 回だけでなく複数回クエリを実行してページを表示する場合があります。

クエリヘルパー

ヘルパー関数を使うだけでなく、where 関数を使ってクエリを構築することもできます。where 関数は 3 つのパラメータを受け取ります。クエリするフィールド、使用する演算子、比較対象の値です。

HelperBeforeAfterExplanation
eqwhere('foo', '==', 'bar')eq('foo', 'bar')foobar と等しいかをチェックします
neqwhere('foo', '!=', 'bar')neq('foo', 'bar')foobar と等しくないかをチェックします
inwhere('foo', 'in', ['bar'])in('foo', ['bar'])foo が指定リストに含まれるかをチェックします
ninwhere('foo', 'not in', ['bar'])nin('foo', ['bar'])foo が指定リストに含まれないかをチェックします
gtwhere('foo', '>', 'bar')gt('foo', 'bar')foobar より大きいかをチェックします
gtewhere('foo', '>=', 'bar')gte('foo', 'bar')foobar 以上かをチェックします
ltwhere('foo', '<', 'bar')lt('foo', 'bar')foobar より小さいかをチェックします
ltewhere('foo', '<=', 'bar')lte('foo', 'bar')foobar 以下かをチェックします
like (case sensitive)where('foo', 'like_cs', '%bar%')like('foo', '%bar%')foo がパターン %bar% にマッチするか(CS)
likewhere('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)
notLikewhere('foo', 'not like', '%bar%')notLike('foo', '%bar%', false)foo がパターン %bar% にマッチしないか(CI)
arrayIncludesSomewhere('foo', 'array_includes_some', ['bar'])arrayIncludesSome('foo', ['bar'])foo 配列が値の一部を含むかをチェックします
arrayIncludesAllwhere('foo', 'array_includes_all', ['bar'])arrayIncludesAll('foo', ['bar'])foo 配列がすべての値を含むかをチェックします
arrayNotIncludeswhere('foo', 'array_not_includes', ['bar'])arrayNotIncludes('foo', ['bar'])foo 配列がいずれの値も含まないかをチェックします

エラーハンドリング

ErrorCauseSolution
Invalid field nameコレクション内に存在しないフィールドでフィルタやソートを行っているフィールド名がコレクションスキーマと TypeScript interface に一致するか確認してください
Limit exceeded最大 1000 件を超える結果を要求しているlimit(1000) 以下にするか、より大きい結果セットには pagination を使用してください
limitBy sort mismatchlimitBy のフィールドが最初の sortBy 句に現れないすべての limitBy フィールドに対応する sortBy 句が正しい順序であることを確認してください
Empty query resultフィルタ条件に一致するドキュメントがないフィルタ値を確認し、想定するコレクションにデータが存在するか確認してください
Security rule rejectionクエリが security rules によりブロックされたユーザーがそのコレクションの読み取り権限を持つか確認してください

ベストプラクティス

  1. 単発の読み取りには snapshot() を使用し、リアルタイム更新が必要な場合にのみ snapshots() を使用してください。不要な購読はリソースを消費します。

  2. サーバーサイドでフィルタを適用し、すべてのドキュメントを取得してアプリケーションコードでフィルタするのは避けてください。

    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');
  3. 明示的に limit を設定して、予期せぬ大きな結果セットを避けてください。デフォルトの limit は 1000 です。

  4. ドキュメント参照ではなくデータだけが必要な場合は、後続コードを簡潔にするため dereference() を使用してください。

  5. フィールドの一部だけが必要な場合は、帯域削減のため field projection を使用してください。

  6. メモリリークを防ぐため、コンポーネントやリスナーが破棄される際には Observable の購読を解除してください。

    Client code
    const subscription = squid
    .collection<User>('users')
    .query()
    .snapshots()
    .subscribe((users) => {
    // handle updates
    });

    // When done:
    subscription.unsubscribe();