Data Connect のスキーマ、クエリ、ミューテーション

Firebase Data Connect を使用すると、Google Cloud SQL で管理されている PostgreSQL インスタンスのコネクタを作成できます。これらのコネクタは、データを使用するためのスキーマ、クエリ、ミューテーションの組み合わせです。

スタートガイドでは、PostgreSQL の映画レビュー アプリのスキーマについて説明しました。このガイドでは、PostgreSQL の Data Connect スキーマを設計する方法について詳しく説明します。

このガイドでは、Data Connect クエリとミューテーションをスキーマの例と組み合わせて説明します。Data Connect スキーマに関するガイドでクエリ(およびミューテーション)について説明する理由他の GraphQL ベースのプラットフォームと同様に、Firebase Data Connectクエリファーストの開発プラットフォームです。そのため、デベロッパーはデータモデリングでクライアントが必要とするデータについて検討する必要があります。これは、プロジェクト用に開発するデータスキーマに大きな影響を与えます。

このガイドでは、まず新しい映画レビューのスキーマについて説明します。次に、そのスキーマから派生したクエリミューテーションについて説明します。最後に、コア Data Connect スキーマと同等の SQL リストを示します。

映画レビュー アプリのスキーマ

ユーザーが映画のレビューを送信して表示できるサービスを構築するとします。

このようなアプリには初期スキーマが必要です。このスキーマは後で拡張して、複雑なリレーショナル クエリを作成します。

映画の表

Movies のスキーマには、次のようなコア ディレクティブが含まれています。

  • @table(name)@col(name): SQL テーブル名と列名をカスタマイズします。指定しない場合、Data Connect は snake_case 名を生成します。
  • @col(dataType): SQL 列の型をカスタマイズします。
  • @default: 挿入時に SQL 列のデフォルト値を構成します。

詳しくは、@table@col@default のリファレンス ドキュメントをご覧ください。

# Movies
type Movie @table(name: "movie", key: "id") {
  id: UUID! @col(name: "movie_id") @default(expr: "uuidV4()")
  title: String!
  releaseYear: Int
  genre: String @col(dataType: "varchar(20)")
  rating: Int
  description: String
}

キースカラーとサーバー値

映画レビュー アプリについて詳しく説明する前に、Data Connectキー スカラーサーバー値について説明します。

キー スカラーは、スキーマ内のキーフィールドから Data Connect によって自動的に組み立てられる簡潔なオブジェクト識別子です。キースカラーは効率性を重視しており、1 回の呼び出しでデータの ID と構造に関する情報を取得できます。これは、新しいレコードに対して連続したアクションを実行し、今後のオペレーションに渡す一意の識別子が必要である場合に特に便利です。また、リレーショナル キーにアクセスして、より複雑な追加オペレーションを実行する場合にも便利です。

サーバー値を使用すると、expr 引数の特定のサーバーサイド CEL 式に従って、保存されている値またはすぐに計算できる値を使用して、テーブルのフィールドに値を動的に入力できます。たとえば、オペレーション リクエストに保存されている時間(updatedAt: Timestamp! @default(expr: "request.time"))を使用して、フィールドにアクセスされたときにタイムスタンプが適用されるフィールドを定義できます。

映画のメタデータ テーブル

次に、映画監督を追跡し、Movie と 1 対 1 の関係を設定しましょう。

参照フィールドを追加して、関係を定義します。

@ref ディレクティブを使用して、外部キー制約をカスタマイズできます。

  • @ref(fields): 外部キー フィールドを指定します。
  • @ref(references): ターゲット テーブルで参照されるフィールドを指定します。この参照はデフォルトでプライマリ キーになりますが、@unique を含むフィールドもサポートされています。

詳しくは、@ref のリファレンス ドキュメントをご覧ください。

# Movie Metadata
# Movie - MovieMetadata is a one-to-one relationship
type MovieMetadata @table {
  # @unique ensures that each Movie only has one MovieMetadata.
  movie: Movie! @unique
  # Since it references to another table type, it adds a foreign key constraint.
  #  movie: Movie! @unique @ref(fields: "movieId", references: "id")
  #  movieId: UUID! <- implicitly added foreign key field
  director: String
}

Actor と MovieActor

次に、俳優が映画に出演するようにします。映画と俳優の間には多対多の関係があるため、結合テーブルを作成します。

# Actors
# Suppose an actor can participate in multiple movies and movies can have multiple actors
# Movie - Actors (or vice versa) is a many to many relationship
type Actor @table {
  id: UUID! @default(expr: "uuidV4()")
  name: String! @col(dataType: "varchar(30)")
}
# Join table for many-to-many relationship for movies and actors
# The 'key' param signifies the primary keys of this table
# In this case, the keys are [movieId, actorId], the foreign key fields of the reference fields [movie, actor]
type MovieActor @table(key: ["movie", "actor"]) {
  movie: Movie!
  # movieId: UUID! <- implicitly added foreign key field
  actor: Actor!
  # actorId: UUID! <- implicitly added foreign key field
  role: String! # "main" or "supporting"
  # optional other fields
}

ユーザー

最後に、アプリのユーザーです。

# Users
# Suppose a user can leave reviews for movies
type User @table {
  id: String! @default(expr: "auth.uid")
  username: String! @col(dataType: "varchar(50)")
}

サポートされるデータタイプ

Data Connect は、@col(dataType:) を使用して PostgreSQL 型に割り当てられる次のスカラー データ型をサポートしています。

