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 注: ローカルのタイムゾーン情報は保存されません。 |
ベクトル | カスタム | ベクトル | ベクトル 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_movies
やactors_via_actormovie
などの明示的な名前を持つマルチテーブル リレーショナル オペレーションのフィールドも生成します。 movie_insert
、movie_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}}}) {...}
}
or
と and
(合成フィルタ用)
より複雑なロジックには or
と and
を使用します。
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
one
たとえば、データベースに 5 つの商品があるとします。結果は次のようになります。
{
"products": [
{
"_count": 5
}
]
}
すべてのフィールドには <field>_count
フィールドがあり、そのフィールドに null 以外の値を持つ行の数をカウントします。
クエリ
query CountProductsWithExpirationDate {
products {
expirationDate_count
}
}
レスポンスfield_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
_min _max _sum _avg
たとえば、次の商品があるとします。
- プロダクト A:
quantityInStock: 10
、price: 2.99
- プロダクト B:
quantityInStock: 5
、price: 5.99
- プロダクト C:
quantityInStock: 20
、price: 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
_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
distinct
たとえば、次のメーカーがあるとします。
- 商品 A:
manufacturer: "Acme"
- 商品 B:
manufacturer: "Beta"
- 商品 C:
manufacturer: "Acme"
結果は次のようになります。
{
"products": [
{ "manufacturer": "Acme" },
{ "manufacturer": "Beta" }
]
}
集計フィールドで distinct
引数を使用して、一意の値を集計することもできます。例:
クエリ
query CountDistinctManufacturers {
products {
manufacturer_count(distinct: true)
}
}
レスポンスdistinctonaggregate
distinctonaggregate
たとえば、次のメーカーがあるとします。
- 商品 A:
manufacturer: "Acme"
- 商品 B:
manufacturer: "Beta"
- 商品 C:
manufacturer: "Acme"
結果は次のようになります。
{
"products": [
{
"manufacturer_count": 2
}
]
}
グループ化された集計
グループ化集計を実行するには、タイプで集計フィールドと非集計フィールドを組み合わせて選択します。これにより、集計以外のフィールドの値が同じ一致する行がすべてグループ化され、そのグループの集計フィールドが計算されます。例:
クエリ
query MostExpensiveProductByManufacturer {
products {
manufacturer
price_max
}
}
レスポンスgroupedaggregates
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 }
]
}
グループ化された集計を含む having
と where
having
引数と where
引数を使用して、指定した条件を満たすグループのみを返すこともできます。
having
を使用すると、グループを集計フィールドでフィルタできます。where
を使用すると、集計以外のフィールドに基づいて行をフィルタできます。
クエリ
query FilteredMostExpensiveProductByManufacturer {
products(having: {price_max: {ge: 2.99}}) {
manufacturer
price_max
}
}
レスポンスhavingwhere
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
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
:Int
、Int64
、Float
、Date
、Timestamp
のデータ型をインクリメントします。dec
:Int
、Int64
、Float
、Date
、Timestamp
のデータ型を減算します。
リストの場合は、次のいずれかを使用して、個々の値または値のリストで更新することもできます。
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 Connect に field_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
エントリなどの関連レコードを作成する基本的な方法は次のとおりです。
Movie
の_insert
ミューテーションを呼び出す- 作成されたムービーの返された鍵を保存する
- 次に、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 リファレンスをご覧ください。
@transaction
と query @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);
次のステップ
- 認可と構成証明を使用してクエリとミューテーションを保護する方法を確認する。
- 自動生成された ウェブ SDK、Android SDK、iOS SDK、Flutter SDK からクエリとミューテーションを呼び出す方法を学びます。