Queries
real-time query support を備え、複数 database 間の data join を含むあらゆる source の data に対して詳細な query を作成します。
Queries を使用する理由
database から特定の data subset を取得し、condition で filter し、result を sort し、collection 間で data を join し、または real-time update を subscribe する必要があります。query API を使用すると、これらすべてを chainable で type-safe な interface により実行できます。
概要
Squid は stream と observable の処理に RxJs を使用します。RxJS と streaming update の詳細については、RxJs documentationを参照してください。
document を query する場合、単一 snapshot または snapshot stream を consume できます。
- snapshot を consume する場合、document の最新 version を
Promiseとして受け取ります。 - snapshot stream を consume する場合、result は query result が変更されるたびに新しい snapshot を emit する RxJs
Observableです。
Squid Client SDK における snapshot と data stream の利用により、最小限の overhead と setup で、data source からの real-time update を継続的に受け取ることができます。
クイックスタート
collection reference で filter method を chain して query を構築し、snapshot() を呼び出して result を取得します。
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 の Query
単一 document を query するには、document reference で snapshot または snapshots method を呼び出します。result は直接 document data(type は T | undefined)になります。
const user = await squid.collection<User>('users').doc('user_id').snapshot();
if (user) {
console.log(user.name);
}
または、snapshots method を使用してこの document の変更を subscribe します。document が変更されるたびに、observable は最新 data を emit します。
squid
.collection<User>('users')
.doc('user_id')
.snapshots()
.subscribe((user) => {
if (user) {
console.log(user.name);
}
});
Squid の backend security rule を使用すると、個々の query を実行できる user を制御できます。これらの rule は query を含む QueryContext を parameter として受け取ります。特定 collection または database connector への read access を制限するには、backend security を設定します。database の read と write privilege を制限する方法については、security rules documentationを参照してください。
Collection から複数 document を Query する
collection から document を query する場合は、query method で query を構築します。
Squid では、snapshot method による単一 query result、または snapshots method による query result stream のいずれかを consume できます。この方法で query result stream を consume すると、query result が変更されるたびに observable が新しい value を emit します。
以下は、18 歳より上のすべての admin を返す単一 query snapshot を取得する例です。
const users = await squid
.collection<User>('users')
.query()
.gt('age', 18)
.eq('role', 'admin')
.snapshot();
次の例では、snapshots method を使用して streaming query result を受け取ります。
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 method を使用して change stream を返すこともサポートします。この method が返す observable には、collection に対する変更を追跡する 3 種類の array が含まれます。
inserts: collection に新たに insert された document の referenceupdates: collection 内で update された document の referencedeletes: 削除された document の data
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 の De-referencing
collection から data を query すると、document reference を受け取ります。document の data には data getter を呼び出して access できます。
const usersObs = squid
.collection<User>('users')
.query()
.snapshots()
.pipe(map((user) => user.data));
data getter を呼び出さずに document data を直接受け取るには、dereference method を呼び出します。
const usersDataObs = squid
.collection<User>('users')
.query()
.dereference()
.snapshots();
Collection と Connector をまたぐ Data Join
Squid では、複数の query を join し、result change を listen できます。異なる data source の data を join する機能により、この feature はさらに強力になります。
たとえば、dept collection と employees collection を query で join し、18 歳より上のすべての employee とその department を返すことができます。
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
});
上記の code では、各 query に alias が割り当てられます。dept collection は d、employees collection は e です。join condition は、employees collection の deptId field と dept collection の id field を join します。
この例では built-in database 内の 2 つの collection を join していますが、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');
default では、Squid は left join を実行します。つまり、この例では empty department を含むすべての department が join result に含まれます。たとえば、department A には 18 歳より上の person が 2 人いる一方、department B には 18 歳より上の person がいないとします。join query を実行すると、result は以下のようになります。
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 data を持つ result を除外するには、inner join を実行します。inner join を実行するには、join method の 4 番目の parameter として { isInner: true } を渡します。次の例では、department 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 つの collection 間で join を記述するには、query に別の join を追加します。たとえば、employees、dept、company の collection がある場合、次の 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 を join します。result object の type は以下のとおりです。
type Result = Array<{
e: DocumentReference<Employee>;
d: DocumentReference<Dept> | undefined;
c: DocumentReference<Company> | undefined;
}>;
Join の left side を選択する
join の left side を選択するには、join method の 4 番目の parameter である options object に leftAlias を渡します。たとえば、employee と dept、および employee と company を join するには、次のように company collection の join における left side を選択します。
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 result を Grouping する
repeat entry を結合するよう join result を group 化するには、join query で grouped() method を呼び出します。
たとえば、employees と dept、および dept と company を join する場合、grouped を使用すると user ごとに 1 entry だけを受け取れます。
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() method を使用しない場合、この query は次の type の result を返します。
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() method を使用する場合、query は次の type の result を返します。
type Result = Array<{
e: DocumentReference<Employee>;
d: Array<{
d: DocumentReference<Dept>;
c: Array<DocumentReference<Company>>;
}>;
}>;
grouped() method は dereference() とともに使用でき、DocumentReference なしで result data を取得できます。
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();
この query は次の type の result を返します。
type Result = Array<{
e: Employee;
d: Array<{
d: Dept;
c: Array<Company>;
}>;
}>;
OR query
collection reference の or() method を使用して、同じ collection に対する複数の query を結合します。result は重複排除され、最初の query の sort order で sort されます。
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() に渡すすべての query は同じ sort order を持つ必要があります。少なくとも 1 つの query を指定する必要があります。
Limit と Sorting
Squid では query を sort および limit でき、application の performance の最適化と user experience の向上に役立ちます。
query を sort するには、sortBy method を使用し、sort 対象の field と optional な sort order parameter を指定します。sort order を指定しない場合、query は ascending order が default になります。
次の例では、query を age の descending order で sort します。
const users = await squid
.collection<User>('users')
.query()
.gt('age', 18)
.eq('role', 'admin')
.sortBy('age', false)
.snapshot();
query が返す result 数を制限するには、limit method を使用して返す result の最大数を指定します。limit を指定しない場合、query の default は 1000 であり、これが許可される最大値でもあります。1000 item より多く取得するには、paginationを使用します。
次の例では、query を 10 result に制限します。
const users = await squid
.collection<User>('users')
.query()
.gt('age', 18)
.eq('role', 'admin')
.limit(10)
.snapshot();
次の例のように、同じ query で sorting と limiting を併用することもできます。
const users = await squid
.collection<User>('users')
.query()
.gt('age', 18)
.eq('role', 'admin')
.sortBy('age', false)
.limit(10)
.snapshot();
別の feature として limitBy があります。これは、fields 内の各 field で同じ value を持つ document のうち、最初の limit document のみを返します。これにより、「各 city で最も若い user を 5 人返す」ような query が可能になります(例を参照)。
const users = await squid
.collection<User>('users')
.query()
.sortBy('state')
.sortBy('city')
.sortBy('age')
.sortBy('name')
.limitBy(5, ['state', 'city'])
.snapshot();
返される query は、各 state と city の組み合わせごとに最大 5 document を含みます。実質的に、この query は各 city で最も若い user を 5 人返します(同じ age の場合は name で決定されます)。
limitBy clause のすべての fields は、query の最初の n 個の sortBy clause に出現する必要があります(n は field 数)。上記の例では、limitBy に含まれる state と city に、query 内の sortBy() が必要です。また、それらの sortBy は age または name より前に置く必要があります。
Field projection
projectFields method を使用すると、query から返す field を指定できます。必要な data のみを fetch することで bandwidth を削減し、performance を向上させます。
const users = await squid
.collection<User>('users')
.query()
.projectFields(['name', 'age'])
.dereference()
.snapshot();
// Results contain only the projected fields: name and age
field projection は real-time subscription、dot notation による nested field、および他のすべての query method で機能します。validation rule、subscription behavior、error handling を含む完全な documentation については、Field projectionを参照してください。
Pagination
Squid は query の paginate method を通じて、query result を paginate する強力な方法を提供します。
paginate method は、次の property を含む PaginationOptions object を parameter として受け取ります。
pageSize: default が 100 の number。subscribe: query の real-time update を subscribe するかを示す boolean。default はtrueです。
呼び出されると、paginate method は次の property を持つ Pagination object を返します。
observeState:PaginationStateとして定義された現在の pagination state を emit する observable。next: 次の page state で resolve する promise を返す function。prev: 前の page state で resolve する promise を返す function。waitForData: loading process の完了後、現在の pagination state で resolve する promise を返す function。unsubscribe: query から unsubscribe し、internal state を clear するよう pagination object に指示する function。
PaginationState object には、次の property が含まれます。
data: 現在の page の data を保持する array。hasNext: 次の page が利用可能かを示す boolean。hasPrev: 前の page が利用可能かを示す boolean。isLoading: pagination が data を loading 中かを示す boolean。
以下は pagination の使用例です。
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();
request されると、pagination object は最新 state を維持するために query の real-time update を active に subscribe します。つまり、data が change すると PaginationState object は最新 data で update されます。
real-time update を維持するため、または server update によって empty page を受信するような edge case に対応するため、pagination object は page を表示するのに複数の query を実行する場合があります。
Query helper
helper function を使用する以外に、where function を使用して query を構築できます。where function は、query 対象の field、使用する operator、比較対象の value という 3 つの parameter を受け取ります。
| Helper | Before | After | 説明 |
|---|---|---|---|
| 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 が指定 list に含まれるか確認する |
| nin | where('foo', 'not in', ['bar']) | nin('foo', ['bar']) | foo が指定 list に含まれないか確認する |
| 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 が pattern %bar% に一致するか確認する(CS) |
| like | where('foo', 'like', '%bar%') | like('foo', '%bar%', false) | foo が pattern %bar% に一致するか確認する(CI) |
| notLike(case sensitive) | where('foo', 'not like_cs', '%bar%') | notLike('foo', '%bar%') | foo が pattern %bar% に一致しないか確認する(CS) |
| notLike | where('foo', 'not like', '%bar%') | notLike('foo', '%bar%', false) | foo が pattern %bar% に一致しないか確認する(CI) |
| arrayIncludesSome | where('foo', 'array_includes_some', ['bar']) | arrayIncludesSome('foo', ['bar']) | foo array が value の一部を含むか確認する |
| arrayIncludesAll | where('foo', 'array_includes_all', ['bar']) | arrayIncludesAll('foo', ['bar']) | foo array がすべての value を含むか確認する |
| arrayNotIncludes | where('foo', 'array_not_includes', ['bar']) | arrayNotIncludes('foo', ['bar']) | foo array がいずれの value も含まないか確認する |
Document ID による Filtering
すべての collection は、基盤となる primary key column 名を知らなくても document identity で filter するための __docId__ pseudo-field を公開します。where() と helper function で直接使用するか、docId() および docIds() shortcut を使用します。
// Single document by ID (shortcut for eq('__docId__', id)):
await squid.collection<User>('users').query().docId('user_abc123').snapshot();
// Multiple documents by ID (shortcut for in('__docId__', ids)):
await squid.collection<User>('users').query().docIds(['user_1', 'user_2']).snapshot();
// Combined with regular conditions:
await squid
.collection<User>('users')
.query()
.docIds(['user_1', 'user_2'])
.eq('status', 'active')
.snapshot();
composite primary key を持つ collection では、すべての key field を含む object を渡します。各 object は 1 unit として match するため、[{ orgId: 'o1', itemId: 'i1' }, { orgId: 'o2', itemId: 'i2' }] が混在した組み合わせに match することはありません。
await squid
.collection('inventory', 'CONNECTOR_ID')
.query()
.in('__docId__', [
{ orgId: 'o1', itemId: 'i1' },
{ orgId: 'o2', itemId: 'i2' },
])
.snapshot();
query result で返された __docId__ value は、別の query にそのまま渡せます。留意すべき制約は以下のとおりです。
- single primary key は、range および
likeoperator を含む完全な operator set をサポートします。 - composite primary key は、
==、!=、in、not inの equality family のみをサポートします。 - array operator は
__docId__ではサポートされません。 - composite-key
__docId__filter は Elasticsearch および DynamoDB connector ではサポートされません(single-key filter はすべての connector で機能します)。
connector ごとの ID structure については、Document IDsを参照してください。
Error Handling
| Error | 原因 | 解決策 |
|---|---|---|
| Invalid field name | collection に存在しない field で filter または sort を行った | field 名が collection schema および TypeScript interface と一致することを確認する |
| Limit exceeded | 最大 1000 result を超える request を行った | limit(1000) 以下を使用するか、より大きい result set には pagination を使用する |
limitBy sort mismatch | limitBy 内の field が最初の sortBy clause に存在しない | すべての limitBy field に対し、正しい順序の対応する sortBy clause があることを確認する |
| Empty query result | filter condition に一致する document がない | filter value を確認し、想定 collection に data が存在することを確認する |
| Security rule rejection | query が security rules により block された | user が collection に対する read permission を持つことを確認する |
ベストプラクティス
-
one-time read には
snapshot()を使用し、real-time update が必要な場合にのみsnapshots()を使用します。不要な subscription は resource を消費します。 -
すべての document を fetch して application code で filter するのではなく、server-side で filter を適用します。
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'); -
予期しない大規模 result set を避けるため、query には明示的な limit を設定します。default limit は 1000 です。
-
document reference ではなく data のみが必要な場合は、downstream code を簡略化するために
dereference()を使用します。 -
field の subset のみが必要な場合は、bandwidth を削減するために field projection を使用します。
-
memory leak を防ぐため、component または listener が destroy されたときに observable を unsubscribe します。
Client codeconst subscription = squid
.collection<User>('users')
.query()
.snapshots()
.subscribe((users) => {
// handle updates
});
// When done:
subscription.unsubscribe();