Data Connect GraphQL 組み込み型または
Data Connect カスタム型
デフォルトの PostgreSQL タイプ サポートされている PostgreSQL 型
(かっこ内のエイリアス)
文字列 GraphQL テキスト text
bit(n), varbit(n)
char(n), varchar(n)
Int GraphQL 整数 Int2(smallint、smallserial)、
int4(integer、int、serial)
浮動小数点数 GraphQL float8 float4(実数)
float8(倍精度)
数値(小数)
ブール値 GraphQL ブール値 ブール値
UUID カスタム uuid uuid
Int64 カスタム bigint int8(bigint、bigserial)
数値(小数)
日付 カスタム date 日付
タイムスタンプ カスタム timestamptz

timestamptz

注: ローカルのタイムゾーン情報は保存されません。
PostgreSQL は、このようなタイムスタンプを UTC に変換して保存します。

ベクトル カスタム ベクトル

ベクトル

Vertex AI でベクトル類似性検索を実行するをご覧ください。

  • GraphQL の List は 1 次元配列にマッピングされます。
    • たとえば、[Int]int5[] にマッピングされ、[Any]jsonb[] にマッピングされます。
    • Data Connect はネストされた配列をサポートしていません。

生成されたフィールドを使用してクエリとミューテーションを作成する

Data Connect クエリとミューテーションは、スキーマの型と型の関係に基づいて自動生成された Data Connect のフィールドセットを拡張します。これらのフィールドは、スキーマを編集するたびにローカル ツールによって生成されます。

  • スタートガイドで説明したように、Firebase コンソールとローカル開発ツールは、これらの自動生成フィールドを使用して、データのシーディングやテーブルの内容の検証に使用できるアドホック管理クエリとミューテーションを提供します。

  • 開発プロセスでは、これらの自動生成フィールドに基づいて、コネクタにバンドルされたデプロイ可能なクエリデプロイ可能なミューテーションを実装します。

自動生成されたフィールド名

Data Connect は、スキーマ型宣言に基づいて自動生成されるフィールドに適した名前を推論します。たとえば、PostgreSQL ソースで Movie という名前のテーブルを定義すると、サーバーは次を生成します。

  • 単一テーブルのユースケースでデータを読み取るためのフィールド。わかりやすい名前は movie(単数形、eq などの引数を渡して個々の結果を取得する場合)と movies(複数形、gt などの引数と orderby などのオペレーションを渡して結果リストを取得する場合)です。Data Connect は、actors_on_moviesactors_via_actormovie などの明示的な名前を持つマルチテーブル リレーショナル オペレーションのフィールドも生成します。
  • movie_insertmovie_upsert などのわかりやすい名前でデータを書き込むフィールド。

スキーマ定義言語では、singular ディレクティブ引数と plural ディレクティブ引数を使用して、フィールドの名前の生成方法を明示的に制御することもできます。

クエリとミューテーションのディレクティブ

Data Connect には、型とテーブルの定義に使用するディレクティブに加えて、クエリとミューテーションの動作を拡張するための @auth@check@redact@transaction ディレクティブが用意されています。

ディレクティブ 対象 説明
@auth クエリとミューテーション クエリまたはミューテーションの認可ポリシーを定義します。承認と構成証明のガイドをご覧ください。
@check マルチステップ オペレーションの query フィールド 指定したフィールドがクエリ結果に存在することを確認します。フィールド値をテストするには、Common Expression Language(CEL)式を使用します。マルチステップ オペレーションをご覧ください。
@redact クエリ クライアントからのレスポンスの一部を削除します。マルチステップ オペレーションをご覧ください。
@transaction ミューテーション ミューテーションが常にデータベース トランザクション内で実行されるようにします。マルチステップ オペレーションをご覧ください。

映画レビュー データベースに対するクエリ

Data Connect クエリは、クエリ オペレーション タイプの宣言、オペレーション名、0 個以上のオペレーション引数、引数を持つ 0 個以上のディレクティブで定義します。

クイックスタートの例の listEmails クエリではパラメータを使用しませんでした。もちろん、多くの場合、クエリ フィールドに渡されるデータは動的になります。$variableName 構文を使用すると、クエリ定義のコンポーネントの 1 つとして変数を操作できます。

したがって、次のクエリには次のものがあります。

  • query 型定義
  • ListMoviesByGenre オペレーション(クエリ)名
  • 単一変数の $genre オペレーション引数
  • 単一のディレクティブ @auth
query ListMoviesByGenre($genre: String!) @auth(level: USER)

すべてのクエリ引数には、型宣言、String などの組み込み型、または Movie などのスキーマ定義のカスタム型が必要です。

複雑なクエリのシグネチャについて見てみましょう。最後に、デプロイ可能なクエリの作成に使用できる、強力で簡潔なリレーションシップ式について説明します。

クエリ内の主なスカラー

ただし、まずキー スカラーについて説明します。

Data Connect は、_Key で識別されるキー スカラーの特殊な型を定義します。たとえば、Movie テーブルのキー スカラーの型は Movie_Key です。

キー スカラーは、ほとんどの自動生成読み取りフィールドから返されるレスポンスとして取得します。もちろん、スカラーキーの作成に必要なすべてのフィールドを取得したクエリからも取得できます。

実行例の movie などの単一の自動クエリは、キー スカラーを受け入れることができるキー引数をサポートしています。

キースカラーをリテラルとして渡すことができます。ただし、キー スカラーを入力として渡す変数を定義できます。

query GetMovie($myKey: Movie_Key!) {
  movie(key: $myKey) { title }
}

これらは、次のリクエスト JSON(または他のシリアル化形式)で指定できます。

{
  # 
  "variables": {
    "myKey": {"foo": "some-string-value", "bar": 42}
  }
}

カスタム スカラー解析により、オブジェクト構文を使用して Movie_Key を作成することもできます。この構文には変数を含めることができます。これは、なんらかの理由で個々のコンポーネントを異なる変数に分割する場合に特に便利です。

クエリでのエイリアス

Data Connect は、クエリでの GraphQL エイリアシングをサポートしています。エイリアスを使用すると、クエリの結果で返されるデータの名前を変更できます。1 つの Data Connect クエリで、複数のフィルタやその他のクエリ オペレーションを 1 つの効率的なリクエストでサーバーに適用し、複数の「サブクエリ」を一度に効果的に実行できます。返されるデータセットで名前の競合を回避するには、エイリアスを使用してサブクエリを区別します。

式で別名 mostPopular を使用するクエリを次に示します。

query ReviewTopPopularity($genre: String) {
  mostPopular: review(first: {
    where: {genre: {eq: $genre}},
    orderBy: {popularity: DESC}
  }) {  }
}

フィルタを使用した簡単なクエリ

Data Connect クエリは、一般的な SQL フィルタと並べ替え演算にマッピングされます。

where 演算子と orderBy 演算子(単数形、複数形のクエリ)

テーブル内の一致するすべての行(およびネストされた関連付け)を返します。フィルタに一致するレコードがない場合、空の配列を返します。

query MovieByTopRating($genre: String) {
  mostPopular: movies(
     where: { genre: { eq: $genre } }, orderBy: { rating: DESC }
  ) {
    # graphql: list the fields from the results to return
    id
    title
    genre
    description
  }
}

query MoviesByReleaseYear($min: Int, $max: Int) {
  movies(where: {releaseYear: {le: $max, ge: $min}}, orderBy: [{releaseYear: ASC}]) {  }
}

limit 演算子と offset 演算子(単数形、複数形のクエリ)

結果のページネーションを行うことができます。これらの引数は受け入れられますが、結果には返されません。

query MoviesTop10 {
  movies(orderBy: [{ rating: DESC }], limit: 10) {
    # graphql: list the fields from the results to return
    title
  }
}

配列フィールドの includes

配列フィールドに指定されたアイテムが含まれていることをテストできます。

# Filter using arrays and embedded fields.
query ListMoviesByTag($tag: String!) {
  movies(where: { tags: { includes: $tag }}) {
    # graphql: list the fields from the results to return
    id
    title
  }
}

文字列演算と正規表現

クエリでは、正規表現など、一般的な文字列検索と比較オペレーションを使用できます。効率性を高めるために、ここでは複数のオペレーションをバンドルし、エイリアスで曖昧さを解消しています。

query MoviesTitleSearch($prefix: String, $suffix: String, $contained: String, $regex: String) {
  prefixed: movies(where: {title: {startsWith: $prefix}}) {...}
  suffixed: movies(where: {title: {endsWith: $suffix}}) {...}
  contained: movies(where: {title: {contains: $contained}}) {...}
  matchRegex: movies(where: {title: {pattern: {regex: $regex}}}) {...}
}

orand(合成フィルタ用)

より複雑なロジックには orand を使用します。

query ListMoviesByGenreAndGenre($minRating: Int!, $genre: String) {
  movies(
    where: { _or: [{ rating: { ge: $minRating } }, { genre: { eq: $genre } }] }
  ) {
    # graphql: list the fields from the results to return
    title
  }
}

複雑なクエリ

Data Connect クエリは、テーブル間のリレーションに基づいてデータにアクセスできます。スキーマで定義されたオブジェクト(1 対 1)または配列(1 対多)の関係を使用して、ネストされたクエリを作成できます。つまり、1 つのタイプのデータとともに、ネストされたタイプまたは関連するタイプのデータを取得できます。

このようなクエリでは、生成された読み取りフィールドでマジック Data Connect _on_ 構文と _via 構文が使用されます。

最初のバージョンのスキーマを変更します。

多対 1

Review テーブルと User の変更により、アプリにレビューを追加しましょう。

# User table is keyed by Firebase Auth UID.
type User @table {
  # `@default(expr: "auth.uid")` sets it to Firebase Auth UID during insert and upsert.
  id: String! @default(expr: "auth.uid")
  username: String! @col(dataType: "varchar(50)")
  # The `user: User!` field in the Review table generates the following one-to-many query field.
  #  reviews_on_user: [Review!]!
  # The `Review` join table the following many-to-many query field.
  #  movies_via_Review: [Movie!]!
}

# Reviews is a join table tween User and Movie.
# It has a composite primary keys `userUid` and `movieId`.
# A user can leave reviews for many movies. A movie can have reviews from many users.
# User  <-> Review is a one-to-many relationship
# Movie <-> Review is a one-to-many relationship
# Movie <-> User is a many-to-many relationship
type Review @table(name: "Reviews", key: ["movie", "user"]) {
  user: User!
  # The user field adds the following foreign key field. Feel free to uncomment and customize it.
  #  userUid: String!
  movie: Movie!
  # The movie field adds the following foreign key field. Feel free to uncomment and customize it.
  #  movieId: UUID!
  rating: Int
  reviewText: String
  reviewDate: Date! @default(expr: "request.time")
}

多対 1 のクエリ

次に、エイリアスを使用したクエリを見て、_via_ 構文を説明します。

query UserMoviePreferences($username: String!) @auth(level: USER) {
  users(where: { username: { eq: $username } }) {
    likedMovies: movies_via_Review(where: { rating: { ge: 4 } }) {
      title
      genre
    }
    dislikedMovies: movies_via_Review(where: { rating: { le: 2 } }) {
      title
      genre
    }
  }
}

1 対 1

パターンが確認できます。以下では、説明のためにスキーマを変更しています。

# Movies
type Movie
  @table(name: "Movies", singular: "movie", plural: "movies", key: ["id"]) {
  id: UUID! @col(name: "movie_id") @default(expr: "uuidV4()")
  title: String!
  releaseYear: Int @col(name: "release_year")
  genre: String
  rating: Int @col(name: "rating")
  description: String @col(name: "description")
  tags: [String] @col(name: "tags")
}
# Movie Metadata
# Movie - MovieMetadata is a one-to-one relationship
type MovieMetadata
  @table(
    name: "MovieMetadata"
  ) {
  # @ref creates a field in the current table (MovieMetadata) that holds the primary key of the referenced type
  # In this case, @ref(fields: "id") is implied
  movie: Movie! @ref
  # movieId: UUID <- this is created by the above @ref
  director: String @col(name: "director")
}


extend type MovieMetadata {
  movieId: UUID! # matches primary key of referenced type
...
}

extend type Movie {
  movieMetadata: MovieMetadata # can only be non-nullable on ref side
  # conflict-free name, always generated
  movieMetadatas_on_movie: MovieMetadata
}

1 対 1 のクエリ

_on_ 構文を使用してクエリを実行できます。

# One to one
query GetMovieMetadata($id: UUID!) @auth(level: PUBLIC) {
  movie(id: $id) {
    movieMetadatas_on_movie {
      director
    }
  }
}

多対多

映画には俳優が必要で、俳優には映画が必要です。これらは多対多のリレーションシップであり、MovieActors 結合テーブルでモデル化できます。

# MovieActors Join Table Definition
type MovieActors @table(
  key: ["movie", "actor"] # join key triggers many-to-many generation
) {
  movie: Movie!
  actor: Actor!
}

# generated extensions for the MovieActors join table
extend type MovieActors {
  movieId: UUID!
  actorId: UUID!
}

# Extensions for Actor and Movie to handle many-to-many relationships
extend type Movie {
  movieActors: [MovieActors!]! # standard many-to-one relation to join table
  actors: [Actor!]! # many-to-many via join table

  movieActors_on_actor: [MovieActors!]!
  # since MovieActors joins distinct types, type name alone is sufficiently precise
  actors_via_MovieActors: [Actor!]!
}

extend type Actor {
  movieActors: [MovieActors!]! # standard many-to-one relation to join table
  movies: [Movie!]! # many-to-many via join table

  movieActors_on_movie: [MovieActors!]!
  movies_via_MovieActors: [Movie!]!
}

多対多のクエリ

エイリアスを使用したクエリを見てみましょう。_via_ 構文を説明します。

query GetMovieCast($movieId: UUID!, $actorId: UUID!) @auth(level: PUBLIC) {
  movie(id: $movieId) {
    mainActors: actors_via_MovieActor(where: { role: { eq: "main" } }) {
      name
    }
    supportingActors: actors_via_MovieActor(
      where: { role: { eq: "supporting" } }
    ) {
      name
    }
  }
  actor(id: $actorId) {
    mainRoles: movies_via_MovieActor(where: { role: { eq: "main" } }) {
      title
    }
    supportingRoles: movies_via_MovieActor(
      where: { role: { eq: "supporting" } }
    ) {
      title
    }
  }
}

集約クエリ

集計とは何ですか?集計を使用する理由

集計フィールドを使用すると、結果のリストの計算を行うことができます。集計フィールドを使用すると、次のようなことができます。

  • レビューの平均スコアを確認する
  • ショッピング カート内の商品の合計金額を確認する
  • 評価が最も高い商品または評価が最も低い商品を見つける
  • ストアの商品数をカウントする

集計はサーバー上で実行されます。これは、クライアントサイドで計算する場合と比べて次のようなメリットがあります。

  • アプリのパフォーマンスの向上(クライアントサイドの計算が不要になるため)
  • データの下り(外向き)コストの削減(すべての入力ではなく、集計結果のみを送信するため)
  • セキュリティの強化(データセット全体ではなく、集計データへのアクセスをクライアントに許可できるため)

集計のスキーマの例

このセクションでは、集計を使用する方法を説明するために、ストアフロントのサンプル スキーマに切り替えます。

  type Product @table {
    name: String!
    manufacturer: String!
    quantityInStock: Int!
    price: Float!
    expirationDate: Date
  }

単純な集計

すべてのフィールドの _count

最も単純な集計フィールドは _count です。これは、クエリに一致する行数を返します。型内の各フィールドについて、Data Connect はフィールドの型に応じて対応する集計フィールドを生成します。

クエリ

query CountProducts {
  products {
    _count
  }
}

レスポンス
one

たとえば、データベースに 5 つの商品があるとします。結果は次のようになります。

{
  "products": [
    {
    "_count": 5
    }
  ]
}

すべてのフィールドには <field>_count フィールドがあり、そのフィールドに null 以外の値を持つ行の数をカウントします。

クエリ

query CountProductsWithExpirationDate {
  products {
    expirationDate_count
  }
}

レスポンス
field_count

たとえば、有効期限のある商品が 3 つある場合、結果は次のようになります。

{
  "products": [
    {
    "expirationDate_count": 3
    }
  ]
}
数値フィールドの _min、_max、_sum、_avg

数値フィールド(int、float、int64)にも <field>_min<field>_max<field>_sum<field>_avg があります。

クエリ

query NumericAggregates {
  products {
  quantityInStock_max
  price_min
  price_avg
  quantityInStock_sum
  }
}

レスポンス
_min _max _sum _avg

たとえば、次の商品があるとします。

  • プロダクト A: quantityInStock: 10price: 2.99
  • プロダクト B: quantityInStock: 5price: 5.99
  • プロダクト C: quantityInStock: 20price: 1.99

結果は次のようになります。

{
  "products": [
    {
    "quantityInStock_max": 20,
    "price_min": 1.99,
    "price_avg": 3.6566666666666666,
    "quantityInStock_sum": 35
    }
  ]
}
日付とタイムスタンプの _min と _max

日付フィールドとタイムスタンプ フィールドには <field>_min<field>_max があります。

クエリ

query DateAndTimeAggregates {
  products {
  expirationDate_max
  expirationDate_min
  }
}

レスポンス
_min _maxdatetime

たとえば、次の有効期限があるとします。

  • 商品 A: 2024-01-01
  • 商品 B: 2024-03-01
  • 商品 C: 2024-02-01

結果は次のようになります。

{
  "products": [
    {
    "expirationDate_max": "2024-03-01",
    "expirationDate_min": "2024-01-01"
    }
  ]
}

Distinct

distinct 引数を使用すると、フィールド(またはフィールドの組み合わせ)の一意の値をすべて取得できます。例:

クエリ

query ListDistinctManufacturers {
  products(distinct: true) {
    manufacturer
  }
}

レスポンス
distinct

たとえば、次のメーカーがあるとします。

  • 商品 A: manufacturer: "Acme"
  • 商品 B: manufacturer: "Beta"
  • 商品 C: manufacturer: "Acme"

結果は次のようになります。

{
  "products": [
    { "manufacturer": "Acme" },
    { "manufacturer": "Beta" }
  ]
}

集計フィールドで distinct 引数を使用して、一意の値を集計することもできます。例:

クエリ

query CountDistinctManufacturers {
  products {
    manufacturer_count(distinct: true)
  }
}

レスポンス
distinctonaggregate

たとえば、次のメーカーがあるとします。

  • 商品 A: manufacturer: "Acme"
  • 商品 B: manufacturer: "Beta"
  • 商品 C: manufacturer: "Acme"

結果は次のようになります。

{
  "products": [
    {
    "manufacturer_count": 2
    }
  ]
}

グループ化された集計

グループ化集計を実行するには、タイプで集計フィールドと非集計フィールドを組み合わせて選択します。これにより、集計以外のフィールドの値が同じ一致する行がすべてグループ化され、そのグループの集計フィールドが計算されます。例:

クエリ

query MostExpensiveProductByManufacturer {
  products {
  manufacturer
  price_max
  }
}

レスポンス
groupedaggregates

たとえば、次の商品があるとします。

  • プロダクト A: manufacturer: "Acme"price: 2.99
  • プロダクト B: manufacturer: "Beta"price: 5.99
  • プロダクト C: manufacturer: "Acme"price: 1.99

結果は次のようになります。

{
  "products": [
    { "manufacturer": "Acme", "price_max": 2.99 },
    { "manufacturer": "Beta", "price_max": 5.99 }
  ]
}
グループ化された集計を含む havingwhere

having 引数と where 引数を使用して、指定した条件を満たすグループのみを返すこともできます。

  • having を使用すると、グループを集計フィールドでフィルタできます。
  • where を使用すると、集計以外のフィールドに基づいて行をフィルタできます。

クエリ

query FilteredMostExpensiveProductByManufacturer {
  products(having: {price_max: {ge: 2.99}}) {
  manufacturer
  price_max
  }
}

レスポンス
havingwhere

たとえば、次の商品があるとします。

  • プロダクト A: manufacturer: "Acme"price: 2.99
  • プロダクト B: manufacturer: "Beta"price: 5.99
  • プロダクト C: manufacturer: "Acme"price: 1.99

結果は次のようになります。

{
  "products": [
    { "manufacturer": "Acme", "price_max": 2.99 },
    { "manufacturer": "Beta", "price_max": 5.99 }
  ]
}

テーブル間の集計

集計フィールドは、生成された 1 対多の関係フィールドと組み合わせて使用することで、データに関する複雑な質問に回答できます。以下は、例で使用できる個別のテーブル Manufacturer を含む変更済みスキーマです。

  type Product @table {
    name: String!
    manufacturer: Manufacturer!
    quantityInStock: Int!
    price: Float!
    expirationDate: Date
  }

  type Manufacturer @table {
    name: String!
    headquartersCountry: String!
  }

これで、集計フィールドを使用して、メーカーが製造する商品の数などを調べることができます。

クエリ

query GetProductCount($id: UUID) {
  manufacturers {
    name
    products_on_manufacturer {
      _count
    }
  }
}

レスポンス
aggregatesacrosstables

たとえば、次のメーカーがあるとします。

  • メーカー A: name: "Acme"products_on_manufacturer: 2
  • メーカー B: name: "Beta"products_on_manufacturer: 1

結果は次のようになります。

{
  "manufacturers": [
    { "name": "Acme", "products_on_manufacturer": { "_count": 2 } },
    { "name": "Beta", "products_on_manufacturer": { "_count": 1 } }
  ]
}

映画レビュー データベースのミューテーション

前述のように、スキーマでテーブルを定義すると、Data Connect は各テーブルに基本的な _insert_update などのフィールドを生成します。

type Movie @table { ... }

extend type Mutation {
  # Insert a row into the movie table.
  movie_insert(...): Movie_Key!
  # Upsert a row into movie."
  movie_upsert(...): Movie_Key!
  # Update a row in Movie. Returns null if a row with the specified id/key does not exist
  movie_update(...): Movie_Key
  # Update rows based on a filter in Movie.
  movie_updateMany(...): Int!
  # Delete a single row in Movie. Returns null if a row with the specified id/key does not exist
  movie_delete(...): Movie_Key
  # Delete rows based on a filter in Movie.
  movie_deleteMany(...): Int!
}

これにより、コア CRUD の複雑なケースを実装できます。それを 5 回続けて言ってみましょう。

作成

基本的な作成を行いましょう。

# Create a movie based on user input
mutation CreateMovie($title: String!, $releaseYear: Int!, $genre: String!, $rating: Int!) {
  movie_insert(data: {
    title: $title
    releaseYear: $releaseYear
    genre: $genre
    rating: $rating
  })
}

# Create a movie with default values
mutation CreateMovie2 {
  movie_insert(data: {
    title: "Sherlock Holmes"
    releaseYear: 2009
    genre: "Mystery"
    rating: 5
  })
}

または upsert です。

# Movie upsert using combination of variables and literals
mutation UpsertMovie($title: String!) {
  movie_upsert(data: {
    title: $title
    releaseYear: 2009
    genre: "Mystery"
    rating: 5
    genre: "Mystery/Thriller"
  })
}

更新を実行する

最新情報は次のとおりです。プロデューサーとディレクターは、平均評価がトレンドに沿っていることを願っています。

movie_update フィールドには、レコードを識別する id 引数と、この更新で値の設定に使用できる data フィールドが含まれています。

mutation UpdateMovie(
  $id: UUID!,
  $genre: String!,
  $rating: Int!,
  $description: String!
) {
  movie_update(id: $id,
    data: {
      genre: $genre
      rating: $rating
      description: $description
    })
}

複数の更新を行うには、movie_updateMany フィールドを使用します。

# Multiple updates (increase all ratings of a genre)
mutation IncreaseRatingForGenre($genre: String!, $rating: Int!) {
  movie_updateMany(
    where: { genre: { eq: $genre } },
    data:
      {
        rating: $rating
      })
}

_update でインクリメント、デクリメント、追加、先頭追加のオペレーションを使用する

_update ミューテーションと _updateMany ミューテーションでは data: で値を明示的に設定できますが、多くの場合、インクリメントなどの演算子を適用して値を更新することをおすすめします。

前の更新例を変更して、特定の映画の評価を増やすとします。rating_update 構文は、inc 演算子で使用できます。

mutation UpdateMovie(
  $id: UUID!,
  $ratingIncrement: Int!
) {
  movie_update(id: $id, data: {
    rating_update: {
      inc: $ratingIncrement
    }
  })
}

Data Connect は、フィールドの更新に次の演算子をサポートしています。

  • inc: IntInt64FloatDateTimestamp のデータ型をインクリメントします。
  • dec: IntInt64FloatDateTimestamp のデータ型を減算します。

リストの場合は、次のいずれかを使用して、個々の値または値のリストで更新することもできます。

  • add: リストタイプにアイテムがまだ存在しない場合は、アイテムを追加します(ベクトルリストを除く)
  • remove: リストタイプからすべてのアイテムを削除します(ベクトルリストを除く)
  • append: リスト型(ベクトルリストを除く)にアイテムを追加します。
  • prepend: ベクトルリストを除くリスト型にアイテムを先頭に追加します。

削除を実行する

もちろん、映画データは削除できます。映画の保存者は、物理的なフィルムをできるだけ長く維持したいと考えています。

# Delete by key
mutation DeleteMovie($id: UUID!) {
  movie_delete(id: $id)
}

ここでは _deleteMany を使用できます。

# Multiple deletes
mutation DeleteUnpopularMovies($minRating: Int!) {
  movie_deleteMany(where: { rating: { le: $minRating } })
}

リレーションにミューテーションを書き込む

関係で暗黙的な _upsert ミューテーションを使用する方法を確認します。

# Create or update a one to one relation
mutation MovieMetadataUpsert($movieId: UUID!, $director: String!) {
  movieMetadata_upsert(
    data: { movie: { id: $movieId }, director: $director }
  )
}

Data Connectfield_expr 構文を使用して値を提供する

キー スカラーとサーバー値で説明したように、クライアント リクエストに応じて、id や日付などの一般的なフィールドにサーバーが値を入力するようにスキーマを設計できます。

また、クライアント アプリから Data Connect request オブジェクトで送信されたユーザー ID などのデータを利用することもできます。

ミューテーションを実装する場合は、field_expr 構文を使用して、サーバー生成の更新をトリガーするか、リクエストからデータにアクセスします。たとえば、リクエストに保存されている承認 uid_upsert オペレーションに渡すには、userId_expr フィールドに "auth.uid" を渡します。

# Add a movie to the user's favorites list
mutation AddFavoritedMovie($movieId: UUID!) @auth(level: USER) {
  favorite_movie_upsert(data: { userId_expr: "auth.uid", movieId: $movieId })
}

# Remove a movie from the user's favorites list
mutation DeleteFavoritedMovie($movieId: UUID!) @auth(level: USER) {
  favorite_movie_delete(key: { userId_expr: "auth.uid", movieId: $movieId })
}

または、よくある To-Do リスト アプリで新しい To-Do リストを作成するときに、id_expr を渡して、リストの UUID を自動生成するようサーバーに指示することもできます。

mutation CreateTodoListWithFirstItem(
  $listName: String!
) @transaction {
  # Step 1
  todoList_insert(data: {
    id_expr: "uuidV4()", # <-- auto-generated. Or a column-level @default on `type TodoList` will also work
    name: $listName,
  })
}

詳細については、スカラー リファレンス_Expr スカラーをご覧ください。

マルチステップ オペレーション

1 つのミューテーションに複数の書き込みフィールド(挿入など)を含める必要がある場合がよくあります。ミューテーションの実行中にデータベースを読み取り、挿入や更新などの操作を行う前に既存のデータを検索して検証することもできます。これらのオプションを使用すると、往復オペレーションが不要になり、コストを削減できます。

Data Connect を使用すると、次の機能をサポートすることで、ミューテーションでマルチステップ ロジックを実行できます。

  • 複数の書き込みフィールド

  • ミューテーション内の複数の読み取りフィールド(query フィールド キーワードを使用)。

  • @transaction ディレクティブ。リレーショナル データベースでよく使用されるトランザクション サポートを提供します。

  • @check ディレクティブ: CEL 式を使用して読み取りの内容を評価し、その評価結果に基づいて次の処理を行います。

    • ミューテーションで定義された作成、更新、削除を続行する
    • クエリフィールドの結果を返す
    • 返されたメッセージを使用して、クライアント コードで適切なロジックを実行する
  • @redact ディレクティブ。ワイヤー プロトコルの結果からクエリフィールドの結果を省略できます。

  • CEL response バインディング。複雑なマルチステップ オペレーションで実行されたすべてのミューテーションとクエリの累積結果を保存します。response バインディングにアクセスできます。

    • @check ディレクティブの expr: 引数
    • サーバー値で field_expr 構文を使用する

@transaction ディレクティブ

複数ステップのミューテーションのサポートには、トランザクションを使用したエラー処理が含まれます。

@transaction ディレクティブは、単一の書き込みフィールド(_insert_update など)または複数の書き込みフィールドを使用するミューテーションが、常にデータベース トランザクションで実行されるようにします。

  • @transaction のないミューテーションは、各ルートフィールドを順番に実行します。オペレーションは、エラーを部分フィールド エラーとして表示しますが、後続の実行の影響は表示しません。

  • @transaction を使用したミューテーションは、完全に成功するか、完全に失敗することを保証します。トランザクション内のいずれかのフィールドでエラーが発生すると、トランザクション全体がロールバックされます。

@check ディレクティブと @redact ディレクティブ

@check ディレクティブは、指定したフィールドがクエリ結果に存在することを確認します。フィールド値のテストには、Common Expression Language(CEL)式を使用します。ディレクティブのデフォルトの動作は、値が null または [] のノードをチェックし、拒否することです(空のリスト)。

@redact ディレクティブは、クライアントからのレスポンスの一部を削除します。除去されたフィールドは、副作用(データの変更や @check など)について引き続き評価され、その結果は CEL 式の後続のステップで引き続き使用できます。

@check@check(message:)@redact を使用する

@check@redact の主な用途は、関連データを検索して、特定のオペレーションを承認する必要があるかどうかを決定することです。この検索はロジックで行われますが、クライアントからは隠されます。クエリは、クライアント コードで適切に処理するために役立つメッセージを返すことができます。

たとえば、次のクエリフィールドは、リクエスト元に映画を編集できるユーザーを表示するための適切な「管理者」ロールがあるかどうかを確認します。

query GetMovieEditors($movieId: UUID!) @auth(level: USER) {
  moviePermission(key: { movieId: $movieId, userId_expr: "auth.uid" }) @redact {
    role @check(expr: "this == 'admin'", message: "You must be an admin to view all editors of a movie.")
  }
  moviePermissions(where: { movieId: { eq: $movieId }, role: { eq: "editor" } }) {
    user {
      id
      username
    }
  }
}

承認チェックの @check ディレクティブと @redact ディレクティブの詳細については、承認データの検索に関する説明をご覧ください。

@check を使用して鍵を検証する

_update などの一部のミューテーション フィールドは、指定されたキーを持つレコードが存在しない場合、無効になることがあります。同様に、ルックアップから null または空のリストが返されることもあります。これらはエラーと見なされないため、ロールバックはトリガーされません。

この結果を防ぐには、@check ディレクティブを使用してキーが見つかるかどうかをテストします。

# Delete by key, error if not found
mutation MustDeleteMovie($id: UUID!) @transaction {
  movie_delete(id: $id) @check(expr: "this != null", message: "Movie not found, therefore nothing is deleted")
}

response バインディングを使用して複数のステップのミューテーションを連結する

新しい Movie と関連付けられた MovieMetadata エントリなどの関連レコードを作成する基本的な方法は次のとおりです。

  1. Movie_insert ミューテーションを呼び出す
  2. 作成されたムービーの返された鍵を保存する
  3. 次に、2 つ目の _insert ミューテーションを呼び出して MovieMetadata レコードを作成します。

一方、Data Connect を使用すると、2 番目の _insert で最初の _insert結果にアクセスすることで、この一般的なケースを 1 つのマルチステップ オペレーションで処理できます。

成功する映画レビュー アプリを作成するには、多くの作業が必要です。新しい例で、ToDo リストを追跡してみましょう。

response を使用してサーバー値を持つフィールドを設定する

