Implementowanie mutacji SQL Connect

Firebase SQL Connect umożliwia tworzenie oprogramowania sprzęgającego dla instancji PostgreSQL zarządzanych za pomocą Google Cloud SQL. Oprogramowanie sprzęgające to połączenie zapytań i mutacji, które umożliwia korzystanie z danych ze schematu.

W przewodniku dla początkujących przedstawiliśmy schemat aplikacji do recenzowania filmów w PostgreSQL.

W tym przewodniku omówiliśmy też operacje administracyjne, które można wdrożyć, oraz operacje ad hoc, w tym mutacje.

  • Mutacje, które można wdrożyć , to mutacje, które implementujesz, aby wywoływać je z aplikacji klienckich w oprogramowaniu sprzęgającym za pomocą zdefiniowanych przez Ciebie punktów końcowych interfejsu API. SQL Connect integruje uwierzytelnianie i autoryzację z tymi mutacjami oraz generuje pakiety SDK klienta na podstawie Twojego interfejsu API.
  • Mutacje administracyjne ad hoc są uruchamiane w środowiskach z uprawnieniami do wypełniania tabel i zarządzania nimi. Możesz je tworzyć i wykonywać w konsoli Firebase z uprawnieniami za pomocą Firebase Admin SDK, oraz w lokalnych środowiskach programistycznych za pomocą naszego rozszerzenia SQL Connect VS Code.

W tym przewodniku szczegółowo omówimy mutacje, które można wdrożyć.

Funkcje mutacji SQL Connect

SQL Connect umożliwia wykonywanie podstawowych mutacji na wszystkie sposoby których można oczekiwać w przypadku bazy danych PostgreSQL:

  • Wykonywanie operacji CRUD
  • Zarządzanie operacjami wieloetapowymi za pomocą transakcji

Dzięki rozszerzeniom SQL Connect do GraphQL możesz jednak implementować zaawansowane mutacje, które przyspieszą działanie aplikacji i zwiększą ich wydajność:

  • Używaj skalarów kluczy zwracanych przez wiele operacji, aby uprościć powtarzające się operacje na rekordach.
  • Używaj wartości serwera , aby wypełniać dane operacjami udostępnianymi przez serwer.
  • Wykonywanie zapytań w trakcie wieloetapowych operacji mutacji w celu wyszukiwania danych, co pozwala zaoszczędzić wiersze kodu i zmniejszyć liczbę połączeń z serwerem.

Implementowanie mutacji za pomocą wygenerowanych pól

Twoje operacje SQL Connect rozszerzą zestaw pól wygenerowanych automatycznie przez SQL Connect na podstawie typów i relacji między typami w Twoim schemacie. Pola te są generowane przez lokalne narzędzia za każdym razem, gdy edytujesz schemat.

Możesz używać wygenerowanych pól do implementowania mutacji – od tworzenia, aktualizowania i usuwania poszczególnych rekordów w pojedynczych tabelach po bardziej złożone aktualizacje obejmujące wiele tabel.

Załóżmy, że Twój schemat zawiera typ Movie i powiązany z nim typ Actor. SQL Connect generuje pola movie_insert, movie_update, movie_delete i inne.

Mutacja z polem
movie_insert

Pole movie_insert reprezentuje mutację, która tworzy pojedynczy rekord w tabeli Movie.

Użyj tego pola, aby utworzyć pojedynczy film.

mutation CreateMovie($title: String!, $releaseYear: Int!, $genre: String!, $rating: Int!) {
  movie_insert(data: {
    title: $title
    releaseYear: $releaseYear
    genre: $genre
    rating: $rating
  })
}

Mutacja z polem
movie_update

Pole movie_update reprezentuje mutację, która aktualizuje pojedynczy rekord w tabeli Movie.

Użyj tego pola, aby zaktualizować pojedynczy film według jego klucza.

mutation UpdateMovie($myKey: Movie_Key!, $genre: String, $rating: Int, $description: String) {
  movie_update(key: $myKey, data: {
    genre: $genre
    rating: $rating
    description: $description
  })
}

Mutacja z polem
movie_delete

Pole movie_delete reprezentuje mutację, która usuwa pojedynczy rekord w tabeli Movie.

Użyj tego pola, aby usunąć pojedynczy film według jego klucza.

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

Podstawowe elementy mutacji

SQL Connect mutacje to mutacje GraphQL z SQL Connect rozszerzeniami. Podobnie jak w przypadku zwykłej mutacji GraphQL możesz zdefiniować nazwę operacji i listę zmiennych GraphQL.

SQL Connect rozszerza zapytania GraphQL o dostosowane dyrektywy , takie jak @auth i @transaction.

Ta mutacja ma:

  • definicję typu mutation
  • nazwę operacji (mutacji) SignUp
  • jeden argument operacji $username
  • jedną dyrektywę @auth
  • jedno pole user_insert.
mutation SignUp($username: String!) @auth(level: USER) {
  user_insert(data: {
    id_expr: "auth.uid"
    username: $username
  })
}

Każdy argument mutacji wymaga deklaracji typu – wbudowanego, np. String, lub niestandardowego, zdefiniowanego w schemacie, np. Movie.

Pisanie podstawowych mutacji

Możesz zacząć pisać mutacje, aby tworzyć, aktualizować i usuwać poszczególne rekordy z bazy danych.

Tworzenie

Zacznijmy od podstawowych operacji tworzenia.

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

Możesz też użyć operacji 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"
  })
}

Wykonywanie aktualizacji

Oto aktualizacje. Producenci i reżyserzy z pewnością mają nadzieję, że te średnie oceny są zgodne z trendami.

Pole movie_update zawiera oczekiwany argument id do identyfikowania rekordu oraz pole data, którego możesz użyć do ustawiania wartości w tej aktualizacji.

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

Aby wykonać wiele aktualizacji, użyj pola 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
      })
}

Używanie operacji zwiększania, zmniejszania, dołączania i dodawania na początku z _update

W mutacjach _update i _updateMany możesz jawnie ustawiać wartości w data:, ale często bardziej sensowne jest zastosowanie operatora, np. zwiększania, aby zaktualizować wartości.

Aby zmodyfikować wcześniejszy przykład aktualizacji, załóżmy, że chcesz zwiększyć ocenę konkretnego filmu. Możesz użyć składni rating_update z operatorem inc.

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

SQL Connect obsługuje te operatory aktualizacji pól:

  • inc do zwiększania wartości typów danych Int, Int64, Float, Date i Timestamp
  • dec do zmniejszania wartości typów danych Int, Int64, Float, Date i Timestamp

W przypadku list możesz też aktualizować poszczególne wartości lub listy wartości za pomocą tych operatorów:

  • add do dołączania elementów, jeśli nie są jeszcze obecne na listach, z wyjątkiem list wektorowych
  • remove do usuwania wszystkich elementów, jeśli są obecne, z list, z wyjątkiem list wektorowych
  • append do dołączania elementów do list, z wyjątkiem list wektorowych
  • prepend do dodawania elementów na początku list, z wyjątkiem list wektorowych

Wykonywanie operacji usuwania

Możesz oczywiście usuwać dane filmów. Konserwatorzy filmów z pewnością będą chcieli, aby fizyczne kopie filmów były zachowane jak najdłużej.

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

W tym przypadku możesz użyć _deleteMany.

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

Pisanie mutacji dotyczących relacji

Zobacz, jak używać domyślnej mutacji _upsert w relacji.

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

Projektowanie schematów pod kątem wydajnych mutacji

SQL Connect udostępnia 2 ważne funkcje, które umożliwiają pisanie wydajniejszych mutacji i oszczędzanie operacji połączeń z serwerem.

Skalary kluczy to zwięzłe identyfikatory obiektów, które SQL Connect automatycznie tworzy na podstawie pól kluczy w Twoich schematach. Skalary kluczy zwiększają wydajność, ponieważ umożliwiają znalezienie w jednym wywołaniu informacji o tożsamości i strukturze Twoich danych. Są one szczególnie przydatne, gdy chcesz wykonywać sekwencyjne działania na nowych rekordach i potrzebujesz unikalnego identyfikatora do przekazania do przyszłych operacji, a także gdy chcesz uzyskać dostęp do kluczy relacyjnych, aby wykonywać dodatkowe, bardziej złożone operacje.

Dzięki wartościom serwera możesz skutecznie pozwolić serwerowi na dynamiczne wypełnianie pól w tabelach za pomocą zapisanych lub łatwo obliczalnych wartości zgodnie z określonymi wyrażeniami CEL po stronie serwera w argumencie expr. Możesz na przykład zdefiniować pole z sygnaturą czasową stosowaną, gdy pole jest dostępne za pomocą czasu przechowywanego w żądaniu operacji, updatedAt: Timestamp! @default(expr: "request.time").

Pisanie zaawansowanych mutacji: używanie składni field_expr do przekazywania wartości przez SQL Connect

Jak wspomnieliśmy w sekcji Skalary kluczy i wartości serwera, możesz zaprojektować schemat tak, aby serwer wypełniał wartości w przypadku typowych pól, takich jak ids i daty, w odpowiedzi na żądania klientów.

Możesz też używać danych, takich jak identyfikatory użytkowników, wysyłanych w SQL Connect request obiektach z aplikacji klienckich.

Podczas implementowania mutacji używaj składni field_expr, aby wywoływać aktualizacje generowane przez serwer lub uzyskiwać dostęp do danych z żądań. Aby na przykład przekazać autoryzację uid przechowywaną w żądaniu do operacji _upsert, przekaż "auth.uid" w polu 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 })
}

W znanej aplikacji do tworzenia list zadań podczas tworzenia nowej listy zadań możesz przekazać id_expr, aby poinstruować serwer, aby automatycznie wygenerował identyfikator UUID dla listy.

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

Więcej informacji znajdziesz w sekcji Skalary _Expr w dokumentacji skalarów .

Pisanie zaawansowanych mutacji: operacje wieloetapowe

W wielu sytuacjach możesz chcieć uwzględnić w jednej mutacji wiele pól zapisu (np. wstawień). Możesz też chcieć odczytać bazę danych podczas wykonywania mutacji, aby wyszukać i zweryfikować istniejące dane przed wykonaniem np. wstawień lub aktualizacji. Te opcje pozwalają oszczędzać operacje w obie strony, a tym samym koszty.

SQL Connect umożliwia wykonywanie logiki wieloetapowej w mutacjach dzięki obsłudze:

  • wielu pól zapisu,

  • wielu pól odczytu w mutacjach (za pomocą słowa kluczowego pola query),

  • dyrektywy @transaction, która zapewnia obsługę transakcji znaną z relacyjnych baz danych,

  • dyrektywy @check, która umożliwia ocenianie zawartości odczytów za pomocą wyrażeń CEL, a na podstawie wyników takiej oceny:

    • kontynuowanie tworzenia, aktualizowania i usuwania zdefiniowanego przez mutację,
    • kontynuowanie zwracania wyników pola zapytania,
    • używanie zwróconych wiadomości do wykonywania odpowiedniej logiki w kodzie klienta,
  • dyrektywy @redact, która umożliwia pomijanie wyników pola zapytania w wynikach protokołu przewodowego,

  • powiązania CEL response, które przechowuje skumulowane wyniki wszystkich mutacji i zapytań wykonywanych w złożonej operacji wieloetapowej. Możesz uzyskać dostęp do powiązania response:

    • w dyrektywach @check za pomocą argumentu expr:,
    • za pomocą wartości serwera, używając składni field_expr.

Dyrektywa @transaction

Obsługa mutacji wieloetapowych obejmuje obsługę błędów za pomocą transakcji.

Dyrektywa @transaction wymusza, aby mutacja – z jednym polem zapisu (np. _insert lub _update) lub z wieloma polami zapisu – zawsze była uruchamiana w transakcji bazy danych.

  • Mutacje bez @transaction wykonują każde pole główne po kolei. Operacja wyświetla wszystkie błędy jako częściowe błędy pola, ale nie skutki kolejnych wykonań.

  • Mutacje z @transaction zawsze kończą się powodzeniem lub niepowodzeniem. Jeśli którekolwiek z pól w transakcji się nie powiedzie, cała transakcja zostanie wycofana.

Dyrektywy @check i @redact

Dyrektywa @check sprawdza, czy określone pola są obecne w wynikach zapytania. Do testowania wartości pól używane jest wyrażenie w języku Common Expression Language (CEL). Domyślnym działaniem dyrektywy jest sprawdzanie i odrzucanie węzłów, których wartość to null lub [] (puste listy).

Dyrektywa @redact redaguje część odpowiedzi od klienta. Pola redagowane są nadal oceniane pod kątem efektów ubocznych (w tym zmian danych i @check), a wyniki są nadal dostępne w późniejszych krokach w wyrażeniach CEL.

Używanie @check, @check(message:) i @redact

Głównym zastosowaniem @check i @redact jest wyszukiwanie powiązanych danych w celu określenia, czy określone operacje powinny być autoryzowane, przy użyciu wyszukiwania w logice, ale ukrywanie go przed klientami. Zapytanie może zwracać przydatne wiadomości do prawidłowej obsługi w kodzie klienta.

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

Więcej informacji o dyrektywach @check i @redact w przypadku sprawdzania autoryzacji, znajdziesz w sekcji Wyszukiwanie danych autoryzacji.

Używanie @check do sprawdzania poprawności kluczy

Niektóre pola mutacji, np. _update, mogą nie wykonywać żadnych działań, jeśli rekord z określonym kluczem nie istnieje. Podobnie wyszukiwania mogą zwracać wartość null lub pustą listę. Nie są one uznawane za błędy, dlatego nie spowodują wycofania.

Aby zapobiec takiemu wynikowi, sprawdź, czy klucze można znaleźć za pomocą dyrektywy @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")
}

Używanie powiązania response do łączenia mutacji wieloetapowych

