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

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

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

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

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

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

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

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

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

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

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

Используя значения сервера , вы можете эффективно позволить серверу динамически заполнять поля в ваших таблицах, используя сохраненные или легко вычислимые значения в соответствии с конкретными выражениями CEL на стороне сервера в аргументе expr . Например, вы можете определить поле с меткой времени, применяемой при доступе к полю, используя время, сохраненное в запросе операции, updatedAt: Timestamp! @default(expr: "request.time") .

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

Теперь давайте проследим за режиссерами фильмов, а также настроим отношения один-к-одному с Movie .

Добавьте поле ссылки, чтобы определить отношения.

Вы можете использовать директиву @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
}

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

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

# 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 поддерживает следующие скалярные типы данных с присвоением типов 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 на основе типов и отношений типов в вашей схеме. Эти поля генерируются локальными инструментами всякий раз, когда вы редактируете свою схему.

  • Как вы узнали из руководства по началу работы, консоль 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 Запросы на поиск данных авторизации Проверяет наличие указанных полей в результатах запроса. Выражение 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 .

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

Запрос «многие к одному»

Теперь давайте посмотрим на запрос с псевдонимами, чтобы проиллюстрировать синтаксис _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
    }
  }
}

Один к одному

Вы можете увидеть закономерность. Ниже схема изменена для иллюстрации.

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

Агрегационные запросы

Что такое агрегаты и зачем их использовать?

Поля агрегирования позволяют выполнять вычисления на основе списка результатов. С помощью агрегированных полей вы можете делать такие вещи, как:

  • Найдите средний балл отзыва
  • Узнать общую стоимость товаров в корзине
  • Найдите продукт с самым высоким или самым низким рейтингом
  • Подсчитайте количество товаров в вашем магазине

Агрегаты выполняются на сервере, что дает ряд преимуществ по сравнению с их расчетом на стороне клиента:

  • Более высокая производительность приложения (поскольку вы избегаете вычислений на стороне клиента)
  • Снижение затрат на исходящие данные (поскольку вы отправляете только агрегированные результаты вместо всех входных данных).
  • Повышенная безопасность (поскольку вы можете предоставить клиентам доступ к агрегированным данным, а не ко всему набору данных).

Пример схемы агрегатов

В этом разделе мы перейдем к примерной схеме витрины, которая хорошо объясняет, как использовать агрегаты:

  type Product @table {
    name: String!
    manufacturer: String!
    quantityInStock: Int!
    price: Float!
    expirationDate: Date
  }

Простые агрегаты

_count для всех полей

Простейшим агрегатным полем является _count : оно возвращает количество строк, соответствующих вашему запросу. Для каждого поля вашего типа Data Connect создает соответствующие агрегированные поля в зависимости от типа поля.

query CountProducts {
  products {
    _count
  }
}

Например, если в вашей базе данных есть 5 продуктов, результат будет таким:

{
  "products": [
    {
    "_count": 5
    }
  ]
}

Все поля имеют поле <field>_count , которое подсчитывает, сколько строк имеют ненулевое значение в этом поле.

query CountProductsWithExpirationDate {
  products {
    expirationDate_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
  }
}

Например, если у вас есть следующие продукты:

  • Товар А: quantityInStock: 10 , price: 2.99
  • Товар Б: 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
  }
}

Например, если у вас есть следующие сроки годности:

  • Продукт А: 2024-01-01
  • Продукт Б: 2024-03-01
  • Продукт С: 2024-02-01

Результатом будет:

{
  "products": [
    {
    "expirationDate_max": "2024-03-01",
    "expirationDate_min": "2024-01-01"
    }
  ]
}

Отчетливый

distinct аргумент позволяет получить все уникальные значения поля (или комбинации полей). Например:

query ListDistinctManufacturers {
  products(distinct: true) {
    manufacturer
  }
}

Например, если у вас есть следующие производители:

  • Товар А: manufacturer: "Acme"
  • Продукт Б: manufacturer: "Beta"
  • Товар С: manufacturer: "Acme"

Результатом будет:

{
  "products": [
    { "manufacturer": "Acme" },
    { "manufacturer": "Beta" }
  ]
}

Вы также можете использовать distinct аргумент в агрегированных полях, чтобы вместо этого агрегировать отдельные значения. Например:

query CountDistinctManufacturers {
  products {
    manufacturer_count(distinct: true)
  }
}

Например, если у вас есть следующие производители:

  • Товар А: manufacturer: "Acme"
  • Продукт Б: manufacturer: "Beta"
  • Товар С: manufacturer: "Acme"

Результатом будет:

{
  "products": [
    {
    "manufacturer_count": 2
    }
  ]
}

Сгруппированные агрегаты

Вы выполняете сгруппированное агрегирование, выбирая сочетание агрегированных и неагрегированных полей для типа. При этом группируются все совпадающие строки, имеющие одинаковое значение для неагрегированных полей, и вычисляются агрегатные поля для этой группы. Например:

query MostExpensiveProductByManufacturer {
  products {
  manufacturer
  price_max
  }
}

Например, если у вас есть следующие продукты:

  • Товар А: manufacturer: "Acme" , price: 2.99
  • Товар Б: manufacturer: "Beta" , price: 5.99
  • Товар С: 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
  }
}

Например, если у вас есть следующие продукты:

  • Товар А: manufacturer: "Acme" , price: 2.99
  • Товар Б: manufacturer: "Beta" , price: 5.99
  • Товар С: manufacturer: "Acme" , price: 1.99

Результатом будет:

{
  "products": [
    { "manufacturer": "Acme", "price_max": 2.99 },
    { "manufacturer": "Beta", "price_max": 5.99 }
  ]
}

Агрегирование по таблицам

Агрегированные поля можно использовать совместно с сгенерированными полями отношений «один-ко-многим», чтобы ответить на сложные вопросы о ваших данных. Вот модифицированная схема с отдельной таблицей 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
    }
  }
}

Например, если у вас есть следующие производители:

  • Производитель А: name: "Acme" , products_on_manufacturer: 2
  • Производитель Б: name: "Beta" , products_on_manufacturer: 1

Результатом будет:

{
  "manufacturers": [
    { "name": "Acme", "products_on_manufacturer": { "_count": 2 } },
    { "name": "Beta", "products_on_manufacturer": { "_count": 1 } }
  ]
}

Мутации в базе данных обзоров фильмов

Как уже упоминалось, когда вы определяете таблицу в своей схеме, 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"
  })
}

Выполнение обновлений

Вот обновления. Продюсеры и режиссеры, конечно, надеются, что эти средние рейтинги находятся в тренде.

Поле 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 и даты, в ответ на запросы клиентов.

Кроме того, вы можете использовать данные, такие как идентификаторы пользователей, отправленные в объектах request Data Connect из клиентских приложений.

При реализации мутаций используйте синтаксис field_expr для запуска обновлений, генерируемых сервером, или доступа к данным из запросов. Например, чтобы передать uid авторизации, сохраненный в запросе, операции _upsert , передайте "auth.uid" в поле userId_expr .

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

Или, в знакомом приложении списка дел, при создании нового списка дел вы можете передать 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 в справочнике по скалярам .

Запросы на поиск данных авторизации

Мутации 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);

Что дальше?

,

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

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

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

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

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

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

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

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

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

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

Используя значения сервера , вы можете эффективно позволить серверу динамически заполнять поля в ваших таблицах, используя сохраненные или легко вычислимые значения в соответствии с конкретными выражениями CEL на стороне сервера в аргументе expr . Например, вы можете определить поле с меткой времени, применяемой при доступе к полю, используя время, сохраненное в запросе операции, updatedAt: Timestamp! @default(expr: "request.time") .

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

Теперь давайте проследим за режиссерами фильмов, а также настроим отношения один-к-одному с Movie .

Добавьте поле ссылки, чтобы определить отношения.

Вы можете использовать директиву @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
}

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

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

# 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 поддерживает следующие скалярные типы данных с присвоением типов 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 на основе типов и отношений типов в вашей схеме. Эти поля генерируются локальными инструментами всякий раз, когда вы редактируете свою схему.

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

Такие запросы используют Magic Data Connect _on_ и _via Syntax в сгенерированных полях чтения.

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

Многие к одному

Давайте добавим обзоры в наше приложение, с таблицей 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")
}

Запрос для многих к одному

Теперь давайте посмотрим на запрос с псевдонимом, чтобы проиллюстрировать _via_ syntax.

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

Один к одному

Вы можете увидеть шаблон. Ниже схема модифицирована для иллюстрации.

# 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_ syntax.

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

Например, если у вас есть 5 продуктов в вашей базе данных, результат будет:

{
  "products": [
    {
    "_count": 5
    }
  ]
}

Все поля имеют поле <field>_count , которое подсчитывает, сколько строк имеет не нулевое значение в этом поле.

query CountProductsWithExpirationDate {
  products {
    expirationDate_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
  }
}

Например, если у вас есть следующие продукты:

  • Продукт 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
  }
}

Например, если у вас есть следующие даты истечения срока действия:

  • Продукт A: 2024-01-01
  • Продукт B: 2024-03-01
  • Продукт C: 2024-02-01

Результатом будет:

{
  "products": [
    {
    "expirationDate_max": "2024-03-01",
    "expirationDate_min": "2024-01-01"
    }
  ]
}

Отчетливый

distinct аргумент позволяет вам получить все уникальные значения для поля (или комбинации полей). Например:

query ListDistinctManufacturers {
  products(distinct: true) {
    manufacturer
  }
}

Например, если у вас есть следующие производители:

  • Продукт A: manufacturer: "Acme"
  • Продукт B: manufacturer: "Beta"
  • Продукт C: manufacturer: "Acme"

Результатом будет:

{
  "products": [
    { "manufacturer": "Acme" },
    { "manufacturer": "Beta" }
  ]
}

