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 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 によって生成された一連の暗黙的なクエリ暗黙的なミューテーションを拡張します。暗黙的なクエリとミューテーションは、スキーマを編集するたびにローカル ツールによって生成されます。

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

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

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 認可データのルックアップ クエリ 指定したフィールドがクエリ結果に存在することを確認します。フィールド値をテストするには、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 の変更により、アプリにレビューを追加しましょう。

# 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 回続けて言ってみましょう。

@transaction ディレクティブ

このディレクティブにより、ミューテーションは常にデータベース トランザクション内で実行されます。

@transaction を使用したミューテーションは、完全に成功するか、完全に失敗するかのどちらかになります。トランザクション内のいずれかのフィールドでエラーが発生すると、トランザクション全体がロールバックされます。クライアントの観点から、失敗は、リクエスト全体がリクエスト エラーで失敗し、実行が開始されていない場合と同様に動作します。

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

作成

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

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

認可データのルックアップ クエリ

Data Connect ミューテーションを承認するには、まずデータベースにクエリを実行し、CEL 式でクエリの結果を確認します。これは、テーブルに書き込みを行う際に、別のテーブルの行の内容を確認する必要がある場合に便利です。

この機能は次のものをサポートしています。

  • @check ディレクティブ: フィールドの内容を評価し、その評価結果に基づいて次の処理を行います。
    • ミューテーションで定義された作成、更新、削除を続行します。
    • クエリによってクライアントに返された値を使用して、クライアントでさまざまなロジックを実行する
  • @redact ディレクティブ。ワイヤー プロトコルの結果からクエリ結果を省略できます。

これらの機能は、認可フローに役立ちます。

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

次のステップ