Podstawowym sposobem tworzenia powiązanych rekordów, np. nowego rekordu Movie i powiązanego z nim rekordu MovieMetadata jest:

  1. wywołanie mutacji _insert dla Movie,
  2. zapisanie zwróconego klucza utworzonego filmu,
  3. następnie wywołanie drugiej mutacji _insert w celu utworzenia rekordu MovieMetadata.

Dzięki SQL Connect możesz jednak obsługiwać ten typowy przypadek w ramach jednej operacji wieloetapowej, uzyskując dostęp do wyników pierwszego _insert w drugim _insert.

Stworzenie udanej aplikacji do recenzowania filmów wymaga dużo pracy. Śledźmy naszą listę zadań na podstawie nowego przykładu.

Używanie response do ustawiania pól z wartościami serwera

W tej mutacji listy zadań:

  • Powiązanie response reprezentuje dotychczasowy obiekt odpowiedzi częściowej, który obejmuje wszystkie pola mutacji najwyższego poziomu przed bieżącym.
  • Wyniki początkowej operacji todoList_insert, która zwraca pole id (klucz), są dostępne później w response.todoList_insert.id, dzięki czemu możemy od razu wstawić nowy element listy zadań.
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,
  })
}

Używanie response do sprawdzania poprawności pól za pomocą @check

response jest też dostępne w @check(expr: "..."), dzięki czemu możesz używać go do tworzenia jeszcze bardziej skomplikowanej logiki po stronie serwera. W połączeniu z krokami query { … } w mutacjach możesz osiągnąć znacznie więcej bez dodatkowych połączeń z serwerem.

W tym przykładzie zwróć uwagę, że @check ma już dostęp do response.query, ponieważ @check zawsze jest uruchamiane po kroku, do którego jest dołączone.

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

Więcej informacji o powiązaniu response znajdziesz w dokumentacji CEL.

Omówienie przerwanych operacji za pomocą @transaction i query @check

W mutacjach wieloetapowych mogą występować błędy:

  • Operacje na bazie danych mogą się nie powieść.
  • Logika `query @check` może zakończyć operacje.

SQL Connect zaleca używanie dyrektywy @transaction w przypadku mutacji wieloetapowych. Zapewnia to większą spójność bazy danych i wyników mutacji, które są łatwiejsze w obsłudze w kodzie klienta:

  • Przy pierwszym błędzie lub nieudanym @check operacja zostanie zakończona, więc nie trzeba zarządzać wykonywaniem kolejnych pól ani ocenianiem CEL.
  • Wycofania są wykonywane w odpowiedzi na błędy bazy danych lub logikę @check, co zapewnia spójny stan bazy danych.
  • Błąd wycofania jest zawsze zwracany do kodu klienta.

W niektórych przypadkach użycia możesz nie używać @transaction: możesz wybrać spójność wynikową, jeśli na przykład potrzebujesz większej przepustowości, skalowalności lub dostępności. Musisz jednak zarządzać bazą danych i kodem klienta, aby uwzględnić te wyniki:

  • Jeśli jedno pole nie powiedzie się z powodu operacji na bazie danych, kolejne pola będą nadal wykonywane. Nieudane @check nadal powodują zakończenie całej operacji.
  • Wycofania nie są wykonywane, co oznacza mieszany stan bazy danych z niektórymi udanymi i niektórymi nieudanymi aktualizacjami.
  • Operacje z @check mogą dawać bardziej niespójne wyniki, jeśli logika @check używa wyników odczytów lub zapisów z poprzedniego kroku.
  • Wynik zwrócony do kodu klienta będzie zawierał bardziej złożoną mieszankę odpowiedzi o powodzeniu i niepowodzeniu, które trzeba będzie obsłużyć.

Dyrektywy dotyczące mutacji SQL Connect

Oprócz dyrektyw używanych do definiowania typów i tabel, SQL Connect udostępnia dyrektywy @auth, @check, @redact i @transaction, które rozszerzają działanie operacji.

Dyrektywa Dotyczy Opis
@auth Zapytania i mutacje Określa zasady autoryzacji dla zapytania lub mutacji. Zapoznaj się z przewodnikiem po autoryzacji i poświadczeniu.
@check Pola query w operacjach wieloetapowych Sprawdza, czy określone pola są obecne w wynikach zapytania. Do testowania wartości pól używane jest wyrażenie w języku Common Expression Language (CEL). Zobacz Operacje wieloetapowe.
@redact Zapytania Redaguje część odpowiedzi od klienta. Zobacz Operacje wieloetapowe.
@transaction Mutacje Wymusza, aby mutacja zawsze była uruchamiana w transakcji bazy danych. Zobacz Operacje wieloetapowe.

Dalsze kroki

Może Cię zainteresować: