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

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

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

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

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

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

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

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

映画の表

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

  • @table: singular 引数と plural 引数を使用してオペレーション名を設定できます。
  • @col - 列名を明示的に設定
  • @default: デフォルトの設定を許可します。
# 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")
}

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

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

サーバー値を使用すると、サーバー側の特定の式に従って、格納されている値や計算しやすい値を使用して、サーバーがテーブルのフィールドに動的に値を入力することができます。たとえば、式 updatedAt: Timestamp! @default(expr: "request.time") を使用して、フィールドにアクセスされたときにタイムスタンプが適用されるフィールドを定義できます。

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

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

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

@ref ディレクティブを追加して関係を定義します。

# 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")
}

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(name: "Actors", singular: "actor", plural: "actors") {
  id: UUID! @col(name: "actor_id") @default(expr: "uuidV4()")
  name: String! @col(name: "name", dataType: "varchar(30)")
}
# Join table for many-to-many relationship for movies and actors
# The 'key' param signifies the primary key(s) of this table
# In this case, the keys are [movieId, actorId], the generated fields of the reference types [movie, actor]

type MovieActor @table(key: ["movie", "actor"]) {
  # @ref creates a field in the current table (MovieActor) 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, see: implicit.gql
  actor: Actor! @ref
  # actorId: UUID! <- this is created by the above @ref, see: implicit.gql
  role: String! @col(name: "role") # "main" or "supporting"
  # optional other fields
}

ユーザー

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

# Users
# Suppose a user can leave reviews for movies
# user:reviews is a one to many relationship, movie:reviews is a one to many relationship, movie:user is a many to many relationship
type User
  @table(name: "Users", singular: "user", plural: "users", key: ["id"]) {
  id: UUID! @col(name: "user_id") @default(expr: "uuidV4()")
  auth: String @col(name: "user_auth") @default(expr: "auth.uid")
  username: String! @col(name: "username", dataType: "varchar(30)")
  # The following are generated from the @ref in the Review table
  # reviews_on_user
  # movies_via_Review
}

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

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 浮動小数点数 8 float4(実数)
float8(倍精度)
数値(小数)
ブール値 GraphQL ブール値 ブール値
UUID カスタム uuid uuid
Int64 カスタム bigint int8(bigint、bigserial)
数値(10 進数)
日付 カスタム date 日付
タイムスタンプ カスタム timestamptz

timestamptz

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

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

ベクトル

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

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

暗黙のクエリと事前定義されたクエリとミューテーション

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

開発プロセスでは、これらの暗黙的なオペレーションに基づいて、事前定義されたクエリ事前定義されたミューテーションを実装します。

暗黙的なクエリとミューテーションの名前付け

Data Connect は、スキーマ型宣言から暗黙的なクエリとミューテーションに適した名前を推論します。たとえば、PostgreSQL ソースで Movie という名前のテーブルを定義すると、サーバーは暗黙的に次のものを生成します。

  • 単一テーブルのユースケースのクエリ。わかりやすい名前は movie(単数形、eq などの引数を渡して個々の結果を取得する場合)と movies(複数形、gt などの引数と orderby などのオペレーションを渡して結果リストを取得する場合)です。Data Connect は、actors_on_moviesactors_via_actormovie などの明示的な名前を持つマルチテーブル リレーショナル オペレーションのクエリも生成します。
  • movie_insertmovie_upsert という名前のミューテーション...

スキーマ定義言語では、singular ディレクティブ引数と plural ディレクティブ引数を使用して、オペレーションの名前を明示的に設定することもできます。

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

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 の変更により、アプリにレビューを追加しましょう。

# Users
# Suppose a user can leave reviews for movies
# user:reviews is a one to many relationship,
# movie:reviews is a one to many relationship,
# movie:user is a many to many relationship
type User
  @table(name: "Users", singular: "user", plural: "users", key: ["id"]) {
  id: UUID! @col(name: "user_id") @default(expr: "uuidV4()")
  auth: String @col(name: "user_auth") @default(expr: "auth.uid")
  username: String! @col(name: "username", dataType: "varchar(30)")
  # The following are generated from the @ref in the Review table
  # reviews_on_user
  # movies_via_Review
}
# Reviews
type Review @table(name: "Reviews", key: ["movie", "user"]) {
  id: UUID! @col(name: "review_id") @default(expr: "uuidV4()")
  user: User! @ref
  movie: Movie! @ref
  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
      description
    }
    dislikedMovies: movies_via_review(where: { rating: { le: 2 } }) {
      title
      genre
      description
    }
  }
}

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

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

前述のように、スキーマでテーブルを定義すると、Data Connect は各テーブルに対して基本的な暗黙的なミューテーションを生成します。

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

更新を実行する

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

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

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

削除を実行する

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

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

同等の 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);

次のステップ