Вы также можете использовать distinct аргумент на совокупных полях, чтобы вместо этого собирать различные значения. Например:

query CountDistinctManufacturers {
  products {
    manufacturer_count(distinct: true)
  }
}

Например, если у вас есть следующие производители:

  • Продукт A: manufacturer: "Acme"
  • Продукт B: manufacturer: "Beta"
  • Продукт C: manufacturer: "Acme"

Результатом будет:

{
  "products": [
    {
    "manufacturer_count": 2
    }
  ]
}

Сгруппированные агрегаты

Вы выполняете сгруппированный заполнитель, выбирая смесь агрегатных и неагрегированных полей на типе. Это объединяет все соответствующие строки, которые имеют одинаковое значение для неагрегических полей, и рассчитывают совокупные поля для этой группы. Например:

query MostExpensiveProductByManufacturer {
  products {
  manufacturer
  price_max
  }
}

Например, если у вас есть следующие продукты:

  • Продукт 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
  }
}

Например, если у вас есть следующие продукты:

  • Продукт 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 }
  ]
}

Агрегаты по таблицам

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

Например, если у вас есть следующие производители:

  • Производитель 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 будет генерировать основные неявные мутации для каждой таблицы.

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 Directive

Эта директива обеспечивает соблюдение того, что мутация всегда работает в транзакции базы данных.

Мутации с @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"
  })
}

Выполнить обновления

Вот обновления. Производители и директора, безусловно, надеются, что эти средние рейтинги находятся на тренде.

Поле 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 Data
  • dec to Derment Int , Int64 , Float , Date и Timestamp Data

Для списков вы также можете обновить с отдельными значениями или списками значений, используя:

  • add в добавление элементов, если они еще не присутствуют для списков, кроме списков векторов
  • remove , чтобы удалить все элементы, если они присутствуют, из типов списков, кроме векторных списков
  • append к добавлению элементов (ы) к типам списков, кроме векторных списков
  • prepend для подготовки элементов (ы) к типам списков, кроме списков векторов

Выполнить удаления

Вы, конечно, можете удалить данные фильма. Компания Conservationisters, безусловно, хотела бы, чтобы физические фильмы поддерживались как можно дольше.

# 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 , и даты в ответ на запросы клиентов.

Кроме того, вы можете использовать данные, такие как идентификаторы пользователей, отправленные в объектах request Data Connect из клиентских приложений.

Когда вы реализуете мутации, используйте синтаксис field_expr для запуска обновлений, сгенерированных сервером или доступа к данным из запросов. Например, чтобы передать uid авторизации, хранящийся в запросе на операцию _upsert , пройти "auth.uid" в поле userId_expr .

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

Или, в знакомых приложениях для списков дел при создании нового списка дел, вы можете передать 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 в ссылке на скаляры .

Запросы поиска данных авторизации

Мутации 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);

Что дальше?

,

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

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

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

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

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

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

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

Стол фильма

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

  • @table(name) и @col(name) чтобы настроить таблицу SQL и имена столбцов. Data Connect генерирует имена smake_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 и значениями сервера .

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

Используя значения сервера , вы можете эффективно позволить серверу динамически заполнять поля в ваших таблицах, используя хранимые или легкокомпюссимые значения в соответствии с конкретными выражениями CEL на стороне сервера в аргументе expr . Например, вы можете определить поле с помощью временной метки, применяемой, когда поле будет доступно с использованием времени, хранящегося в запросе операции, updatedAt: Timestamp! @default(expr: "request.time") .

Стол метаданных фильмов

Теперь давайте следим за режиссерами кино, а также установим отношения с одним к одному с Movie .

Добавьте справочное поле, чтобы определить отношения.

Вы можете использовать директиву @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
}

Актер и движущийся

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

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

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

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

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

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

вектор

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

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

Используйте сгенерированные поля для построения запросов и мутаций

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

  • Как вы обнаружили в Руководстве по началу работы, консоль Firebase и наше локальное инструменты для разработки используют эти автоматические поля, чтобы предоставить вам специальные административные запросы и мутации, которые вы можете использовать для семян данных и проверить содержимое ваших таблиц.

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

Автопогенерированное поле поля

Data Connect Prefers Подходящие имена для поля автоматически генерируются на основе объявлений типа схемы. Например, работа с источником 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 Запросы поиска данных авторизации Проверяет, что указанные поля присутствуют в результатах запроса. Экспрессия общего языка экспрессии (CEL) используется для проверки значений поля. См. Руководство по авторизации и аттестации .
@redact Запросы Отредактирует часть ответа от клиента. См. Руководство по авторизации и аттестации .
@transaction Мутации Обеспечивает соблюдение того, что мутация всегда работает в транзакции базы данных. См. Примеры мутации фильма .

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

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

В QuickStart пример запроса 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 и операциям заказа.

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

Такие запросы используют Magic Data Connect _on_ и _via Syntax в сгенерированных полях чтения.

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

Многие к одному

Давайте добавим обзоры в наше приложение, с таблицей 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")
}

