Návrh schématu GraphQL a resolverů: implementace

Návrh GraphQL schématu a resolver: Implementace

Cíle a principy návrhu GraphQL schématu

GraphQL schéma je kontrakt mezi klienty a serverem. Definuje tvary dat (typy), způsob jejich čtení (Query), změn (Mutation) a událostní proudy (Subscription). Návrh schématu a resolverů mají řídit potřeby domény, nikoli fyzický model databáze. Klíčové charakteristiky kvalitního návrhu jsou: explicitní typy, jasná nulovatelnost, stabilní identita uzlů, dobře definované hranice (argumenty, filtry, stránkování) a pozorovatelnost (telemetrie, chybové kódy, latence).

Doménové modelování: od Ubiquitous Language k SDL

  • Ubiquitous Language: nejprve popište subdomény, entity a jejich vztahy jazykem byznysu. Vyhněte se databázovým pojmům v rozhraní.
  • Agregáty a hranice: určete, které uzly mají globální identitu (např. User, Order) a které jsou hodnotovými objekty (Money, Address).
  • GraphQL SDL jako kontrakt: zapisujte typy s popisky (""" … """) a direktivami pro nástroje (např. @deprecated).

Základní stavební prvky schématu: typy, rozhraní, unie

  • Objektové typy: reprezentují doménové entity. Příklad: type User { id: ID!, email: String!, name: String, roles: [Role!]! }
  • Rozhraní (Interfaces): pro polymorfismus a sdílená pole. Příklad: interface Node { id: ID! }
  • Unie (Union): pro variantní návratové hodnoty bez společných polí. Příklad: union SearchResult = User | Organization | Article
  • Vlastní skalární typy: scalar DateTime, scalar URL; vždy specifikujte formát a validaci.
  • Vstupní typy: jsou určeny pro argumenty, aby byly stabilní a rozšiřitelné. Příklad: input UserFilter { email: String, role: Role, createdFrom: DateTime }

Nulovatelnost a kontrakt kvality dat

Operátor ! vyjadřuje, že hodnota nesmí být null. Používejte jej uvážlivě: přísná nulovatelnost zvyšuje spolehlivost klientů, ale vyžaduje pečlivé strategie resolverů a deterministické chování při chybách. Držte se zásady: non-null pro identitu a klíčová pole, nullable pro volitelné a odvozené hodnoty.

Kořenové operace: Query, Mutation, Subscription

  • Query: navrhujte jako čtení orientované na konkrétní případy použití (use case) (např. userById(id: ID!): User, search(query: String!, first: Int, after: Cursor): SearchConnection!).
  • Mutation: používejte slovesa označující příkazy (command) a vstupní typy (input) (createUser(input: CreateUserInput!): CreateUserPayload!) pro stabilitu a možnost rozšíření o clientMutationId.
  • Subscription: explicitně popište sémantiku streamu a filtry (např. orderStatusChanged(orderId: ID!): OrderStatusEvent!); dbejte na autorizaci.

Stránkování a kolekce: offset vs. kurzory

  • Offset: jednoduchý pro malé, stabilní seznamy; citlivý na změny pořadí.
  • Stránkování podle kurzoru (Relay Connection): edges { node, cursor }, pageInfo { hasNextPage, endCursor }; odolnější vůči změnám v čase. Doporučuje se pro velké seznamy.
  • Filtry a řazení: udržujte filter a orderBy jako vstupní typy (input); umožníte tak jejich vývoj bez narušení kontraktu.

Architektura resolverů: vrstvy a odpovědnosti

  • Čisté resolvery: tenké adaptéry bez business logiky; orchestrují přístup ke službám a mapují data do tvaru schématu.
  • Servisní vrstva: doménové služby, které řeší pravidla a transakce; lze je znovu použít i mimo GraphQL.
  • DataLoader/hromadné načítání: eliminace dotazů N+1 agregací požadavků podle klíče a ukládáním do mezipaměti na úrovni požadavku.
  • Kontext: injektujte uživatele, tenanta, jazykové nastavení a trace ID; nepoužívejte mezipaměť specifickou pro požadavek s globálním dopadem.

Eliminace problému N+1 a strategie načítání

  • DataLoader pro každý požadavek: dávkově načítejte podle ID a ukládejte výsledky do mezipaměti po dobu jednoho dotazu.
  • Projekce a výběr polí: přenášejte do datové vrstvy informaci o požadovaných polích (např. výběr polí → projekce SQL).
  • Joiny vs. následné dotazy: u malých vztahů se vyplatí join/projekce; pro velké seznamy upřednostněte stránkování podle kurzoru.

Model chyb a návratové payloady

  • Částečný úspěch: GraphQL umožňuje vrátit data i s chybami; proto navrhujte deterministickou nulovatelnost a chyby v errors se strojově čitelným extensions.code.
  • Payload mutace: vracejte { success: Boolean!, user: User, errors: [UserError!]! } s polem pro chyby validace (na úrovni polí).
  • Business kódy: standardizujte extensions.code (např. UNAUTHORIZED, FORBIDDEN, NOT_FOUND, CONFLICT).

Autentizace a autorizace

  • Autentizace v kontextu: ověřte token a načtěte identitu a role dříve, než se spustí resolvery.
  • Autorizace jako knihovna: zajistěte, aby resolvery volaly politiky (ABAC/RBAC), nebo použijte direktivy (např. @auth(role: ADMIN)) napojené na validační logiku.
  • Ochrana na úrovni polí: přístup k citlivým polím (např. User.email) kontrolujte v resolveru daného pole, ne pouze v kořenovém resolveru.

Vývoj schématu a kompatibilita

  • Rozšiřování bez narušení kompatibility: přidávejte volitelná pole, nové typy a nové unie; nepřidávejte ! tam, kde dříve nebyl.
  • Označení za zastaralé: používejte @deprecated(reason: "...") a zveřejňujte plán odstranění.
  • Verzování: upřednostňujte evoluční vývoj v rámci jednoho endpointu; endpoint v2 vytvářejte pouze při zásadní refaktorizaci.

Federace a modulární schéma

  • Propojování schémat / federace: rozdělte monolit na doménové podgrafy (Users, Orders, Catalog) a skládejte je přes gateway; dbejte na globální identitu a reference.
  • Kontrakty mezi týmy: každý tým vlastní část schématu; gateway vynucuje kompozici, limity a pozorovatelnost.

Výkon, mezipaměť a perzistentní dotazy

  • Perzistentní dotazy: připněte dotazy identifikované pomocí hashů; snížíte tak riziko DoS prostřednictvím textu dotazu a zlepšíte TTFB.
  • Vrstvy mezipaměti: mezipaměť odpovědí pro idempotentní dotazy, mezipaměť objektů (např. podle id), mezipaměť na okraji sítě s Cache-Control pro operace označené jako @cacheable.
  • Limity a složitost: vypočítejte náročnost dotazu (hloubku, násobitele seznamů), zaveďte omezení počtu požadavků a časové limity pro jednotlivé resolvery.

Bezpečnost a odolnost

  • Validace vstupů: validujte délky řetězců, formáty a číselné rozsahy; chraňte se proti Regex DoS a hluboké rekurzi.
  • Aliasy a fragmenty: omezte počet aliasů, hloubku fragmentů a cyklické reference.
  • Pozorovatelnost: trace ID v kontextu, metriky latencí a počtu volání resolverů, vzorkování a strukturované logy s poli operationName a variables (bez tajných údajů).

Testování a kvalita

  • Jednotkové testy resolverů: simulujte služby a kontext; testujte autorizaci a chybové větve.
  • Integrační testy operací: spouštějte skutečné GraphQL dotazy proti testovacímu serveru s databází v paměti.
  • Kontraktní testy: ověřujte, že publikované SDL odpovídá očekáváním klientů; využijte registr schémat a kontrolu nekompatibilních změn v CI.

Praktický návrhový vzor pro mutace

Pro každou mutaci používejte dvojici input a payload, aby bylo možné snadno přidávat pole, aniž by se narušila funkčnost klientů. Příklad: input CreateUserInput { email: String!, name: String, role: Role = USER } a type CreateUserPayload { success: Boolean!, user: User, errors: [UserError!]! }.

Ukázkový výřez SDL pro blogovou doménu

interface Node { id: ID! }
type User implements Node { id: ID!, email: String!, name: String }
type Post implements Node { id: ID!, title: String!, body: String!, author: User! }
input PostFilter { authorId: ID, query: String }
type PostEdge { node: Post!, cursor: String! }
type PostConnection { edges: [PostEdge!]!, pageInfo: PageInfo! }
type PageInfo { hasNextPage: Boolean!, endCursor: String }
type Query { post(id: ID!): Post, posts(first: Int = 20, after: String, filter: PostFilter): PostConnection! }
input CreatePostInput { title: String!, body: String! }
type CreatePostPayload { success: Boolean!, post: Post, errors: [UserError!]! }
type Mutation { createPost(input: CreatePostInput!): CreatePostPayload! }

Resolvery: vzorce chování

  • Kořenové resolvery (Query/Mutation): validujte vstupy, kontrolujte oprávnění a delegujte na doménové služby.
  • Resolvery polí (např. Post.author): používejte DataLoader pro hromadné načítání autora pomocí loadMany podle authorId.
  • Resolvery spojení: implementujte kurzory jako bezpečné, neprůhledné hodnoty (např. base64 s id|sortKey) a vracejte konzistentní pageInfo.

Lokalizace, měny a jednotky

  • Jazykové nastavení v kontextu: předávejte jazyk a časové pásmo prostřednictvím kontextu a respektujte je v resolverech.
  • Typy pro hodnoty: místo Float používejte strukturované typy (Money { amount: Decimal!, currency: Currency! }) a pevně definované jednotky (Length, Weight).

Ověřitelnost a dokumentace

  • Popisy (docstrings): u každého typu a pole sjednoťte terminologii a uvádějte příklady.
  • Registr schémat a changelog: zveřejňujte SDL, sledujte změny a rozesílejte upozornění při možných nekompatibilních změnách.
  • Prozkoumávání: povolte GraphiQL/Explorer pouze v bezpečných prostředích; v variables maskujte tajné údaje.

Kontrolní seznam pro závěrečnou revizi schématu

  • Jsou ID stabilní a globálně jedinečná? Je rozhraní Node implementováno konzistentně?
  • Je nulovatelnost konzistentní a odpovídá skutečnému chování?
  • Mají kolekce stránkování podle kurzoru a definované řazení?
  • Jsou mutace idempotentní tam, kde je to žádoucí, a vracejí payload s chybami?
  • Nemají resolvery problémy N+1 a využívají dávkové načítání a metriky?
  • Jsou autorizace a limity požadavků vynucovány ve správných vrstvách?

Závěr: navrhněte kontrakt pro lidi, ne pro databázi

Úspěšné GraphQL schéma odráží doménu a potřeby klientů, nikoli fyzické tabulky. Dobře navržené typy, disciplinovaná nulovatelnost, promyšlené stránkování, čisté resolvery s dávkovým načítáním, robustní model chyb a průběžná pozorovatelnost vedou ke stabilnímu, výkonnému a bezpečnému API, které se může vyvíjet bez narušení funkčnosti klientů.