Queries (クエリ)
任意のソースからのデータに対して、細かいクエリを作成し、複数のデータベース間でデータを結合するなど、リアルタイムクエリサポートを活用したクエリが可能です。
Squid はストリームおよび observables の処理に 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 を指定することで、異なるデータベースコネクタ間でも結合を行うことができます。たとえば、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) を実行してください。内部結合を実行するには、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 つのコレクション間で結合を行う場合は、クエリにさらに結合を追加します。たとえば、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 の左側の選択
join の左側を選択するには、join
メソッドの第4パラメータである options
オブジェクトに leftAlias
を渡します。たとえば、employee
と dept
の結合および employee
と company
の結合を行いたい場合、company
コレクションの左側として employee
を選択するには、以下のようにします:
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 クエリで grouped()
メソッドを呼び出して、結果をグループ化することができます。たとえば、employees
と dept
、および dept
と company
の結合時に、重複するユーザーエントリを 1 つにまとめる場合は、grouped
を使用します。
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<{
// 同じユーザーが複数回返される可能性があります(各部署および会社ごとに 1 つ)
e: DocumentReference<Employee>;
// 同じ部署が複数回返される可能性があります(会社ごとに 1 つ)
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
はフィールドの数です)。上記の例では、limitBy
に含まれる state
と city
は、それぞれ sortBy()
に含まれており、かつ age
や name
よりも前に配置されている必要があります。
ページネーション
Squid は、クエリ結果をページネートする強力な方法として、クエリ上の paginate
メソッドを提供しています。
paginate
メソッドは、次のプロパティを含む PaginationOptions
オブジェクトをパラメータとして受け取ります:
pageSize
: デフォルト値は 100 の数値subscribe
: クエリのリアルタイム更新を購読するかどうかを示す boolean。デフォルトはtrue
このメソッドを呼び出すと、以下のプロパティを持つ Pagination
オブジェクトが返されます:
observeState
: 現在のページネーション状態(PaginationState
として定義)を発行する observablenext
: 次のページ状態を Promise で解決する関数prev
: 前のページ状態を Promise で解決する関数waitForData
: ローディングが完了したら現在のページネーション状態を Promise で返す関数unsubscribe
: クエリの購読を解除し、内部状態をクリアするための関数
PaginationState
オブジェクトは以下のプロパティを持ちます:
data
: 現在のページのデータを保持する配列hasNext
: 次のページが存在する場合は truehasPrev
: 前のページが存在する場合は trueisLoading
: ページネーションがデータのローディング中かどうかを示す boolean
以下はページネーションの使用例です:
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
オブジェクトはデータが変化するたびに更新されます。
リアルタイム更新を維持するため、またはサーバーの更新により空のページが返されるといったエッジケースに対応するために、ページネーションオブジェクトは 1 回以上のクエリを実行する場合があります。
クエリヘルパー
ヘルパー関数を使用することに加え、where
関数を利用してクエリを構築することもできます。where
関数は、クエリ対象のフィールド、使用する演算子、比較する値の 3 つのパラメータを受け取ります。
ヘルパー | 変更前 | 変更後 | 説明 |
---|---|---|---|
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 配列がいずれの値も含まないかをチェックする |