次の To-Do リストのミューテーションでは、

  • response バインディングは、これまでの部分的なレスポンス オブジェクトを表します。これには、現在のミューテーションの前にすべての最上位ミューテーション フィールドが含まれます。
  • 最初の todoList_insert オペレーションの結果(id(キー)フィールドを返す)は、後で response.todoList_insert.id でアクセスされるため、新しい ToDo アイテムをすぐに挿入できます。
mutation CreateTodoListWithFirstItem(
  $listName: String!,
  $itemContent: String!
) @transaction {
  # Sub-step 1:
  todoList_insert(data: {
    id_expr: "uuidV4()", # <-- auto-generated. Or a column-level @default on `type TodoList` will also work
    name: $listName,
  })
  # Sub-step 2:
  todo_insert(data: {
    listId_expr: "response.todoList_insert.id" # <-- Grab the newly generated ID from the partial response so far.
    content: $itemContent,
  })
}

response を使用して @check でフィールドを検証する

response@check(expr: "...") でも使用できるため、さらに複雑なサーバーサイド ロジックを構築できます。ミューテーションの query { … } ステップと組み合わせることで、クライアントとサーバーの間の追加のラウンドトリップなしで、さらに多くのことを実現できます。

次の例では、@check は常に接続されているステップの後に実行されるため、@check はすでに response.query にアクセスできることに注意してください。

mutation CreateTodoInNamedList(
  $listName: String!,
  $itemContent: String!
) @transaction {
  # Sub-step 1: Look up List.id by its name
  query
  @check(expr: "response.query.todoLists.size() > 0", message: "No such TodoList with the name!")
  @check(expr: "response.query.todoLists.size() < 2", message: "Ambiguous listName!") {
    todoLists(where: { name: $listName }) {
      id
    }
  }
  # Sub-step 2:
  todo_insert(data: {
    listId_expr: "response.todoLists[0].id" # <-- Now we have the parent list ID to insert to
    content: $itemContent,
  })
}

response バインディングの詳細については、CEL リファレンスをご覧ください。

@transactionquery @check で中断されたオペレーションを理解する

マルチステップ ミューテーションでは、次のようなエラーが発生する可能性があります。

  • データベース オペレーションが失敗する可能性があります。
  • クエリ @check ロジックによってオペレーションが終了することがあります。

Data Connect では、マルチステップ ミューテーションで @transaction ディレクティブを使用することをおすすめします。これにより、データベースとミューテーションの結果の一貫性が向上し、クライアント コードで簡単に処理できるようになります。

  • 最初のエラーまたは @check の失敗でオペレーションが終了するため、後続のフィールドの実行や CEL の評価を管理する必要はありません。
  • ロールバックは、データベース エラーまたは @check ロジックに応じて実行され、データベースの状態が一貫したものになります。
  • ロールバック エラーは常にクライアント コードに返されます。

@transaction を使用しない場合もあります。たとえば、より高いスループット、スケーラビリティ、可用性が必要な場合は、結果整合性を選択できます。ただし、結果を許可するには、データベースとクライアント コードを管理する必要があります。

  • データベース オペレーションが原因で 1 つのフィールドが失敗した場合、後続のフィールドは引き続き実行されます。ただし、失敗した @check は、オペレーション全体を終了します。
  • ロールバックは実行されません。つまり、一部の更新が成功し、一部の更新が失敗した、混合データベース状態になります。
  • @check ロジックで前のステップの読み取り結果や書き込み結果を使用すると、@check を使用したオペレーションで結果の不整合がさらに発生する可能性があります。
  • クライアント コードに返される結果には、処理が必要な成功レスポンスと失敗レスポンスが複雑に混在します。

同等の SQL スキーマ

これは説明のみを目的としています。
-- Movies Table
CREATE TABLE Movies (
    movie_id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    release_year INT,
    genre VARCHAR(30),
    rating INT,
    description TEXT,
    tags TEXT[]
);
-- Movie Metadata Table
CREATE TABLE MovieMetadata (
    movie_id UUID REFERENCES Movies(movie_id) UNIQUE,
    director VARCHAR(255) NOT NULL,
    PRIMARY KEY (movie_id)
);
-- Actors Table
CREATE TABLE Actors (
    actor_id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
    name VARCHAR(30) NOT NULL
);
-- MovieActor Join Table for Many-to-Many Relationship
CREATE TABLE MovieActor (
    movie_id UUID REFERENCES Movies(movie_id),
    actor_id UUID REFERENCES Actors(actor_id),
    role VARCHAR(50) NOT NULL, # "main" or "supporting"
    PRIMARY KEY (movie_id, actor_id),
    FOREIGN KEY (movie_id) REFERENCES Movies(movie_id),
    FOREIGN KEY (actor_id) REFERENCES Actors(actor_id)
);
-- Users Table
CREATE TABLE Users (
    user_id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
    user_auth VARCHAR(255) NOT NULL
    username VARCHAR(30) NOT NULL
);
-- Reviews Table
CREATE TABLE Reviews (
    review_id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
    user_id UUID REFERENCES Users(user_id),
    movie_id UUID REFERENCES Movies(movie_id),
    rating INT,
    review_text TEXT,
    review_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE (movie_id, user_id)
    FOREIGN KEY (user_id) REFERENCES Users(user_id),
    FOREIGN KEY (movie_id) REFERENCES Movies(movie_id)
);
-- Self Join Example for Movie Sequel Relationship
ALTER TABLE Movies
ADD COLUMN sequel_to UUID REFERENCES Movies(movie_id);

次のステップ