Produkty Poradenství O nás Blog Kontakt English
arrow_back Zpět na blog

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

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":

GROOVY
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í Mono na 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:

GROOVY
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ě:

GROOVY
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:

GRAPHQL
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:

GRAPHQL
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ř. String na Int)
  • 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:

Bash
# CI pipeline — compare current schema against the deployed version
graphql-inspector diff schema-deployed.graphqls schema-current.graphqls

Toto 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:

graph TD Client --> GW["API Gateway
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:

  1. 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 @DgsQuery a @DgsMutation fungují beze změny.

  2. 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í.

  3. 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.

  4. 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:

graph LR S1["Service A
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:

Text
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.java

To 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ý:

Java
// 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:

graph TD subgraph Product Module PG["ProductDataFetcher
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ématuSchema-first s doménově specifickými type souboryČást 1
Typová bezpečnostDGS code generation + MapStruct mapperyČást 1
Izolace vrstevAnti-corruption layers mezi API, doménou a persistencíČást 1, Část 5
N+1 výkonMappedBatchLoader 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í chybDataFetcherExceptionHandler se sanitizacíČást 3
Ochrana proti zneužití dotazůLimity hloubky + complexityČást 3
Real-time aktualizace@DgsSubscription s WebSocket transportemČást 4
Asynchronní vzoryCompletableFuture pro fetchery, Mono pro službyČást 4
ObservabilitaAOP metriky + strukturované logováníČást 4
TestováníSpock specifikace s mockovanými závislostmiČást 5 (tento článek)
Evoluce schématuAditivní změny + deprecationČást 5 (tento článek)
FederationKompozice na úrovni gatewayČást 5 (tento článek)
Typová bezpečnost na frontenduGraphQL Code Generator + TypedDocumentNodeČást 6
ObservabilitaMetriky operací, SLO histogramy, distribuované tracováníČást 7
VýkonPersisted 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.

Další z blogu