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として定義)を発行する observable
- next: 次のページ状態を Promise で解決する関数
- prev: 前のページ状態を Promise で解決する関数
- waitForData: ローディングが完了したら現在のページネーション状態を Promise で返す関数
- unsubscribe: クエリの購読を解除し、内部状態をクリアするための関数
PaginationState オブジェクトは以下のプロパティを持ちます:
- data: 現在のページのデータを保持する配列
- hasNext: 次のページが存在する場合は true
- hasPrev: 前のページが存在する場合は true
- isLoading: ページネーションがデータのローディング中かどうかを示す 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配列がいずれの値も含まないかをチェックする |