Запрос для многих к одному

Теперь давайте посмотрим на запрос с псевдонимом, чтобы проиллюстрировать _via_ syntax.

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

Один к одному

Вы можете увидеть шаблон. Ниже схема модифицирована для иллюстрации.

# 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_ syntax.

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

Например, если у вас есть 5 продуктов в вашей базе данных, результат будет:

{
  "products": [
    {
    "_count": 5
    }
  ]
}

Все поля имеют поле <field>_count , которое подсчитывает, сколько строк имеет не нулевое значение в этом поле.

query CountProductsWithExpirationDate {
  products {
    expirationDate_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
  }
}

Например, если у вас есть следующие продукты:

  • Продукт 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
  }
}

Например, если у вас есть следующие даты истечения срока действия:

  • Продукт A: 2024-01-01
  • Продукт B: 2024-03-01
  • Продукт C: 2024-02-01

Результатом будет:

{
  "products": [
    {
    "expirationDate_max": "2024-03-01",
    "expirationDate_min": "2024-01-01"
    }
  ]
}

Отчетливый

distinct аргумент позволяет вам получить все уникальные значения для поля (или комбинации полей). Например:

query ListDistinctManufacturers {
  products(distinct: true) {
    manufacturer
  }
}

Например, если у вас есть следующие производители:

  • Продукт A: manufacturer: "Acme"
  • Продукт B: manufacturer: "Beta"
  • Продукт C: manufacturer: "Acme"

Результатом будет:

{
  "products": [
    { "manufacturer": "Acme" },
    { "manufacturer": "Beta" }
  ]
}

Вы также можете использовать distinct аргумент на совокупных полях, чтобы вместо этого собирать различные значения. Например:

query CountDistinctManufacturers {
  products {
    manufacturer_count(distinct: true)
  }
}

Например, если у вас есть следующие производители:

  • Продукт A: manufacturer: "Acme"
  • Продукт B: manufacturer: "Beta"
  • Продукт C: manufacturer: "Acme"

Результатом будет:

{
  "products": [
    {
    "manufacturer_count": 2
    }
  ]
}

Сгруппированные агрегаты

Вы выполняете сгруппированный заполнитель, выбирая смесь агрегатных и неагрегированных полей на типе. Это объединяет все соответствующие строки, которые имеют одинаковое значение для неагрегических полей, и рассчитывают совокупные поля для этой группы. Например:

query MostExpensiveProductByManufacturer {
  products {
  manufacturer
  price_max
  }
}

Например, если у вас есть следующие продукты:

  • Продукт 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
  }
}

Например, если у вас есть следующие продукты:

  • Продукт 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 }
  ]
}

Агрегаты по таблицам

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

Например, если у вас есть следующие производители:

  • Производитель 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 будет генерировать основные неявные мутации для каждой таблицы.

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 Directive

Эта директива обеспечивает соблюдение того, что мутация всегда работает в транзакции базы данных.

Мутации с @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"
  })
}

Выполнить обновления

Вот обновления. Производители и директора, безусловно, надеются, что эти средние рейтинги находятся на тренде.

Поле 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 Data
  • dec to Derment Int , Int64 , Float , Date и Timestamp Data

Для списков вы также можете обновить с отдельными значениями или списками значений, используя:

  • add в добавление элементов, если они еще не присутствуют для списков, кроме списков векторов
  • remove , чтобы удалить все элементы, если они присутствуют, из типов списков, кроме векторных списков
  • append к добавлению элементов (ы) к типам списков, кроме векторных списков
  • prepend для подготовки элементов (ы) к типам списков, кроме списков векторов

Выполнить удаления

Вы, конечно, можете удалить данные фильма. Film preservationists will certainly want the physical films to be maintained for as long as possible.

# Delete by key
mutation DeleteMovie($id: UUID!) {
  movie_delete(id: $id)
}

Here you can use _deleteMany .

# Multiple deletes
mutation DeleteUnpopularMovies($minRating: Int!) {
  movie_deleteMany(where: { rating: { le: $minRating } })
}

Write mutations on relations

Observe how to use the implicit _upsert mutation on a relation.

# Create or update a one to one relation
mutation MovieMetadataUpsert($movieId: UUID!, $director: String!) {
  movieMetadata_upsert(
    data: { movie: { id: $movieId }, director: $director }
  )
}

Let Data Connect supply values using field_expr syntax

As discussed in key scalars and server values , you can design your schema so that the server populates values for common fields like id s and dates in response to client requests.

In addition, you can make use of data, such as user IDs, sent in Data Connect request objects from client apps.

When you implement mutations, use field_expr syntax to trigger server-generated updates or access data from requests. For example, to pass the authorization uid stored in a request to an _upsert operation, pass "auth.uid" in the userId_expr field.

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

Or, in a familiar to-do list app, when creating a new to-do list, you could pass id_expr to instruct the server to auto-generate a UUID for the list.

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

For more information, see the _Expr scalars in the scalars reference .

Authorization data lookup queries

