Схемы подключения к данным, запросы и мутации

Firebase Data Connect позволяет создавать коннекторы для ваших экземпляров PostgreSQL, управляемых с помощью Google Cloud SQL. Эти соединители представляют собой комбинацию схемы, запросов и мутаций для использования ваших данных.

В руководстве по началу работы представлена ​​схема приложения для просмотра фильмов для PostgreSQL, а в этом руководстве более подробно рассматривается разработка схем Data Connect для PostgreSQL.

В этом руководстве запросы и мутации Data Connect сочетаются с примерами схем. Зачем обсуждать запросымутации ) в руководстве по схемам Data Connect ? Как и другие платформы на основе GraphQL, Firebase Data Connect — это платформа разработки, ориентированная на запросы , поэтому, как разработчик, при моделировании данных вы будете думать о данных, которые нужны вашим клиентам, что сильно повлияет на схему данных, которую вы разрабатываете для своих клиентов. проект.

Это руководство начинается с новой схемы для обзоров фильмов , затем рассматриваются запросы и изменения, полученные на основе этой схемы, и, наконец, приводится список SQL, эквивалентный базовой схеме Data Connect .

Схема приложения для обзора фильмов

Представьте, что вы хотите создать сервис, который позволит пользователям отправлять и просматривать обзоры фильмов.

Вам нужна исходная схема для такого приложения. Позже вы расширите эту схему для создания сложных реляционных запросов.

Стол для фильмов

Схема фильмов содержит такие основные директивы, как:

  • @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 автоматически собирает из ключевых полей в ваших схемах. Ключевые скаляры связаны с эффективностью, позволяя вам за один вызов найти информацию о личности и структуре ваших данных. Они особенно полезны, когда вы хотите выполнить последовательные действия над новыми записями и вам нужен уникальный идентификатор для перехода к предстоящим операциям, а также когда вы хотите получить доступ к реляционным ключам для выполнения дополнительных, более сложных операций.

Таблица метаданных фильма

Теперь давайте проследим за режиссерами фильмов, а также настроим отношения один-к-одному с 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")
}

Актер и киноактер

Далее вы хотите, чтобы актеры снимались в ваших фильмах, и, поскольку между фильмами и актерами существует связь «многие ко многим», создайте соединительную таблицу.

# 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 поддерживает следующие скалярные типы данных с присвоением типов PostgreSQL с помощью @col(dataType:) .

Тип Data Connect Встроенный тип GraphQL или
Пользовательский тип Data Connect
Тип PostgreSQL по умолчанию Поддерживаемые типы PostgreSQL
(псевдоним в скобках)
Нить ГрафQL текст текст
бит (n), варбит (n)
символ (п), варчар (п)
Int ГрафQL интервал Int2 (маллинт, малый сериал),
int4 (целое число, целое число, серийный номер)
Плавать ГрафQL поплавок8 float4 (реальный)
float8 (двойная точность)
числовой (десятичный)
логическое значение ГрафQL логическое значение логическое значение
UUID Обычай uuid uuid
Int64 Обычай bigint int8 (бигинт, большойсериал)
числовой (десятичный)
Дата Обычай дата дата
Временная метка Обычай временная метка

временная метка

Примечание. Информация о местном часовом поясе не сохраняется.
PostgreSQL преобразует и сохраняет такие временные метки, как UTC.

Вектор Обычай вектор

вектор

См . раздел Выполнение поиска по сходству векторов с помощью Vertex AI .

  • List GraphQL отображается в одномерный массив.
    • Например, [Int] сопоставляется с int5[] , [Any] сопоставляется с jsonb[] .
    • Data Connect не поддерживает вложенные массивы.

Неявные и предопределенные запросы и мутации

Ваши запросы и мутации Data Connect расширят набор неявных запросов и неявных мутаций, созданных Data Connect на основе типов и отношений типов в вашей схеме. Неявные запросы и мутации генерируются локальными инструментами всякий раз, когда вы редактируете схему.

В процессе разработки вы будете реализовывать предопределенные запросы и предопределенные мутации на основе этих неявных операций.

Неявный запрос и именование мутаций

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 Запросы на поиск данных авторизации Проверяет наличие указанных полей в результатах запроса. Выражение Common Expression Language (CEL) используется для проверки значений полей. См. руководство по авторизации и аттестации .
@redact Запросы Редактирует часть ответа клиента. См. руководство по авторизации и аттестации .
@transaction Мутации Обеспечивает, чтобы мутация всегда выполнялась в транзакции базы данных. См . примеры мутаций приложения Movie .

Запросы к базе данных обзоров фильмов

Вы определяете запрос Data Connect с помощью объявления типа операции запроса, имени операции, нуля или более аргументов операции и нуля или более директив с аргументами.

В кратком руководстве пример запроса listEmails не имел параметров. Конечно, во многих случаях данные, передаваемые в поля запроса, будут динамическими. Вы можете использовать синтаксис $variableName для работы с переменными как с одним из компонентов определения запроса.

Итак, следующий запрос имеет:

  • Определение типа query
  • Имя операции (запроса) ListMoviesByGenre
  • Аргумент операции $genre с одной переменной
  • Одна директива @auth .
query ListMoviesByGenre($genre: String!) @auth(level: USER)

Для каждого аргумента запроса требуется объявление типа, встроенного, например String , или пользовательского типа, определяемого схемой, например Movie .

Давайте посмотрим на сигнатуру все более сложных запросов. В конце вы представите мощные и краткие выражения отношений, доступные в неявных запросах, которые вы можете использовать в своих предопределенных запросах.

Ключевые скаляры в запросах

Но сначала замечание о ключевых скалярах.

Data Connect определяет специальный тип для ключевых скаляров, идентифицируемый _Key . Например, тип ключевого скаляра для нашей таблицы MovieMovie_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 и операциями заказа.

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

включает в себя поля массива

Вы можете проверить, содержит ли поле массива указанный элемент.

# 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 могут получать доступ к данным на основе связей между таблицами. Вы можете использовать отношения объекта (один-к-одному) или массива (один-ко-многим), определенные в вашей схеме, для создания вложенных запросов, т. е. выборки данных для одного типа вместе с данными из вложенного или связанного типа.

Такие запросы используют магический синтаксис 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. Скажи это пять раз быстрее!

директива @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
  })
}

Или расстройство.

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

Что дальше?