DGS ve velkém měřítku: Testování, evoluce schématu a federation

Část 5 ze 7 v sérii „Production GraphQL with Netflix DGS"
Postavit GraphQL API je jedna věc. Provozovat stovky operací v produkci, spolehlivě je testovat a vyvíjet schéma bez rozbití klientů — to je teprve začátek skutečného inženýrství. Tento závěrečný článek pokrývá postupy, na kterých záleží, jakmile vaše DGS API přeroste pár dotazů.
Testování DGS komponent
DGS data fetchery jsou Spring beany s injektovanými závislostmi. To je činí přímočarými pro unit testování — zamockujte závislosti, zavolejte metodu, ověřte výsledek.
Unit testování se Spockem
Expresivní syntaxe Spocku se dobře hodí k vzoru DGS „jedna metoda na operaci":
class ProductDataFetcherSpec extends Specification {
ProductService productService = Mock()
CurrentUserProvider currentUser = Mock()
ProductDataFetcher dataFetcher
def setup() {
dataFetcher = new ProductDataFetcher(productService, currentUser)
}
def "should fetch product by ID"() {
given:
def productId = "prod-123"
def mockProduct = new Product(id: productId, name: "Widget")
when:
def result = dataFetcher.product(productId).block()
then:
1 * productService.findById(productId) >> Mono.just(mockProduct)
result.id == productId
result.name == "Widget"
}
def "should return null when product not found"() {
when:
def result = dataFetcher.product("non-existent").block()
then:
1 * productService.findById("non-existent") >> Mono.empty()
result == null
}
def "should propagate service errors"() {
given:
def error = new RuntimeException("Database unavailable")
when:
dataFetcher.product("prod-123").block()
then:
1 * productService.findById(_) >> Mono.error(error)
thrown(RuntimeException)
}
}Klíčové vzory:
- Constructor injection — data fetcher vytvoříte s mockovanými závislostmi v
setup(). Žádný Spring kontext není potřeba. .block()— převede reaktivníMonona synchronní výsledek pro aserce. Přijatelné v testech, nikdy v produkci.- Ověření interakcí —
1 * productService.findById(productId)ověří, že služba byla zavolána přesně jednou se správným argumentem.
Testování autentizovaných operací
Zamockujte auth supplier tak, aby vracel různá user ID:
def "should fetch authenticated user's orders"() {
given:
def userId = "user-42"
def mockPage = new OrderPage(items: [new Order(id: "order-1")], totalElements: 1)
when:
def result = dataFetcher.myOrders(0, 10).block()
then:
1 * currentUser.getUserId() >> Mono.just(userId)
1 * orderService.findByOwner(userId, 0, 10) >> Mono.just(mockPage)
result.items.size() == 1
}
def "should reject unauthenticated access"() {
when:
dataFetcher.myOrders(0, 10).block()
then:
1 * currentUser.getUserId() >> Mono.error(new AccessDeniedException("Not authenticated"))
thrown(AccessDeniedException)
}Poznámka: anotace @PreAuthorize se v unit testech nevyhodnocují (jsou to Spring AOP proxy). Pro testování autorizačních pravidel použijte integrační testy s @SpringBootTest a reálným security kontextem.
Testování Data Loaderů
Data loadery jsou samostatné třídy s jedinou metodou — testujte je izolovaně:
class ProductDataLoaderSpec extends Specification {
ProductRepository productRepository = Mock()
ProductDataLoader loader
def setup() {
loader = new ProductDataLoader(productRepository)
}
def "should batch load products by IDs"() {
given:
def ids = ["prod-1", "prod-2", "prod-3"] as Set
def products = [
new Product(id: "prod-1", name: "Widget"),
new Product(id: "prod-2", name: "Gadget")
// prod-3 intentionally missing — deleted product
]
when:
def result = loader.load(ids).toCompletableFuture().get()
then:
1 * productRepository.findByIds(ids as List) >> Flux.fromIterable(products)
result.size() == 2
result["prod-1"].name == "Widget"
result["prod-2"].name == "Gadget"
result["prod-3"] == null // Missing entries return null
}
}Kontrakt MappedBatchLoader znamená, že chybějící klíče jednoduše v mapě nejsou. DataLoader framework to pro žádající field resolver přeloží na null.
Evoluce schématu
GraphQL schémata jsou kontrakty. Jejich změna může rozbít klienty. Zde je návod, jak je bezpečně vyvíjet.
Aditivní změny (vždy bezpečné)
Tyto změny nikdy nerozbijí existující klienty:
type Product {
id: ID!
name: String!
description: String
price: Float!
category: ProductCategory!
# New fields — clients that don't request them are unaffected
rating: Float # ← Added
reviewCount: Int # ← Added
tags: [String] # ← Added
}
type Query {
product(id: ID!): Product
products(...): ProductPage
# New queries — existing clients don't know about them
productsByTag(tag: String!, ...): ProductPage # ← Added
featuredProducts(...): ProductPage # ← Added
}Přidání polí, dotazů, mutací nebo typů je vždy zpětně kompatibilní.
Deprecation (šetrné odstraňování)
Když potřebujete přejmenovat nebo nahradit pole, nejdříve ho označte jako deprecated:
type Product {
id: ID!
name: String!
# Phase 1: Add new field alongside old one
productName: String! # ← New preferred field
name: String! @deprecated(reason: "Use productName instead") # ← Mark old one
}GraphQL nástroje (GraphiQL, Apollo Studio, code generátory) zobrazují klientům varování o deprecation. Poté co monitoring potvrdí nulové používání deprecated pole, můžete ho odstranit.
Breaking changes (vyhněte se jim, pokud je to možné)
Tyto změny rozbijí klienty a měly by být poslední možností:
- Odstranění pole nebo typu
- Změna typu pole (např.
StringnaInt) - Změna nullable pole na non-nullable
- Přidání povinného argumentu k existujícímu dotazu
Pokud se tomu nelze vyhnout, koordinujte se s konzumenty API a naplánujte migrační období.
Lintování schématu
Zachyťte breaking changes dříve, než se dostanou do produkce. Nástroje jako graphql-inspector dokáží porovnat verze schématu:
# CI pipeline — compare current schema against the deployed version
graphql-inspector diff schema-deployed.graphqls schema-current.graphqlsToto zachytí odstranění polí, změny typů a další breaking modifikace jako CI bránu.
Federation: jeden graph, více služeb
Jak váš systém roste, jediná backendová služba může být příliš velká. GraphQL federation vám umožní rozdělit schéma napříč více službami, zatímco klientům prezentujete jednotné API.
Vzor gateway
Místo federace na úrovni DGS (pomocí @DgsEntityFetcher) mnoho týmů používá gateway router, který skládá schémata z více služeb:
Auth + rate limiting"] GW --> Router["GraphQL Router
Schema composition + query planning"] Router --> A["Service A — DGS
Products, Inventory"] Router --> B["Service B — DGS
Orders, Payments"] Router --> C["Service C — DGS
Users, Notifications"] style Client fill:#4a9eff,stroke:#2171c7,color:#fff style GW fill:#7c4dff,stroke:#5e35b1,color:#fff style Router fill:#7c4dff,stroke:#5e35b1,color:#fff style A fill:#00bfa5,stroke:#00897b,color:#fff style B fill:#00bfa5,stroke:#00897b,color:#fff style C fill:#00bfa5,stroke:#00897b,color:#fff
Router (Apollo Router nebo podobné federation-capable routery) čte schéma každé služby, skládá je do supergraphu a směruje příchozí dotazy na správnou službu.
Proč federace na úrovni gateway
Federace na gateway místo uvnitř DGS samotného má praktické výhody:
Služby zůstávají jednoduché. Každá DGS služba vlastní své schéma a data fetchery bez federation-specifických anotací. Standardní vzory
@DgsQuerya@DgsMutationfungují beze změny.Nezávislý deployment. Služby lze nasazovat, vracet zpět a škálovat nezávisle. Router znovu složí supergraph, když se schéma služby změní.
Oddělení zodpovědností. Router řeší průřezové záležitosti (plánování dotazů, deduplikace, caching), které by jednotlivé služby neměly vlastnit.
Technologická svoboda. Ne každá služba musí být DGS. Router složí jakoukoli GraphQL službu bez ohledu na framework za ní.
Kompozice supergraphu
Supergraph schéma se typicky skládá v rámci CI/CD:
schema"] --> REG["Schema Registry"] S2["Service B
schema"] --> REG S3["Service C
schema"] --> REG REG --> COMP["Composition Step
CI/CD pipeline"] COMP --> SG["Supergraph Schema"] SG --> R["Router reloads
& validates"] style REG fill:#7c4dff,stroke:#5e35b1,color:#fff style COMP fill:#4a9eff,stroke:#2171c7,color:#fff style SG fill:#00bfa5,stroke:#00897b,color:#fff style R fill:#00bfa5,stroke:#00897b,color:#fff
Kompozice zachytí konflikty včas — dvě služby definující stejný typ, nekompatibilní typy polí nebo chybějící entity reference se objeví jako build selhání, ne jako runtime chyby.
Organizace DGS komponent ve velkém měřítku
Když máte desítky doménových modulů s vlastními dotazy, mutacemi a data loadery, organizace je klíčová.
Doménově řízená struktura
Seskupte DGS komponenty podle business domény, ne podle technické role:
com.example.product.graphql/
├── ProductDataFetcher.java # Queries
├── CreateProductMutation.java # Mutations (one per operation)
├── UpdateProductMutation.java
├── ProductStockResolver.java # Field resolvers
└── ProductDataLoader.java # Data loaders
com.example.order.graphql/
├── OrderDataFetcher.java
├── PlaceOrderMutation.java
├── OrderProductResolver.java
└── OrderCustomerDataLoader.javaTo je lepší než technické seskupení jako com.example.graphql.queries/, com.example.graphql.mutations/ atd. — protože když debugujete vyhledávání produktů, chcete mít veškerý GraphQL kód související s produkty na jednom místě.
Jedna třída na mutaci
Malé API si může dovolit všechny mutace v jedné třídě. Ve větším měřítku ale vzniká god class, která lineárně roste s každou novou operací. Vzor jedna mutace na třídu udržuje každý soubor zaměřený:
// CreateProductMutation.java — ~40 lines, single responsibility
@DgsComponent
@RequiredArgsConstructor
public class CreateProductMutation {
private final ProductService productService;
private final CurrentUserProvider currentUser;
@DgsMutation
@PreAuthorize("hasRole('ADMIN')")
public Mono<Product> createProduct(@InputArgument CreateProductInput input) {
return currentUser.getUserId().flatMap(userId ->
productService.create(input, userId));
}
}Každý soubor je malý, testovatelný a jeho účel je zřejmý. Název souboru vám přesně říká, co dělá.
Hranice modulů a Anti-Corruption Layer
Pokud používáte Spring Modulith nebo podobnou modulární architekturu, DGS komponenty by měly respektovat hranice modulů:
- DGS komponenta modulu produktů může volat produktovou službu přímo.
- Pro přístup k datům objednávek by měla projít přes publikované API (query gateway, event nebo veřejné rozhraní) — ne importovat interní třídy modulu objednávek.
Jedná se o stejný princip Anti-Corruption Layer z části 1, aplikovaný na úrovni modulů. V části 1 jsme umístili mappery mezi GraphQL vrstvu, doménový model a databázi, aby jeden model nekorumpoval druhý. Ve větším měřítku se stejný vzor aplikuje mezi moduly:
GraphQL layer"] PS["ProductService
Domain layer"] PR["ProductRepository
Persistence layer"] PG --> PS --> PR end subgraph Order Module OG["OrderDataFetcher
GraphQL layer"] OS["OrderService
Domain layer"] OR["OrderRepository
Persistence layer"] OG --> OS --> OR end OS -->|"ACL: published API
or event"| PS style PG fill:#4a9eff,stroke:#2171c7,color:#fff style PS fill:#00bfa5,stroke:#00897b,color:#fff style PR fill:#ff7043,stroke:#e64a19,color:#fff style OG fill:#4a9eff,stroke:#2171c7,color:#fff style OS fill:#00bfa5,stroke:#00897b,color:#fff style OR fill:#ff7043,stroke:#e64a19,color:#fff
Služba modulu objednávek neimportuje ProductEntity ani nevolá ProductRepository přímo. Místo toho komunikuje přes ACL — publikované rozhraní, dotaz nebo doménový event — který překládá mezi modely obou modulů. Každý modul vlastní své typy a může se vyvíjet nezávisle.
To je důležité, protože hranice modulů se při federaci stávají hranicemi služeb. Pokud modul objednávek už komunikuje s produkty přes dobře definovaný ACL, jeho extrakce do samostatné DGS služby je změna nasazení, ne přepisování kódu. Anti-corruption layers, které jste vybudovali v monolitu, se stanou service kontrakty vašeho distribuovaného systému.
Rekapitulace: checklist pro produkční GraphQL
Napříč touto sedmidílnou sérií jsme pokryli:
| Oblast | Řešení | Článek |
|---|---|---|
| Návrh schématu | Schema-first s doménově specifickými type soubory | Část 1 |
| Typová bezpečnost | DGS code generation + MapStruct mappery | Část 1 |
| Izolace vrstev | Anti-corruption layers mezi API, doménou a persistencí | Část 1, Část 5 |
| N+1 výkon | MappedBatchLoader data loadery | Část 2 |
| Lazy načítání polí | @DgsData field resolvery | Část 2 |
| Stránkování | Offset-based *Page wrapper typy | Část 2 |
| Autentizace | @PreAuthorize + auth supplier | Část 3 |
| Zpracování chyb | DataFetcherExceptionHandler se sanitizací | Část 3 |
| Ochrana proti zneužití dotazů | Limity hloubky + complexity | Část 3 |
| Real-time aktualizace | @DgsSubscription s WebSocket transportem | Část 4 |
| Asynchronní vzory | CompletableFuture pro fetchery, Mono pro služby | Část 4 |
| Observabilita | AOP metriky + strukturované logování | Část 4 |
| Testování | Spock specifikace s mockovanými závislostmi | Část 5 (tento článek) |
| Evoluce schématu | Aditivní změny + deprecation | Část 5 (tento článek) |
| Federation | Kompozice na úrovni gateway | Část 5 (tento článek) |
| Typová bezpečnost na frontendu | GraphQL Code Generator + TypedDocumentNode | Část 6 |
| Observabilita | Metriky operací, SLO histogramy, distribuované tracování | Část 7 |
| Výkon | Persisted queries, complexity budgety, load testing | Část 7 |
DGS framework, nyní hluboce integrovaný se Spring for GraphQL, vám dává vyspělý stack pro budování GraphQL API, která jsou rychlá, bezpečná a udržovatelná ve velkém měřítku. Anotacemi řízený programovací model udržuje váš kód čitelný. Spring integrace vám dává celý ekosystém. A vzory v této sérii vás chrání před úskalími, která potkávají týmy s rostoucím API.
Co dál
V části 6 přejdeme z backendu na frontend — prozkoumáme, jak GraphQL Code Generator a TypedDocumentNode přinášejí stejnou typovou bezpečnost do klientského kódu, jakou DGS codegen poskytuje na serveru. Výsledkem je typově bezpečný řetězec od schématu po UI komponentu s compile-time zárukami na každé vrstvě.
Titulní foto: Yuriy Vertikov na Unsplash.


