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

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 を取得します。

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 の Query​

単一 document を query するには、document reference で snapshot または snapshots method を呼び出します。result は直接 document data(type は T | undefined)になります。

Client code
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 します。

Client code
squid
.collection<User>('users')
.doc('user_id')
.snapshots()
.subscribe((user) => {
if (user) {
console.log(user.name);
}
});
Query の保護

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 を取得する例です。

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

次の例では、snapshots method を使用して streaming query result を受け取ります。

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 method を使用して change stream を返すこともサポートします。この method が返す observable には、collection に対する変更を追跡する 3 種類の array が含まれます。

  • inserts: collection に新たに insert された document の reference
  • updates: collection 内で update された document の reference
  • deletes: 削除された document の data
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 の De-referencing​

collection から data を query すると、document reference を受け取ります。document の data には data getter を呼び出して access できます。

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

data getter を呼び出さずに document data を直接受け取るには、dereference method を呼び出します。

Client code
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 を返すことができます。

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
});

上記の 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 を実行できます。

Client code
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 は以下のようになります。

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 data を持つ result を除外するには、inner join を実行します。inner join を実行するには、join method の 4 番目の parameter として { isInner: true } を渡します。次の例では、department 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 つの collection 間で join を記述するには、query に別の join を追加します。たとえば、employees、dept、company の collection がある場合、次の 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();

上記の例では、employees と dept、および dept と company を join します。result object の type は以下のとおりです。

Client code
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 を選択します。

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 result を Grouping する​

repeat entry を結合するよう join result を group 化するには、join query で grouped() method を呼び出します。 たとえば、employees と dept、および dept と company を join する場合、grouped を使用すると user ごとに 1 entry だけを受け取れます。

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() method を使用しない場合、この query は次の type の result を返します。

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() method を使用する場合、query は次の type の result を返します。

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

grouped() method は dereference() とともに使用でき、DocumentReference なしで result data を取得できます。

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

この query は次の type の result を返します。

Client code
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 されます。

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() に渡すすべての 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 します。

Client code
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 に制限します。

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

次の例のように、同じ query で sorting と limiting を併用することもできます。

Client code
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 が可能になります(例を参照)。

Client code
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 を向上させます。

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 は 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 の使用例です。

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

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 を受け取ります。

HelperBeforeAfter説明
eqwhere('foo', '==', 'bar')eq('foo', 'bar')foo が bar と等しいか確認する
neqwhere('foo', '!=', 'bar')neq('foo', 'bar')foo が bar と等しくないか確認する
inwhere('foo', 'in', ['bar'])in('foo', ['bar'])foo が指定 list に含まれるか確認する
ninwhere('foo', 'not in', ['bar'])nin('foo', ['bar'])foo が指定 list に含まれないか確認する
gtwhere('foo', '>', 'bar')gt('foo', 'bar')foo が bar より大きいか確認する
gtewhere('foo', '>=', 'bar')gte('foo', 'bar')foo が bar 以上か確認する
ltwhere('foo', '<', 'bar')lt('foo', 'bar')foo が bar より小さいか確認する
ltewhere('foo', '<=', 'bar')lte('foo', 'bar')foo が bar 以下か確認する
like(case sensitive)where('foo', 'like_cs', '%bar%')like('foo', '%bar%')foo が pattern %bar% に一致するか確認する(CS)
likewhere('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)
notLikewhere('foo', 'not like', '%bar%')notLike('foo', '%bar%', false)foo が pattern %bar% に一致しないか確認する(CI)
arrayIncludesSomewhere('foo', 'array_includes_some', ['bar'])arrayIncludesSome('foo', ['bar'])foo array が value の一部を含むか確認する
arrayIncludesAllwhere('foo', 'array_includes_all', ['bar'])arrayIncludesAll('foo', ['bar'])foo array がすべての value を含むか確認する
arrayNotIncludeswhere('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 を使用します。

Client code
// 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 することはありません。

Client code
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 および like operator を含む完全な 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 namecollection に存在しない field で filter または sort を行ったfield 名が collection schema および TypeScript interface と一致することを確認する
Limit exceeded最大 1000 result を超える request を行ったlimit(1000) 以下を使用するか、より大きい result set には pagination を使用する
limitBy sort mismatchlimitBy 内の field が最初の sortBy clause に存在しないすべての limitBy field に対し、正しい順序の対応する sortBy clause があることを確認する
Empty query resultfilter condition に一致する document がないfilter value を確認し、想定 collection に data が存在することを確認する
Security rule rejectionquery が security rules により block されたuser が collection に対する read permission を持つことを確認する

ベストプラクティス​

  1. one-time read には snapshot() を使用し、real-time update が必要な場合にのみ snapshots() を使用します。不要な subscription は resource を消費します。

  2. すべての 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');
  3. 予期しない大規模 result set を避けるため、query には明示的な limit を設定します。default limit は 1000 です。

  4. document reference ではなく data のみが必要な場合は、downstream code を簡略化するために dereference() を使用します。

  5. field の subset のみが必要な場合は、bandwidth を削減するために field projection を使用します。

  6. memory leak を防ぐため、component または listener が destroy されたときに observable を unsubscribe します。

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

    // When done:
    subscription.unsubscribe();