Data Connect mutations can be authorized by first querying the database and verifying the results of the query with CEL expressions. This is useful when, for example, you are writing to a table, and need to check the contents of a row in another table.

This feature supports:

  • The @check directive , which lets you evaluate the contents of fields, and based on the results of such evaluation:
    • Proceed with create, update and deletes defined by a mutation
    • Proceed to return the results of a query
    • Use returned values to perform different logic in your client code
  • The @redact directive , which lets you omit query results from wire protocol results.

These features are useful for authorization flows .

Equivalent SQL schema

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

Что дальше?

,

Firebase Data Connect lets you create connectors for your PostgreSQL instances managed with Google Cloud SQL. These connectors are combinations of a schema, queries, and mutations for using your data.

The Get started guide introduced a movie review app schema for PostgreSQL, and this guide takes a deeper look at how to design Data Connect schemas for PostgreSQL.

This guide pairs Data Connect queries and mutations with schema examples. Why discuss queries (and mutations ) in a guide about Data Connect schemas? Like other GraphQL-based platforms, Firebase Data Connect is a query-first development platform, so as a developer, in your data modeling you'll be thinking about the data your clients need, which will greatly influence the data schema you develop for your project.

This guide starts with a new schema for movie reviews , then covers the queries and mutations derived from that schema, and lastly provides a SQL listing equivalent to the core Data Connect schema.

The schema for a movie review app

Imagine you want to build a service that lets users submit and view movies reviews.

You need an initial schema for such an app. You will extend this schema later to create complex relational queries.

Movie table

The schema for Movies contains core directives like:

  • @table(name) and @col(name) to customize the SQL table and column names. Data Connect generates snake_case names if not specified.
  • @col(dataType) to customize SQL column types.
  • @default to configure SQL column default values during insert.

For more details, check out the reference docs for @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
}

Key scalars and server values

Before looking more at the movie review app, let's introduce Data Connect key scalars and server values .

Key scalars are concise object identifiers that Data Connect automatically assembles from key fields in your schemas. Key scalars are about efficiency, allowing you to find in a single call information about the identity and structure of your data. They are especially useful when you want to perform sequential actions on new records and need a unique identifier to pass to upcoming operations, and also when you want to access relational keys to perform additional more complex operations.

Using server values , you can effectively let the server dynamically populate fields in your tables using stored or readily-computable values according to particular server-side CEL expressions in the expr argument. For example, you can define a field with a timestamp applied when the field is accessed using the time stored in an operation request, updatedAt: Timestamp! @default(expr: "request.time") .

Movie metadata table

Now let's keep track of movie directors, as well as set up a one-to-one relationship with Movie .

Add the reference field to define a relationships.

You can use @ref directive to customize foreign key constraint.

  • @ref(fields) to specify the foreign key fields.
  • @ref(references) to specify the fields referenced in the target table. This reference defaults to the primary key, but fields with @unique are also supported.

For more details, check out the reference docs for @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 and MovieActor

Next, you want actors to star in your movies, and since you have a many-to-many relationship between movies and actors, create a join table.

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

Пользователь

Lastly, users for your app.

# Users
# Suppose a user can leave reviews for movies
type User @table {
  id: String! @default(expr: "auth.uid")
  username: String! @col(dataType: "varchar(50)")
}

Supported data types

Data Connect supports the following scalar data types, with assignments to PostgreSQL types using @col(dataType:) .

Data Connect type GraphQL built-in type or
Data Connect custom type
Default PostgreSQL type Supported PostgreSQL types
(alias in parentheses)
Нить ГрафQL текст текст
bit(n), varbit(n)
char(n), varchar(n)
Int ГрафQL интервал Int2 (smallint, smallserial),
int4 (integer, int, serial)
Плавать ГрафQL поплавок8 float4 (real)
float8 (double precision)
numeric (decimal)
логическое значение ГрафQL логическое значение логическое значение
UUID Обычай uuid uuid
Int64 Обычай bigint int8 (bigint, bigserial)
numeric (decimal)
Дата Обычай дата дата
Временная метка Обычай временная метка

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

Note: Local timezone information is not stored.
PostgreSQL converts and stores such timestamps as UTC.

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

вектор

See Perform vector similarity search with Vertex AI .

  • GraphQL List maps to a one-dimensional array.
    • For example, [Int] maps to int5[] , [Any] maps to jsonb[] .
    • Data Connect does not support nested arrays.

Use generated fields to build queries and mutations

Your Data Connect queries and mutations will extend a set of fields automatically generated Data Connect based on the types and type relationships in your schema. These fields are are generated by local tooling whenever you edit your schema.

  • As you discovered in the Get started guide, the Firebase console and our local development tooling use these auto-generated fields to provide you with ad hoc administrative queries and mutations you can use to seed data and verify the contents of your tables.

  • In your development process, you will implement deployable queries and deployable mutations bundled in your connectors, based on these auto-generated fields.

Auto-generated field naming

Data Connect infers suitable names for fields auto-generated based on your schema type declarations. For example, working with a PostgreSQL source, if you define a table named Movie , the server will generate:

  • Fields for reading data in single table use cases with the friendly names movie (singular, for retrieving individual results passing args like eq ) and movies (plural, for retrieving result lists passing args like gt and operations like orderby ). Data Connect also generates fields for multi-table, relational operations with explicit names like actors_on_movies or actors_via_actormovie .
  • Fields for writing data with familiar name like movie_insert , movie_upsert ...

The schema definition language also lets you explicitly control how names are generated for fields using singular and plural directive arguments.

Directives for queries and mutations

In addition to the directives you use in defining types and tables, Data Connect provides the @auth , @check , @redact and @transaction directives for augmenting the behavior of queries and mutations.

Директива Применимо к Описание
@auth Queries and mutations Defines the authentication policy for a query or mutation. See the authorization and attestation guide .
@check Authorization data lookup queries Verifies that specified fields are present in query results. A Common Expression Language (CEL) expression is used to test field values. See the authorization and attestation guide .
@redact Запросы Redacts a part of the response from the client. See the authorization and attestation guide .
@transaction Мутации Enforces that a mutation always run in a database transaction. See the movie app mutation examples .

Queries for the movie review database

You define a Data Connect query with a query operation type declaration, operation name, zero or more operation arguments, and zero or more directives with arguments.

In the quickstart, the example listEmails query took no parameters. Of course, in many cases, data passed to query fields will be dynamic. You can use $variableName syntax to work with variables as one of the components of a query definition.

So the following query has:

  • A query type definition
  • A ListMoviesByGenre operation (query) name
  • A single variable $genre operation argument
  • A single directive, @auth .
query ListMoviesByGenre($genre: String!) @auth(level: USER)

Every query argument requires a type declaration, a built-in like String , or a custom, schema-defined type like Movie .

Let's look at the signature of increasingly complex queries. You'll end by introducing powerful, concise relationship expressions you can use to build your deployable queries.

Key scalars in queries

But first, a note about key scalars.

Data Connect defines a special type for key scalars, identified by _Key . For example, the type of a key scalar for our Movie table is Movie_Key .

You retrieve key scalars as a response returned by most auto-generated read fields, or of course from queries where you have retrieved all the fields needed to build the scalar key.

Singular automatic queries, like movie in our running example, support a key argument that accepts a key scalar.

You might pass a key scalar as a literal. But, you can define variables to pass key scalars as input.

query GetMovie($myKey: Movie_Key!) {
  movie(key: $myKey) { title }
}

These can be provided in request JSON like this (or other serialization formats):

{
  # 
  "variables": {
    "myKey": {"foo": "some-string-value", "bar": 42}
  }
}

Thanks to custom scalar parsing, a Movie_Key can also be constructed using the object syntax, which may contain variables. This is mostly useful when you want to break individual components into different variables for some reason.

Aliasing in queries

Data Connect supports GraphQL aliasing in queries. With aliases, you rename the data that is returned in a query's results. A single Data Connect query can apply multiple filters or other query operations in one efficient request to the server, effectively issuing several "sub-queries" at once. To avoid name collisions in the returned data set, you use aliases to distinguish the sub-queries.

Here is a query where an expression uses the alias mostPopular .

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

Simple queries with filters

Data Connect queries map to all common SQL filters and order operations.

where and orderBy operators (singular, plural queries)

Returns all matched rows from the table (and nested associations). Returns an empty array if no records match the filter.

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 and offset operators (singular, plural queries)

You can perform pagination on results. These arguments are accepted but not returned in results.

query MoviesTop10 {
  movies(orderBy: [{ rating: DESC }], limit: 10) {
    # graphql: list the fields from the results to return
    title
  }
}

includes for array fields

You can test that an array field includes a specified item.

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

String operations and regular expressions

Your queries can use typical string search and comparison operations, including regular expressions. Note for efficiency you are bundling several operations here and disambiguating them with aliases.

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 and for composed filters

Use or and and for more complex logic.

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

Complex queries

Data Connect queries can access data based on the relationships among tables. You can use the object (one-to-one) or array (one-to-many) relationships defined in your schema to make nested queries, ie fetch data for one type along with data from a nested or related type.

Such queries use magic Data Connect _on_ and _via syntax in generated read fields.

You'll be making modifications to the schema from our initial version .

Many to one

Let's add reviews to our app, with a Review table and modifications to 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")
}

Query for many to one

Now let's look at a query, with aliasing, to illustrate _via_ syntax.

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

Один к одному

You can see the pattern. Below, the schema is modified for illustration.

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

Query for one to one

You can query using _on_ syntax.

# One to one
query GetMovieMetadata($id: UUID!) @auth(level: PUBLIC) {
  movie(id: $id) {
    movieMetadatas_on_movie {
      director
    }
  }
}

Many to many

Movies need actors, and actors need movies. They have a many to many relationship you can model with a MovieActors join table.

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

Query for many to many

Let's look at a query, with aliasing, to illustrate _via_ syntax.

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

Aggregation queries

What are aggregates, and why use them?

Aggregate fields let you perform calculations on a list of results. With aggregate fields, you can do things like:

  • Find the average score of a review
  • Find the total cost of items in a shopping cart
  • Find the highest- or lowest-rated product
  • Count the number of products in your store

Aggregates are performed on the server, which offers a number of benefits over calculating them client side:

  • Faster app performance (since you avoid client side calculations)
  • Reduced data egress costs (since you send just the aggregated results instead of all of the inputs)
  • Improved security (since you can give clients access to aggregated data instead of the entire data set)

Example schema for aggregates

In this section, we'll switch to a storefront example schema, which is a good for explaining how to use aggregates:

  type Product @table {
    name: String!
    manufacturer: String!
    quantityInStock: Int!
    price: Float!
    expirationDate: Date
  }

Simple aggregates

_count for all fields

The simplest aggregate field is _count : it returns how many rows match your query. For each field in your type, Data Connect generates corresponding aggregate fields depending on the field type.

query CountProducts {
  products {
    _count
  }
}

For example, if you have 5 products in your database, the result would be:

{
  "products": [
    {
    "_count": 5
    }
  ]
}

All fields have a <field>_count field, which counts how many rows have a non-null value in that field.

query CountProductsWithExpirationDate {
  products {
    expirationDate_count
  }
}

For example, if you have 3 products with an expiration date, the result would be:

{
  "products": [
    {
    "expirationDate_count": 3
    }
  ]
}
_min, _max, _sum, and _avg for numeric fields

Numeric fields (int, float, int64) also have <field>_min , <field>_max , <field>_sum , and <field>_avg .

query NumericAggregates {
  products {
  quantityInStock_max
  price_min
  price_avg
  quantityInStock_sum
  }
}

For example, if you have the following products:

  • Product A: quantityInStock: 10 , price: 2.99
  • Product B: quantityInStock: 5 , price: 5.99
  • Product C: quantityInStock: 20 , price: 1.99

The result would be:

{
  "products": [
    {
    "quantityInStock_max": 20,
    "price_min": 1.99,
    "price_avg": 3.6566666666666666,
    "quantityInStock_sum": 35
    }
  ]
}
_min and _max for dates and timestamps

Date and timestamp fields have <field>_min and <field>_max .

query DateAndTimeAggregates {
  products {
  expirationDate_max
  expirationDate_min
  }
}

For example, if you have the following expiration dates:

  • Product A: 2024-01-01
  • Product B: 2024-03-01
  • Product C: 2024-02-01

The result would be:

{
  "products": [
    {
    "expirationDate_max": "2024-03-01",
    "expirationDate_min": "2024-01-01"
    }
  ]
}

Отчетливый

The distinct argument lets you get all unique values for a field (or combination of fields). Например:

query ListDistinctManufacturers {
  products(distinct: true) {
    manufacturer
  }
}

For example, if you have the following manufacturers:

  • Product A: manufacturer: "Acme"
  • Product B: manufacturer: "Beta"
  • Product C: manufacturer: "Acme"

The result would be:

{
  "products": [
    { "manufacturer": "Acme" },
    { "manufacturer": "Beta" }
  ]
}

You can also use the distinct argument on aggregate fields to instead aggregate the distinct values. Например:

query CountDistinctManufacturers {
  products {
    manufacturer_count(distinct: true)
  }
}

For example, if you have the following manufacturers:

  • Product A: manufacturer: "Acme"
  • Product B: manufacturer: "Beta"
  • Product C: manufacturer: "Acme"

The result would be:

{
  "products": [
    {
    "manufacturer_count": 2
    }
  ]
}

Grouped aggregates

You perform a grouped aggregate by selecting a mix of aggregate and non-aggregate fields on a type. This groups together all matching rows that have the same value for the non-aggregate fields, and calculate the aggregate fields for that group. Например:

query MostExpensiveProductByManufacturer {
  products {
  manufacturer
  price_max
  }
}

For example, if you have the following products:

  • Product A: manufacturer: "Acme" , price: 2.99
  • Product B: manufacturer: "Beta" , price: 5.99
  • Product C: manufacturer: "Acme" , price: 1.99

The result would be:

{
  "products": [
    { "manufacturer": "Acme", "price_max": 2.99 },
    { "manufacturer": "Beta", "price_max": 5.99 }
  ]
}
having and where with grouped aggregates

You can also use the having and where argument to only return groups that meet a provided criteria.

  • having lets you filter groups by their aggregate fields
  • where lets you filter the rows based on non-aggregate fields.

query FilteredMostExpensiveProductByManufacturer {
  products(having: {price_max: {ge: 2.99}}) {
  manufacturer
  price_max
  }
}

For example, if you have the following products:

  • Product A: manufacturer: "Acme" , price: 2.99
  • Product B: manufacturer: "Beta" , price: 5.99
  • Product C: manufacturer: "Acme" , price: 1.99

The result would be:

{
  "products": [
    { "manufacturer": "Acme", "price_max": 2.99 },
    { "manufacturer": "Beta", "price_max": 5.99 }
  ]
}

Aggregates across tables

Aggregate fields can be used in concert with generated one-to-many relationship fields to answer complex questions about your data. Here is a modified schema, with separate table, Manufacturer , we can use in examples:

  type Product @table {
    name: String!
    manufacturer: Manufacturer!
    quantityInStock: Int!
    price: Float!
    expirationDate: Date
  }

  type Manufacturer @table {
    name: String!
    headquartersCountry: String!
  }

Now we can use aggregate fields to do things like find how many products a manufacturer makes:

query GetProductCount($id: UUID) {
  manufacturers {
    name
    products_on_manufacturer {
      _count
    }
  }
}

For example, if you have the following manufacturers:

  • Manufacturer A: name: "Acme" , products_on_manufacturer: 2
  • Manufacturer B: name: "Beta" , products_on_manufacturer: 1

The result would be:

{
  "manufacturers": [
    { "name": "Acme", "products_on_manufacturer": { "_count": 2 } },
    { "name": "Beta", "products_on_manufacturer": { "_count": 1 } }
  ]
}

Mutations for the movie review database

As mentioned, when you define a table in your schema, Data Connect will generate basic implicit mutations for each table.

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

With these, you can implement increasingly complex core CRUD cases. Say that five times fast!

@transaction directive

This directive enforces that a mutation always run in a database transaction.

Mutations with @transaction are guaranteed to either fully succeed or fully fail. If any of the fields within the transaction fails, the entire transaction is rolled back. From a client standpoint, any failure behaves as if the entire request had failed with a request error and execution had not begun.

Mutations without @transaction execute each root field one after another in sequence. It surfaces any errors as partial field errors, but not the impacts of the subsequent executions.

Создавать

Let's do basic creates.

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

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

Perform updates

Here are updates. Producers and directors certainly hope that those average ratings are on trend.

The movie_update field contains an expected id argument to identify a record and a data field you can use to set values in this update.

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

To perform multiple updates, use the movie_updateMany field.

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

Use increment, decrement, append, and prepend operations with _update

While in _update and _updateMany mutations you can explicitly set values in data: , it often makes more sense to apply an operator like increment to update values.

To modify the earlier update example, assume you want to increment the rating of a particular movie. You can use rating_update syntax with the inc operator.

mutation UpdateMovie(
  $id: UUID!,
  $ratingIncrement: Int!
) {
  movie_update(id: $id, data: {
    rating_update: {
      inc: $ratingIncrement
    }
  })
}

Data Connect supports the following operators for field updates:

  • inc to increment Int , Int64 , Float , Date and Timestamp data types
  • dec to decrement Int , Int64 , Float , Date and Timestamp data types

For lists, you can also update with individual values or lists of values using:

  • add to append item(s) if they are not already present to list types, except Vector lists
  • remove to remove all items, if present, from list types, except Vector lists
  • append to append item(s) to list types, except Vector lists
  • prepend to prepend item(s) to list types, except Vector lists

Perform deletes

You can of course delete movie data. Film preservationists will certainly want the physical films to be maintained for as long as possible.

# Delete by key
mutation DeleteMovie($id: UUID!) {
  movie_delete(id: $id)
}

Here you can use _deleteMany .

# Multiple deletes
mutation DeleteUnpopularMovies($minRating: Int!) {
  movie_deleteMany(where: { rating: { le: $minRating } })
}

Write mutations on relations

Observe how to use the implicit _upsert mutation on a relation.

# Create or update a one to one relation
mutation MovieMetadataUpsert($movieId: UUID!, $director: String!) {
  movieMetadata_upsert(
    data: { movie: { id: $movieId }, director: $director }
  )
}

Let Data Connect supply values using field_expr syntax

As discussed in key scalars and server values , you can design your schema so that the server populates values for common fields like id s and dates in response to client requests.

In addition, you can make use of data, such as user IDs, sent in Data Connect request objects from client apps.

When you implement mutations, use field_expr syntax to trigger server-generated updates or access data from requests. For example, to pass the authorization uid stored in a request to an _upsert operation, pass "auth.uid" in the userId_expr field.

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

Or, in a familiar to-do list app, when creating a new to-do list, you could pass id_expr to instruct the server to auto-generate a UUID for the list.

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

For more information, see the _Expr scalars in the scalars reference .

Authorization data lookup queries

Data Connect mutations can be authorized by first querying the database and verifying the results of the query with CEL expressions. This is useful when, for example, you are writing to a table, and need to check the contents of a row in another table.

This feature supports:

  • The @check directive , which lets you evaluate the contents of fields, and based on the results of such evaluation:
    • Proceed with create, update and deletes defined by a mutation
    • Proceed to return the results of a query
    • Use returned values to perform different logic in your client code
  • The @redact directive , which lets you omit query results from wire protocol results.

These features are useful for authorization flows .

Equivalent SQL schema

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

Что дальше?