Koncepcje, Idee, Strategie
Koncepcje, Idee, StrategieWyjaśnienie mutations zagnieżdżonych

Wyjaśnienie mutations zagnieżdżonych

Mutations to operacje, które mogą zmieniać dane na serwerze GraphQL, na przykład przy tworzeniu posta, aktualizacji nazwy użytkownika, dodaniu komentarza do posta czy innych działaniach.

W GraphQL mutations są eksponowane wyłącznie pod typem MutationRoot, w następujący sposób:

type MutationRoot {
  createPost(id: ID!, title: String!, content: String): Post!
  updateUserName(userID: ID!, newName: String!): User!
  addCommentToPost(postID: ID!, comment: String!, userID: ID): Comment!
}

(Schema GraphQL w tym przewodniku służy do zilustrowania przykładów; różni się od schematu dostarczonego przez plugin.)

Przy tym schemacie modyfikacja nazwy użytkownika wygląda następująco:

mutation {
  updateUserName(userID: 37, newName: "Peter") {
    name
  }
}

Mutations są eksponowane wyłącznie w mutation root object type, aby zagwarantować ich seryjne wykonanie, zgodnie z wyjaśnieniem w specyfikacji GraphQL:

It is expected that the top level fields in a mutation operation perform side‐effects on the underlying data system. Serial execution of the provided mutations ensures against race conditions during these side‐effects.

Termin "wykonanie seryjne" jest przeciwieństwem "wykonania równoległego", które jest zalecanym zachowaniem przy rozwiązywaniu pól.

Na przykład w poniższym query nie ma znaczenia, które pole (name czy email) serwer GraphQL rozwiąże jako pierwsze — mogą być rozwiązane równolegle:

query {
  user(by: { id: 37 }) {
    name
    email
  }
}

Mutations jednak zmieniają dane, więc kolejność rozwiązywania pól ma znaczenie — muszą być wykonywane seryjnie (w przeciwnym razie mogłyby powodować race conditions).

Na przykład dwa poniższe queries zwrócą różne wyniki:

# Query 1: po wykonaniu nazwa użytkownika będzie "John"
mutation {
  updateUserName(userID: 37, newName: "Peter") {
    name
  }
  updateUserName(userID: 37, newName: "John") {
    name
  }
}
 
# Query 2: po wykonaniu nazwa użytkownika będzie "Peter"
mutation {
  updateUserName(userID: 37, newName: "John") {
    name
  }
  updateUserName(userID: 37, newName: "Peter") {
    name
  }
}

Konsekwencją eksponowania mutations wyłącznie przez MutationRoot jest to, że ten typ staje się bardzo przeładowany, zawierając pola, które nie mają ze sobą nic wspólnego poza koniecznością seryjnego wykonania (co jest kwestią techniczną, a nie decyzją projektową interfejsu).

Argument za mutations zagnieżdżonymi

Spośród powyższych mutations tylko createPost naprawdę należy do typu MutationRoot, ponieważ tworzy nowy element od podstaw. Mutations updateUserName i addCommentToPost mogą natomiast mieć równoważne operacje stosowane na istniejącej encji innego typu:

type User {
  updateName(newName: String!): User!
}
 
type Post {
  addComment(comment: String!, userID: ID): Comment!
}

Przy tym schemacie modyfikacja nazwy użytkownika mogłaby wyglądać następująco:

mutation {
  user(ID: 37) {
    updateName(newName: "Peter") {
      name
    }
  }
}

Ta funkcja nosi nazwę "mutations zagnieżdżone": stosowanie mutation do wyniku innej operacji, zarówno query, jak i mutation.

Zwróć uwagę, jak użycie mutations zagnieżdżonych sprawia, że schema GraphQL staje się bardziej eleganckie:

  • Podczas gdy operacja MutationRoot.updateUserName musi przyjmować ID użytkownika, jej odpowiednik User.updateName nie musi, ponieważ jest już wykonywana na encji użytkownika
  • Nazwa pola zostaje skrócona z updateUserName do updateName

Ponadto usługa GraphQL staje się prostsza i bardziej zrozumiała, ponieważ możemy poruszać się po encjach grafu, aby modyfikować ich dane w taki sam sposób, jak je odpytujemy.

Mutations zagnieżdżone mogą sięgać wielu poziomów wgłąb. Na przykład możemy dodać komentarz do nowo utworzonego posta — wszystko w ramach jednego query:

mutation {
  createPost(ID: 37, title: "Hello world!", content: "Just another post") {
    id
    addComment(comment: "Lovely post") {
      id
    }
  }
}

Dzięki temu mutations zagnieżdżone mogą również poprawić wydajność poprzez zmniejszenie opóźnień wynikających z wielu podróży sieciowych — zamiast wykonywać wiele queries do mutowania kilku elementów, wystarczy jedno query.

Dlaczego mutations zagnieżdżone nie są częścią specyfikacji

Specyfikacja GraphQL jest zaprojektowana tak, aby działać ze wszystkimi implementacjami serwerów GraphQL w dowolnym języku. Jednak jej siłą napędową jest JavaScript za pośrednictwem graphql-js, implementacji referencyjnej.

Innymi słowy, żadna funkcja, która nie może być obsługiwana przez graphql-js, nie stanie się częścią specyfikacji.

Ponieważ JavaScript obsługuje promises, równoległa rozwiązywanie pól było możliwe, a paralelizm stał się jedną z fundamentalnych zasad przy pierwszym projektowaniu graphql-js, co widać w DataLoader (warstwa pobierania danych), której funkcje grupowania zwracają JavaScript promises.

Zalety równoległego wykonania dla wydajności są zbyt liczne, a mutations zagnieżdżone nie mogą współpracować z paralelizmem. Zdecydowano, że nie warto rezygnować z wykonania równoległego na rzecz mutations zagnieżdżonych.

Mutations zagnieżdżone a wydajność

W pluginie Gato GraphQL pola są zawsze rozwiązywane seryjnie, a kolejność ich rozwiązywania jest deterministyczna. (Ta cecha nie wpływa na wydajność rozwiązywania query, ponieważ serwer najpierw przekształca graf w query w model komponentów, który jest rozwiązywany w optymalnym czasie liniowym).

Oznacza to, że plugin może obsługiwać mutations zagnieżdżone, korzystając ze wszystkich ich zalet, bez ponoszenia żadnych konsekwencji.

Specyfikacja GraphQL

Ta funkcja nie jest obecnie częścią specyfikacji GraphQL, ale została zgłoszona w: