Implementar mutações do SQL Connect

Firebase SQL Connect permite criar conectores para instâncias do PostgreSQL gerenciadas com o Google Cloud SQL. Esses conectores são combinações de consultas e mutações para usar os dados do seu esquema.

O guia de introdução apresentou um esquema de app de resenha de filmes para PostgreSQL.

Esse guia também apresentou operações administrativas implantáveis e ad hoc, incluindo mutações.

  • Mutações implantáveis são aquelas que você implementa para chamar de apps cliente em um conector, com endpoints de API definidos. SQL Connect integra autenticação e autorização a essas mutações e gera SDKs do cliente com base na sua API.
  • Mutações administrativas ad hoc são executadas em ambientes privilegiados para preencher e gerenciar tabelas. É possível criar e executar essas mutações no Firebase console, em ambientes privilegiados usando o Firebase Admin SDK, e em ambientes de desenvolvimento local usando nossa extensão do SQL Connect para VS Code.

Este guia aborda as mutações implantáveis.

Recursos de SQL Connect mutações

SQL Connect permite realizar mutações básicas de todas as maneiras esperadas em um banco de dados PostgreSQL:

  • Realizar operações CRUD
  • Gerenciar operações de várias etapas com transações

No entanto, com as extensões do SQL Connect para GraphQL, é possível implementar mutações avançadas para apps mais rápidos e eficientes:

  • Use escalares de chave retornados por muitas operações para simplificar operações repetidas em registros
  • Use valores do servidor para preencher dados com operações fornecidas pelo servidor
  • Realize consultas no curso de operações de mutação de várias etapas para pesquisar dados, salvando linhas de código e viagens de ida e volta ao servidor.

Usar campos gerados para implementar mutações

As operações do SQL Connect vão estender um conjunto de campos gerados automaticamente pelo SQL Connect com base nos tipos e nas relações de tipo no seu esquema. Esses campos são gerados por ferramentas locais sempre que você edita o esquema.

É possível usar campos gerados para implementar mutações, desde a criação, atualização e exclusão de registros individuais em tabelas únicas até atualizações mais complexas de várias tabelas.

Considere que o esquema contenha um tipo Movie e um tipo Actor associado. SQL Connect gera movie_insert, movie_update, campos movie_delete e muito mais.

Mutação com o
movie_insert campo

O campo movie_insert representa uma mutação para criar um único registro na tabela Movie.

Use esse campo para criar um único filme.

mutation CreateMovie($data: Movie_Data!) {
  movie_insert(data: $data) { key }
}

Mutação com o
movie_update campo

O campo movie_update representa uma mutação para atualizar um único registro na tabela Movie.

Use esse campo para atualizar um único filme pela chave.

mutation UpdateMovie($myKey: Movie_Key!, $data: Movie_Data!) {
  movie_update(key: $myKey, data: $data) { key }
}

Mutação com o
movie_delete campo

O campo movie_delete representa uma mutação para excluir um único registro na tabela Movie.

Use esse campo para excluir um único filme pela chave.

  mutation DeleteMovie($myKey: Movie_Key!) {
    movie_delete(key: $myKey) { key }
  }

Elementos essenciais de uma mutação

SQL Connect mutações são mutações GraphQL com SQL Connect extensões. Assim como em uma mutação GraphQL normal, é possível definir um nome de operação e uma lista de variáveis GraphQL.

SQL Connect estende as consultas GraphQL com diretivas personalizadas, como @auth e @transaction.

Portanto, a mutação a seguir tem:

  • Uma definição de tipo mutation
  • Um nome de operação (mutação) SignUp
  • Um único argumento de operação $username
  • Uma única diretiva, @auth
  • Um único campo user_insert.
mutation SignUp($username: String!) @auth(level: USER) {
  user_insert(data: {
    id_expr: "auth.uid"
    username: $username
  })
}

Cada argumento de mutação requer uma declaração de tipo, um tipo integrado como String ou um tipo personalizado definido pelo esquema, como Movie.

Escrever mutações básicas

É possível começar a escrever mutações para criar, atualizar e excluir registros individuais do seu banco de dados.

Criar

Vamos fazer criações básicas.

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

Ou uma operação 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"
  })
}

Realizar atualizações

Confira as atualizações. Produtores e diretores certamente esperam que essas classificações médias estejam na moda.

O campo movie_update contém um argumento id esperado para identificar um registro e um campo data que pode ser usado para definir valores nessa atualização.

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

Para realizar várias atualizações, use o campo 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
      })
}

Usar operações de incremento, decremento, anexação e prefixação com _update

Embora nas mutações _update e _updateMany seja possível definir valores explicitamente em data:, muitas vezes é mais sensato aplicar um operador como incremento para atualizar valores.

Para modificar o exemplo de atualização anterior, considere que você quer incrementar a classificação de um filme específico. É possível usar a sintaxe rating_update com o operador inc.

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

SQL Connect oferece suporte aos seguintes operadores para atualizações de campo:

  • inc para incrementar tipos de dados Int, Int64, Float, Date e Timestamp
  • dec para decrementar tipos de dados Int, Int64, Float, Date e Timestamp

Para listas, também é possível atualizar com valores individuais ou listas de valores usando:

  • add para anexar itens, se eles ainda não estiverem presentes em tipos de lista, exceto listas de vetores
  • remove para remover todos os itens, se presentes, de tipos de lista, exceto listas de vetores
  • append para anexar itens a tipos de lista, exceto listas de vetores
  • prepend para prefixar itens a tipos de lista, exceto listas de vetores

Realizar exclusões

É claro que é possível excluir dados de filmes. Os preservacionistas de filmes certamente querem que os filmes físicos sejam mantidos pelo maior tempo possível.

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

Aqui, é possível usar _deleteMany.

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

Escrever mutações em relações

Observe como usar a mutação _upsert implícita em uma relação.

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

Projetar esquemas para mutações eficientes

SQL Connect oferece dois recursos importantes que permitem escrever mutações mais eficientes e salvar operações de ida e volta.

Escalares de chave são identificadores de objetos concisos que o SQL Connect monta automaticamente a partir de campos de chave nos seus esquemas. Os escalares de chave são sobre eficiência, permitindo encontrar em uma única chamada informações sobre a identidade e a estrutura dos seus dados. Eles são especialmente úteis quando você quer realizar ações sequenciais em novos registros e precisa de um identificador exclusivo para transmitir às próximas operações, e também quando você quer acessar chaves relacionais para realizar outras operações mais complexas.

Usando valores do servidor, é possível permitir que o servidor preencha dinamicamente campos nas suas tabelas usando valores armazenados ou facilmente calculáveis de acordo com expressões CEL específicas do lado do servidor no argumento expr. Por exemplo, é possível definir um campo com um carimbo de data/hora aplicado quando o campo é acessado usando o horário armazenado em uma solicitação de operação, updatedAt: Timestamp! @default(expr: "request.time").

Escrever mutações avançadas: permitir que SQL Connect forneça valores usando a sintaxe field_expr

Conforme discutido em escalares de chave e valores do servidor, é possível projetar o esquema para que o servidor preencha valores para campos comuns, como ids e datas, em resposta a solicitações do cliente.

Além disso, é possível usar dados, como IDs de usuário, enviados em SQL Connect request objetos de apps cliente.

Ao implementar mutações, use a sintaxe field_expr para acionar atualizações geradas pelo servidor ou acessar dados de solicitações. Por exemplo, para transmitir a autorização uid armazenada em uma solicitação para uma operação _upsert, transmita "auth.uid" no campo 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 })
}

Ou, em um app de lista de tarefas familiar, ao criar uma nova lista de tarefas, é possível transmitir id_expr para instruir o servidor a gerar automaticamente um UUID para a lista.

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

Para mais informações, consulte os _Expr escalares na referência de escalares.

Escrever mutações avançadas: operações de várias etapas

Há muitas situações em que você pode querer incluir vários campos de gravação (como inserções) em uma mutação. Também é possível ler o banco de dados durante a execução de uma mutação para pesquisar e verificar os dados existentes antes de realizar, por exemplo, inserções ou atualizações. Essas opções salvam operações de ida e volta e, portanto, custos.

SQL Connect permite realizar lógica de várias etapas nas mutações, oferecendo suporte a:

  • Vários campos de gravação

  • Vários campos de leitura nas mutações (usando a palavra-chave do campo query).

  • A diretiva @transaction, que oferece suporte a transações familiares de bancos de dados relacionais.

  • A diretiva @check, que permite avaliar o conteúdo das leituras usando expressões CEL e, com base nos resultados dessa avaliação:

    • Prossiga com criações, atualizações e exclusões definidas por uma mutação
    • Prossiga para retornar os resultados de um campo de consulta
    • Use mensagens retornadas para realizar a lógica apropriada no código do cliente
  • A diretiva @redact, que permite omitir resultados de campos de consulta dos resultados do protocolo de rede.

  • A vinculação response do CEL, que armazena os resultados acumulados de todas as mutações e consultas realizadas em uma operação complexa de várias etapas. É possível acessar a vinculação response:

    • Em diretivas @check, pelo argumento expr:
    • Com valores do servidor, usando a sintaxe field_expr

A diretiva @transaction

O suporte a mutações de várias etapas inclui tratamento de erros usando transações.

A diretiva @transaction impõe que uma mutação, com um único campo de gravação (por exemplo, _insert ou _update) ou com vários campos de gravação, sempre seja executada em uma transação de banco de dados.

  • Mutações sem @transaction executam cada campo raiz um após o outro em sequência. A operação mostra erros como erros de campo parciais, mas não os impactos das execuções subsequentes.

  • As mutações com @transaction têm garantia de sucesso ou falha total. Se algum dos campos na transação falhar, toda a transação será revertida.

As diretivas @check e @redact

A diretiva @check verifica se os campos especificados estão presentes nos resultados da consulta. Uma expressão Common Expression Language (CEL) é usada para testar valores de campo. O comportamento padrão da diretiva é verificar e rejeitar nós cujo valor seja null ou [] (listas vazias).

A diretiva @redact edita uma parte da resposta do cliente. Os campos editados ainda são avaliados quanto a efeitos colaterais (incluindo mudanças de dados e @check) e os resultados ainda estão disponíveis para etapas posteriores em expressões CEL.

Usar @check, @check(message:) e @redact

Um dos principais usos de @check e @redact é pesquisar dados relacionados para decidir se determinadas operações devem ser autorizadas, usando a pesquisa na lógica, mas ocultando-a dos clientes. A consulta pode retornar mensagens úteis para o tratamento correto no código do cliente.

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

Para saber mais sobre as diretivas @check e @redact em verificações de autorização, consulte a discussão sobre a pesquisa de dados de autorização.

Usar @check para validar chaves

Alguns campos de mutação, como _update, podem não funcionar se um registro com uma chave especificada não existir. Da mesma forma, as pesquisas podem retornar nulo ou uma lista vazia. Esses não são considerados erros e, portanto, não acionam rollbacks.

Para evitar esse resultado, teste se as chaves podem ser encontradas usando a diretiva @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")
}

Usar a vinculação response para encadear mutações de várias etapas

A abordagem básica para criar registros relacionados, por exemplo, um novo Movie e uma entrada MovieMetadata associada, é:

  1. Chamar uma mutação _insert para Movie
  2. Armazenar a chave retornada do filme criado
  3. Em seguida, chame uma segunda mutação _insert para criar o registro MovieMetadata.

No entanto, com SQL Connect, é possível processar esse caso comum em uma única operação de várias etapas acessando os resultados da primeira _insert na segunda _insert.

Criar um app de resenha de filmes bem-sucedido é muito trabalho. Vamos acompanhar nossa lista de tarefas com um novo exemplo.

Usar response para definir campos com valores do servidor

Na mutação da lista de tarefas a seguir:

  • A vinculação response representa o objeto de resposta parcial até o momento, que inclui todos os campos de mutação de nível superior antes do atual.
  • Os resultados da operação inicial todoList_insert, que retorna o id (chave), são acessados mais tarde em response.todoList_insert.id para que possamos inserir imediatamente um novo item de tarefa.
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,
  })
}

Usar response para validar campos usando @check

response também está disponível em @check(expr: "..."), para que seja possível usá-lo para criar uma lógica do lado do servidor ainda mais complicada. Combinado com etapas query { … } em mutações, é possível conseguir muito mais sem viagens de ida e volta adicionais do cliente-servidor.

No exemplo a seguir, observe que @check já tem acesso a response.query porque um @check sempre é executado após a etapa a que está anexado.

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

Para mais informações sobre a vinculação response, consulte a referência de CEL.

Entender operações interrompidas com @transaction e query @check

Mutações de várias etapas podem encontrar erros:

  • As operações de banco de dados podem falhar.
  • A lógica de @check de consulta pode encerrar operações.

SQL Connect recomenda que você use a diretiva @transaction com suas mutações de várias etapas. Isso resulta em um banco de dados mais consistente e resultados de mutação mais fáceis de processar no código do cliente:

  • No primeiro erro ou @check com falha, a operação será encerrada. Portanto, não é necessário gerenciar a execução de campos subsequentes ou a avaliação de CEL.
  • Os rollbacks são realizados em resposta a erros de banco de dados ou lógica @check, gerando um estado de banco de dados consistente.
  • Um erro de rollback sempre é retornado ao código do cliente.

Pode haver alguns casos de uso em que você opta por não usar @transaction: é possível optar pela consistência posterior se, por exemplo, precisar de maior capacidade de processamento, escalonabilidade ou disponibilidade. No entanto, é necessário gerenciar o banco de dados e o código do cliente para permitir os resultados:

  • Se um campo falhar devido a operações de banco de dados, os campos subsequentes continuarão sendo executados. No entanto, @checks com falha ainda encerram toda a operação.
  • Os rollbacks não são realizados, o que significa um estado de banco de dados misto com algumas atualizações bem-sucedidas e algumas com falha.
  • Suas operações com @check podem gerar resultados mais inconsistentes se a lógica @check usar os resultados de leituras e/ou gravações em uma etapa anterior.
  • O resultado retornado ao código do cliente vai conter uma combinação mais complexa de respostas de sucesso e falha a serem processadas.

Diretivas para SQL Connect mutações

Além das diretivas usadas na definição de tipos e tabelas, SQL Connect oferece as @auth, @check, @redact e @transaction diretivas para aumentar o comportamento das operações.

Diretiva Aplicável a Descrição
@auth Consultas e mutações Define a política de autorização para uma consulta ou mutação. Consulte o guia de autorização e atestado.
@check Campos query em operações de várias etapas Verifica se os campos especificados estão presentes nos resultados da consulta. Uma expressão Common Expression Language (CEL) é usada para testar valores de campo. Consulte Operações de várias etapas.
@redact Consultas Edita uma parte da resposta do cliente. Consulte Operações de várias etapas.
@transaction Mutações Impõe que uma mutação sempre seja executada em uma transação de banco de dados. Consulte Operações de várias etapas.

Próximas etapas

Talvez você se interesse por: