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: singularplural 인수를 사용하여 작업 이름을 설정할 수 있습니다.
  • @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가 스키마의 키 필드에서 자동으로 어셈블하는 간결한 객체 식별자입니다. 키 스칼라는 효율성에 관한 것으로, 단일 호출에서 데이터의 ID와 구조에 관한 정보를 찾을 수 있습니다. 새 레코드에 순차적 작업을 실행하고 향후 작업에 전달할 고유 식별자가 필요한 경우와 추가적인 복잡한 작업을 실행하기 위해 관계형 키에 액세스하려는 경우에 특히 유용합니다.

ID 유형

GraphQL에서 ID 유형은 문자열로 직렬화되는 불투명 유형으로 정의됩니다. GraphQL은 ID 형식을 알 수 없지만 입력에서 문자열과 정수를 강제합니다.

PostgreSQL 키는 일반적으로 문자열이 아닌 정수 또는 UUID입니다. Data Connect는 스키마에서 이러한 키를 자동으로 생성합니다. @default 지시문을 사용하여 키 생성을 맞춤설정할 수 있습니다. 액터 표의 id 필드 정의 id: ID! … @default(generate: "UUID")를 참고하세요.

영화 메타데이터 테이블

이제 영화 감독을 추적하고 Movie와의 일대일 관계를 설정해 보겠습니다.

@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 type GraphQL 기본 제공 유형 또는
Data Connect 맞춤 유형
기본 PostgreSQL 유형 지원되는 PostgreSQL 유형
(괄호 안의 별칭)
문자열 GraphQL 텍스트 text
bit(n), varbit(n)
char(n), varchar(n)
정수 GraphQL int Int2 (smallint, smallserial),
int4 (integer, int, serial)
부동 소수점 수 GraphQL float8 float4 (실수)
float8 (배정밀도)
numeric (10진수)
불리언 GraphQL 부울 부울
UUID 커스텀 uuid uuid
Int64 커스텀 bigint int8 (bigint, bigserial)
numeric (decimal)
날짜 커스텀 date 날짜
타임스탬프 커스텀 timestamptz

timestamptz

참고: 현지 시간대 정보는 저장되지 않습니다.
PostgreSQL은 이러한 타임스탬프를 UTC로 변환하여 저장합니다.

열거 커스텀 enum

enum

벡터 커스텀 vector

벡터

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 Connectactors_on_movies 또는 actors_via_actormovie과 같은 명시적 이름으로 다중 테이블, 관계형 작업의 쿼리를 생성합니다.
  • movie_insert, movie_upsert 등의 이름이 지정된 변이

스키마 정의 언어를 사용하면 singularplural 지시어 인수를 사용하여 작업 이름을 명시적으로 설정할 수도 있습니다.

영화 리뷰 데이터베이스 쿼리

쿼리 작업 유형 선언, 작업 이름, 0개 이상의 작업 인수, 인수가 있는 0개 이상의 지시어를 사용하여 Data Connect 쿼리를 정의합니다.

빠른 시작에서 예시 listEmails 쿼리는 매개변수를 사용하지 않았습니다. 물론 쿼리 필드에 전달되는 데이터는 동적인 경우가 많습니다. $variableName 문법을 사용하여 변수를 쿼리 정의의 구성요소 중 하나로 사용할 수 있습니다.

따라서 다음 쿼리에는 다음이 포함됩니다.

  • 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 별칭을 지원합니다. 별칭을 사용하면 쿼리 결과로 반환되는 데이터의 이름을 바꿀 수 있습니다. 단일 Data Connect 쿼리는 서버에 대한 하나의 효율적인 요청에서 여러 필터 또는 기타 쿼리 작업을 적용하여 여러 '하위 쿼리'를 한 번에 효과적으로 실행할 수 있습니다. 반환된 데이터 세트에서 이름 충돌을 방지하려면 별칭을 사용하여 하위 쿼리를 구분합니다.

다음은 표현식에서 별칭 mostPopular을 사용하는 쿼리입니다.

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

필터가 있는 간단한 쿼리

Data Connect 쿼리는 모든 일반적인 SQL 필터 및 순서 작업에 매핑됩니다.

whereorderBy 연산자 (단수, 복수 쿼리)

표 (및 중첩된 연결)에서 일치하는 모든 행을 반환합니다. 필터와 일치하는 레코드가 없으면 빈 배열을 반환합니다.

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}]) {  }
}

limitoffset 연산자 (단수, 복수 쿼리)

결과에 페이지로 나누기를 실행할 수 있습니다. 이러한 인수는 허용되지만 결과에 반환되지는 않습니다.

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 쿼리는 테이블 간의 관계를 기반으로 데이터에 액세스할 수 있습니다. 스키마에 정의된 객체 (일대일) 또는 배열 (일대다) 관계를 사용하여 중첩된 쿼리를 만들 수 있습니다. 즉, 중첩되거나 관련 유형의 데이터와 함께 한 유형의 데이터를 가져올 수 있습니다.

이러한 쿼리는 생성된 암시적 쿼리에서 매직 Data Connect _on__via 구문을 사용합니다.

초기 버전의 스키마를 수정합니다.

다대일

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

다대일 쿼리

이제 별칭을 사용하여 _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
    }
  }
}

일대일

패턴을 확인할 수 있습니다. 아래에서는 설명을 위해 스키마가 수정되었습니다.

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

일대일 쿼리

_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 사례를 구현할 수 있습니다. 이 문장을 다섯 번 빠르게 말해 보세요.

만들기

기본 생성 작업을 해 보겠습니다.

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

다음 단계