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 (псевдоним в скобках) |
---|---|---|---|
Нить | GraphQL | текст | текст бит(n), варбит(n) символ(n), varchar(n) |
Инт | GraphQL | инт | Int2 (smallint, smallserial), int4 (целое, целое, последовательное) |
Плавать | GraphQL | float8 | float4 (действительный) float8 (двойная точность) числовой (десятичный) |
Булев | GraphQL | булев | булев |
UUID | Обычай | uuid | uuid |
Int64 | Обычай | bigint | int8 (bigint, bigserial) числовой (десятичный) |
Дата | Обычай | дата | дата |
Временная метка | Обычай | временная меткаtz | временная меткаtz Примечание: информация о местном часовом поясе не сохраняется. |
Вектор | Обычай | вектор | вектор См. раздел Выполнение поиска сходства векторов с помощью 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 | поля query в многошаговых операциях | Проверяет, что указанные поля присутствуют в результатах запроса. Выражение Common Expression Language (CEL) используется для проверки значений полей. См. Многошаговые операции . |
@redact | Запросы | Редактирует часть ответа от клиента. См. Многошаговые операции . |
@transaction | Мутации | Обеспечивает, чтобы мутация всегда выполнялась в транзакции базы данных. См. Многошаговые операции . |
Запросы к базе данных рецензий на фильмы
Вы определяете запрос Data Connect с помощью объявления типа операции запроса, имени операции, нуля или более аргументов операции и нуля или более директив с аргументами.
В кратком старте пример запроса listEmails
не принимал параметров. Конечно, во многих случаях данные, передаваемые в поля запроса, будут динамическими. Вы можете использовать синтаксис $variableName
для работы с переменными как одним из компонентов определения запроса.
Итак, следующий запрос имеет:
- Определение типа
query
- Имя операции (запроса)
ListMoviesByGenre
- Аргумент операции
$genre
с одной переменной - Одна директива,
@auth
.
query ListMoviesByGenre($genre: String!) @auth(level: USER)
Каждый аргумент запроса требует объявления типа: встроенного, например String
, или пользовательского, определяемого схемой типа, например Movie
.
Давайте рассмотрим сигнатуру все более сложных запросов. Вы закончите введением мощных, кратких выражений отношений, которые вы можете использовать для построения развертываемых запросов.
Ключевые скаляры в запросах
Но сначала — замечание о ключевых скалярах.
Data Connect определяет специальный тип для ключевых скаляров, идентифицируемый _Key
. Например, тип ключевого скаляра для нашей таблицы Movie
— Movie_Key
.
Скалярные значения ключей извлекаются в качестве ответа, возвращаемого большинством автоматически сгенерированных полей чтения, или, конечно, из запросов, в которых вы извлекаете все поля, необходимые для построения скалярного ключа.
Отдельные автоматические запросы, такие как movie
в нашем примере, поддерживают ключевой аргумент, который принимает ключевой скаляр.
Вы можете передать скаляр ключа как литерал. Но вы можете определить переменные для передачи скаляров ключа как входных данных.
query GetMovie($myKey: Movie_Key!) {
movie(key: $myKey) { title }
}
Их можно предоставить в запросе JSON следующим образом (или в других форматах сериализации):
{
# …
"variables": {
"myKey": {"foo": "some-string-value", "bar": 42}
}
}
Благодаря пользовательскому скалярному разбору Movie_Key
также может быть сконструирован с использованием синтаксиса объекта, который может содержать переменные. Это в основном полезно, когда вы хотите по какой-то причине разбить отдельные компоненты на разные переменные.
Псевдонимы в запросах
Data Connect поддерживает псевдонимы GraphQL в запросах. С помощью псевдонимов вы переименовываете данные, возвращаемые в результатах запроса. Один запрос Data Connect может применять несколько фильтров или других операций запроса в одном эффективном запросе к серверу, эффективно выдавая несколько «подзапросов» одновременно. Чтобы избежать конфликтов имен в возвращаемом наборе данных, вы используете псевдонимы для различения подзапросов.
Вот запрос, в котором выражение использует псевдоним mostPopular
.
query ReviewTopPopularity($genre: String) {
mostPopular: review(first: {
where: {genre: {eq: $genre}},
orderBy: {popularity: DESC}
}) { … }
}
Простые запросы с фильтрами
Запросы Data Connect сопоставляются со всеми распространенными фильтрами SQL и операциями заказов.
Операторы 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
}
}
Ответ one
one
Например, если в вашей базе данных 5 продуктов, результат будет следующим:
{
"products": [
{
"_count": 5
}
]
}
Во всех полях есть поле <field>_count
, которое подсчитывает количество строк, имеющих ненулевое значение в этом поле.
Запрос
query CountProductsWithExpirationDate {
products {
expirationDate_count
}
}
Ответ field_count
field_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
}
}
Ответ _min _max _sum _avg
_min _max _sum _avg
Например, если у вас есть следующие продукты:
- Продукт 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
}
}
Ответ _min _maxdatetime
_min _maxdatetime
Например, если у вас следующие сроки годности:
- Продукт А:
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
}
}
Ответ distinct
distinct
Например, если у вас есть следующие производители:
- Продукт А:
manufacturer: "Acme"
- Продукт B:
manufacturer: "Beta"
- Продукт C:
manufacturer: "Acme"
Результат будет следующим:
{
"products": [
{ "manufacturer": "Acme" },
{ "manufacturer": "Beta" }
]
}
Вы также можете использовать аргумент distinct
в полях агрегата, чтобы вместо этого агрегировать отдельные значения. Например:
Запрос
query CountDistinctManufacturers {
products {
manufacturer_count(distinct: true)
}
}
Ответ distinctonaggregate
distinctonaggregate
Например, если у вас есть следующие производители:
- Продукт А:
manufacturer: "Acme"
- Продукт B:
manufacturer: "Beta"
- Продукт C:
manufacturer: "Acme"
Результат будет следующим:
{
"products": [
{
"manufacturer_count": 2
}
]
}
Сгруппированные агрегаты
Вы выполняете групповой агрегат, выбирая смесь агрегированных и неагрегированных полей в типе. Это группирует все соответствующие строки, которые имеют одинаковое значение для неагрегированных полей, и вычисляет агрегированные поля для этой группы. Например:
Запрос
query MostExpensiveProductByManufacturer {
products {
manufacturer
price_max
}
}
Ответ groupedaggregates
groupedaggregates
Например, если у вас есть следующие продукты:
- Продукт А:
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
}
}
Ответ havingwhere
havingwhere
Например, если у вас есть следующие продукты:
- Продукт А:
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
}
}
}
Ответ aggregatesacrosstables
aggregatesacrosstables
Например, если у вас есть следующие производители:
- Производитель 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 сгенерирует базовые поля _insert
, _update
и т. д. для каждой таблицы.
type Movie @table { ... }
extend type Mutation {
# Insert a row into the movie table.
movie_insert(...): Movie_Key!
# Upsert a row into movie."
movie_upsert(...): Movie_Key!
# Update a row in Movie. Returns null if a row with the specified id/key does not exist
movie_update(...): Movie_Key
# Update rows based on a filter in Movie.
movie_updateMany(...): Int!
# Delete a single row in Movie. Returns null if a row with the specified id/key does not exist
movie_delete(...): Movie_Key
# Delete rows based on a filter in Movie.
movie_deleteMany(...): Int!
}
С ними вы можете реализовать все более сложные основные CRUD-кейсы. Скажите это в пять раз быстрее!
Создавать
Давайте сделаем базовые творения.
# Create a movie based on user input
mutation CreateMovie($title: String!, $releaseYear: Int!, $genre: String!, $rating: Int!) {
movie_insert(data: {
title: $title
releaseYear: $releaseYear
genre: $genre
rating: $rating
})
}
# Create a movie with default values
mutation CreateMovie2 {
movie_insert(data: {
title: "Sherlock Holmes"
releaseYear: 2009
genre: "Mystery"
rating: 5
})
}
Или неожиданность.
# 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 позволяет вам выполнять многошаговую логику в ваших мутациях, поддерживая:
Несколько полей записи
Несколько полей чтения в ваших мутациях (с использованием ключевого слова
query
field).Директива
@transaction
, которая обеспечивает поддержку транзакций, знакомую по реляционным базам данных.Директива
@check
, которая позволяет оценивать содержимое прочтений с использованием выражений CEL и на основе результатов такой оценки:- Продолжайте создавать, обновлять и удалять объекты, определенные мутацией.
- Перейти к возврату результатов поля запроса
- Используйте возвращаемые сообщения для выполнения соответствующей логики в клиентском коде.
Директива
@redact
, которая позволяет исключить результаты поля запроса из результатов проводного протокола.Привязка
response
CEL, которая хранит накопленные результаты всех мутаций и запросов, выполненных в сложной многошаговой операции. Вы можете получить доступ к привязкеresponse
:- В директивах
@check
, через аргументexpr:
- Со значениями сервера, используя синтаксис
field_expr
- В директивах
Директива @transaction
Поддержка многошаговых мутаций включает обработку ошибок с использованием транзакций.
Директива @transaction
обеспечивает, чтобы мутация — как с одним полем записи (например, _insert
или _update
), так и с несколькими полями записи — всегда выполнялась в транзакции базы данных.
Мутации без
@transaction
выполняют каждое корневое поле одно за другим в последовательности. Операция выявляет любые ошибки как частичные ошибки поля, но не последствия последующих выполнений.Мутации с
@transaction
гарантированно либо полностью успешны, либо полностью провалены. Если какое-либо из полей в транзакции не выполняется, вся транзакция откатывается.
Директивы @check
и @redact
Директива @check
проверяет, присутствуют ли указанные поля в результатах запроса. Выражение Common Expression Language (CEL) используется для проверки значений полей. Поведение директивы по умолчанию — проверять и отклонять узлы, значение которых равно null
или []
(пустые списки).
Директива @redact
редактирует часть ответа клиента. Редактированные поля все еще оцениваются на предмет побочных эффектов (включая изменения данных и @check
), а результаты все еще доступны для последующих шагов в выражениях CEL.
Используйте @check
, @check(message:)
и @redact
Основное применение @check
ad @redact
— поиск связанных данных для принятия решения о том, следует ли авторизовать определенные операции, используя поиск в логике, но скрывая его от клиентов. Ваш запрос может возвращать полезные сообщения для правильной обработки в клиентском коде.
Для иллюстрации следующее поле запроса проверяет, имеет ли запрашивающая сторона соответствующую роль «администратор» для просмотра пользователей, которые могут редактировать фильм.
query GetMovieEditors($movieId: UUID!) @auth(level: USER) {
moviePermission(key: { movieId: $movieId, userId_expr: "auth.uid" }) @redact {
role @check(expr: "this == 'admin'", message: "You must be an admin to view all editors of a movie.")
}
moviePermissions(where: { movieId: { eq: $movieId }, role: { eq: "editor" } }) {
user {
id
username
}
}
}
Дополнительную информацию о директивах @check
и @redact
при проверке авторизации см. в обсуждении поиска данных авторизации .
Используйте @check
для проверки ключей
Некоторые поля мутации, такие как _update
, могут быть пустыми, если запись с указанным ключом не существует. Аналогично, поиск может возвращать null или пустой список. Это не считается ошибками и, следовательно, не вызовет откатов.
Чтобы защититься от такого результата, проверьте, можно ли найти ключи, используя директиву @check
.
# Delete by key, error if not found
mutation MustDeleteMovie($id: UUID!) @transaction {
movie_delete(id: $id) @check(expr: "this != null", message: "Movie not found, therefore nothing is deleted")
}
Используйте связывание response
для создания цепочки многошаговых мутаций
Основной подход к созданию связанных записей, например, нового Movie
и связанной с ним записи MovieMetadata
, заключается в следующем:
- Вызов мутации
_insert
дляMovie
- Сохраните возвращенный ключ созданного фильма.
- Затем вызовите вторую мутацию
_insert
для создания записиMovieMetadata
.
Но с помощью Data Connect вы можете обработать этот распространенный случай за одну многошаговую операцию, обратившись к результатам первой _insert
во второй _insert
.
Создание успешного приложения для обзора фильмов — это большая работа. Давайте отследим наш список дел с помощью нового примера.
Используйте response
для установки полей со значениями сервера
В следующем списке дел мутация:
- Привязка
response
представляет собой частичный объект ответа на данный момент, который включает все поля мутации верхнего уровня до текущего. - Результаты первоначальной операции
todoList_insert
, которая возвращает полеid
(ключа), доступны позже вresponse.todoList_insert.id
, поэтому мы можем немедленно вставить новый элемент списка дел.
mutation CreateTodoListWithFirstItem(
$listName: String!,
$itemContent: String!
) @transaction {
# Sub-step 1:
todoList_insert(data: {
id_expr: "uuidV4()", # <-- auto-generated. Or a column-level @default on `type TodoList` will also work
name: $listName,
})
# Sub-step 2:
todo_insert(data: {
listId_expr: "response.todoList_insert.id" # <-- Grab the newly generated ID from the partial response so far.
content: $itemContent,
})
}
Используйте response
для проверки полей с помощью @check
response
также доступен в @check(expr: "...")
, поэтому вы можете использовать его для построения еще более сложной логики на стороне сервера. В сочетании с шагами query { … }
в мутациях вы можете достичь гораздо большего без дополнительных клиент-серверных циклов.
Обратите внимание: в следующем примере @check
уже имеет доступ к response.query
, поскольку @check
всегда запускается после шага, к которому он прикреплен.
mutation CreateTodoInNamedList(
$listName: String!,
$itemContent: String!
) @transaction {
# Sub-step 1: Look up List.id by its name
query
@check(expr: "response.query.todoLists.size() > 0", message: "No such TodoList with the name!")
@check(expr: "response.query.todoLists.size() < 2", message: "Ambiguous listName!") {
todoLists(where: { name: $listName }) {
id
}
}
# Sub-step 2:
todo_insert(data: {
listId_expr: "response.todoLists[0].id" # <-- Now we have the parent list ID to insert to
content: $itemContent,
})
}
Более подробную информацию о привязке response
см. в справочнике CEL .
Понимание прерванных операций с помощью @transaction
и query @check
Многошаговые мутации могут привести к ошибкам:
- Операции с базой данных могут завершиться неудачей.
- Логика запроса
@check
может завершить работу.
Data Connect рекомендует использовать директиву @transaction
с вашими многошаговыми мутациями. Это приводит к более согласованной базе данных и результатам мутаций, которые легче обрабатывать в клиентском коде:
- При первой ошибке или неудачном выполнении
@check
операция будет прекращена, поэтому нет необходимости управлять выполнением каких-либо последующих полей или оценкой CEL. - Откаты выполняются в ответ на ошибки базы данных или логику
@check
, обеспечивая согласованное состояние базы данных. - Ошибка отката всегда возвращается в клиентский код.
Могут быть некоторые варианты использования, когда вы решите не использовать @transaction
: вы можете выбрать конечную согласованность, если, например, вам нужна более высокая пропускная способность, масштабируемость или доступность. Однако вам нужно управлять своей базой данных и клиентским кодом, чтобы обеспечить результаты:
- Если одно поле выходит из строя из-за операций с базой данных, последующие поля продолжат выполняться. Однако неудачные
@check
все равно завершают всю операцию. - Откаты не выполняются, что означает смешанное состояние базы данных с некоторыми успешными обновлениями и некоторыми неудачными обновлениями.
- Ваши операции с
@check
могут давать более непоследовательные результаты, если ваша логика@check
использует результаты чтения и/или записи на предыдущем шаге. - Результат, возвращаемый клиентскому коду, будет содержать более сложную смесь ответов об успехе и неудаче, которые необходимо обработать.
Эквивалентная схема 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);
Что дальше?
- Узнайте, как защитить свои запросы и мутации с помощью авторизации и аттестации .
- Узнайте, как вызывать запросы и мутации из автоматически сгенерированного веб-SDK , Android SDK , iOS SDK и Flutter SDK .