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

Schema-first GraphQL s Netflix DGS

Schema-first GraphQL s Netflix DGS

1. díl ze 7 série “Production GraphQL with Netflix DGS”


Pokud stavíte GraphQL API se Spring Bootem, pravděpodobně jste si všimli dvou velkých frameworků soupeřících o vaši pozornost: Netflix DGS a Spring for GraphQL. Roky si týmy musely vybrat jednu stranu. Tato volba už neexistuje — a příběh toho, jak tyto frameworky splynuly, stojí za pochopení dřív, než napíšete svůj první @DgsQuery.

Dva frameworky se staly jedním

Netflix open-sourcoval framework Domain Graph Service (DGS) v roce 2021. Byl ověřený na Netflixím měřítku — stovky služeb, tisíce dotazů — a nabízel něco, co Spring v té době neměl: propracovaný, anotacemi řízený programovací model pro GraphQL.

Mezitím tým Springu (ve spolupráci s maintainery GraphQL Java) vybudoval Spring for GraphQL se zaměřením na HTTP transport, WebSocket subscriptions a hlubokou integraci s ekosystémem Springu.

Nějakou dobu se tyto dva frameworky vyvíjely nezávisle. Týmy si vybíraly jeden, nebo druhý. Pak, v březnu 2024, Netflix a tým Springu oznámili hlubokou integraci: DGS 8.5.0 přinesl opt-in podporu Spring for GraphQL. V prosinci 2024 DGS 10.0.0 odstranil veškerý legacy kód — Spring for GraphQL se stal jediným execution enginem.

Zde je přehled, co dnes má na starosti který framework:

ZodpovědnostKdo ji řeší
HTTP transport (WebMVC, WebFlux, RSocket)Spring for GraphQL
Execution engine pro dotazySpring for GraphQL
WebSocket / SSE subscriptionsSpring for GraphQL
Konstrukce schématuDGS
Programovací model (@DgsQuery, @DgsData, @DgsDataLoader)DGS
Generování kóduDGS
Testovací utility (DgsQueryExecutor)DGS (proxuje do Springu)

Co to znamená pro vás: Získáte robustní transportní vrstvu a execution engine Springu v kombinaci s vývojářsky přívětivými anotacemi a generováním kódu od DGS. Nemusíte si vybírat — dostanete obojí.

Závislost pro DGS starter, kterou použijete:

KOTLIN
implementation("com.netflix.graphql.dgs:graphql-dgs-spring-graphql-starter")

Tato jediná závislost vám dá integrovaný stack. Starší graphql-dgs-spring-boot-starter (bez “spring-graphql” v názvu) je legacy cesta a pro nové projekty by se neměl používat.

Schema-first vývoj

DGS zastává přístup schema-first: napíšete GraphQL schéma v .graphqls souborech a framework z nich vygeneruje Java typy. Je to opak přístupu code-first (kde anotujete Java třídy a schéma se odvodí) a má to zásadní výhodu — schéma se stává kontraktem.

Vaše schéma je jediný zdroj pravdy, na který se odkazují frontendové týmy, konzumenti API i dokumentační nástroje. Generování kódu zajistí, že vaše Java typy vždy odpovídají.

Organizace souborů schématu

Malé API se může vejít do jediného schema.graphqls. Produkční API se stovkami operací ne. Zde je vzor, který dobře škáluje: jeden kořenový soubor pro operace, doménově specifické soubory pro typy.

Text
src/main/resources/schema/
├── schema.graphqls           # Všechny Query a Mutation definice
├── common_types.graphqls     # Sdílené scalars, enumy, utility typy
├── product_types.graphqls    # Product, ProductPage, CreateProductInput
├── order_types.graphqls      # Order, OrderItem, OrderStatus
├── user_types.graphqls       # User, UserSettings, UserPage
├── inventory_types.graphqls  # Stock, Warehouse, StockMovement
└── ...                       # Jeden soubor na doménu

Klíčové pravidlo: všechny definice Query a Mutation žijí pouze v schema.graphqls. Nepoužívejte extend type Query v doménově specifických souborech. DGS (a pod ním graphql-java) při startu selže s duplicitními definicemi typů, pokud to uděláte. Toto je častá past, na které se ztrácejí hodiny debugováním:

GRAPHQL
# schema.graphqls — JEDINÝ soubor s bloky Query/Mutation

type Query {
    # Product dotazy
    product(id: ID!): Product
    products(
        searchText: String,
        category: ProductCategory,
        pageNumber: Int!,
        pageSize: Int!,
        sortBy: String,
        sortOrder: SortOrder
    ): ProductPage

    # Order dotazy
    order(id: ID!): Order
    myOrders(pageNumber: Int!, pageSize: Int!): OrderPage

    # User dotazy
    me: User
}

type Mutation {
    createProduct(input: CreateProductInput!): Product
    updateProduct(input: UpdateProductInput!): Product
    placeOrder(input: PlaceOrderInput!): Order
}
GRAPHQL
# product_types.graphqls — POUZE definice typů, žádné Query/Mutation

type Product {
    id: ID!
    name: String!
    description: String
    price: Float!
    category: ProductCategory!
    createdAt: DateTime
    warehouse: Warehouse      # Resolved by a field resolver
    stockLevel: Int            # Resolved by a data loader
}

type ProductPage {
    items: [Product]
    totalPages: Int
    currentPage: Int
    totalElements: Int
}

input CreateProductInput {
    name: String!
    description: String
    price: Float!
    category: ProductCategory!
}

enum ProductCategory {
    ELECTRONICS
    CLOTHING
    FOOD
    BOOKS
}

Toto oddělení udržuje každý soubor zaměřený a usnadňuje hledání, kde je typ definován — typy jsou vždy v {doména}_types.graphqls, operace jsou vždy v schema.graphqls.

Custom scalars

Vestavěné scalars GraphQL (String, Int, Float, Boolean, ID) nepokryjí vše. DGS podporuje rozšířené scalars přímo z balíčku:

GRAPHQL
# common_types.graphqls
scalar Date       # java.time.LocalDate
scalar DateTime   # java.time.OffsetDateTime
scalar JSON       # java.util.Map
scalar Long       # java.lang.Long

Mapování mezi GraphQL scalars a Java typy se konfiguruje v build souboru (více v sekci o generování kódu níže).

Váš první query a mutation

DGS používá malou sadu anotací k propojení vašeho Java kódu se schématem. Pojďme vytvořit jednoduché produktové API.

Dotazy s @DgsQuery

Java
@DgsComponent
@RequiredArgsConstructor
public class ProductDataFetcher {

    private final ProductService productService;

    @DgsQuery
    public Mono<Product> product(@InputArgument String id) {
        return productService.findById(id);
    }

    @DgsQuery
    public Mono<ProductPage> products(
            @InputArgument String searchText,
            @InputArgument ProductCategory category,
            @InputArgument Integer pageNumber,
            @InputArgument Integer pageSize,
            @InputArgument String sortBy,
            @InputArgument SortOrder sortOrder) {

        int page = pageNumber != null ? pageNumber : 0;
        int size = pageSize != null ? pageSize : 10;

        return productService.search(searchText, category, page, size, sortBy, sortOrder);
    }
}

Pár poznámek:

  • @DgsComponent označuje třídu jako DGS data fetcher kontejner. Pod kapotou je to Springový @Component, takže dependency injection funguje normálně.
  • @DgsQuery mapuje metodu na pole typu Query. Název metody musí odpovídat názvu pole ve schématu (product se mapuje na Query.product).
  • @InputArgument váže GraphQL argumenty na parametry metody. DGS řeší konverzi typů — GraphQL Int se stane Java Integer, input objekty se stanou vygenerovanými Java typy.
  • Návratové typy mohou být reaktivní. Mono<T> i CompletableFuture<T> jsou podporovány. Fungují i prosté synchronní návraty.

Mutace s @DgsMutation

Java
@DgsComponent
@RequiredArgsConstructor
public class CreateProductMutation {

    private final ProductService productService;

    @DgsMutation
    @PreAuthorize("hasRole('ADMIN')")
    public Mono<Product> createProduct(@InputArgument CreateProductInput input) {
        return productService.create(input);
    }
}

Vzor je identický s dotazy — jediný rozdíl je v anotaci. Všimněte si, že @PreAuthorize ze Spring Security funguje přímo na DGS metodách. To je jedna z výhod integrace se Spring for GraphQL: bezpečnostní infrastruktura Springu je plně k dispozici.

Oddělení dotazů a mutací

Nemusíte dávat všechny operace do jedné třídy. Ve skutečnosti jejich rozdělení do zaměřených tříd usnadňuje navigaci v kódu:

Text
com.example.product.graphql/
├── ProductDataFetcher.java       # @DgsQuery metody
├── CreateProductMutation.java    # @DgsMutation pro vytvoření
├── UpdateProductMutation.java    # @DgsMutation pro aktualizaci
└── ProductStockResolver.java     # @DgsData field resolver

Každá třída je @DgsComponent s jedinou zodpovědností. Ve větším měřítku to znamená, že jakoukoli operaci najdete podle domény a typu akce, aniž byste scrollovali přes 500řádkovou god class.

Generování kódu

Jednou z nejpraktičtějších vlastností DGS je jeho plugin pro generování kódu. Čte vaše .graphqls soubory a generuje:

  • Java typy pro každý GraphQL typ, input a enum
  • Client query třídy pro typově bezpečné klientské operace
  • Interfaces (volitelně) pro polymorfní typy

Konfigurace v Gradlu

KOTLIN
plugins {
    id("com.netflix.dgs.codegen") version "8.3.0"
}

tasks.withType<com.netflix.graphql.dgs.codegen.gradle.GenerateJavaTask> {
    generateClient = true
    packageName = "com.example.generated"
    typeMapping = mutableMapOf(
        "Date" to "java.time.LocalDate",
        "DateTime" to "java.time.OffsetDateTime",
        "JSON" to "java.util.Map",
        "Long" to "java.lang.Long"
    )
    generateInterfaces = true
    generateDataTypes = true
    schemaPaths = mutableListOf("${projectDir}/src/main/resources/schema/")
}

Klíčová nastavení:

  • typeMapping — mapuje custom scalars na Java typy. Bez toho by se pole DateTime vygenerovalo jako Object.
  • generateClient = true — generuje typově bezpečné query builder třídy (užitečné pro testování a komunikaci mezi službami).
  • generateInterfaces = true — generuje interfaces pro typy, což pomáhá s mapovacími vzory.
  • schemaPaths — ukazuje na adresář se schématem. Plugin čte všechny .graphqls soubory rekurzivně.

Po spuštění ./gradlew generateJava najdete vygenerované typy v build/generated/sources/dgs-codegen/:

Text
build/generated/sources/dgs-codegen/
└── com/example/generated/
    ├── types/
    │   ├── Product.java           # Plný typ s builderem
    │   ├── ProductPage.java       # Paginační wrapper
    │   ├── CreateProductInput.java # Input typ
    │   ├── ProductCategory.java   # Enum
    │   └── SortOrder.java         # Sdílený enum
    └── client/
        ├── ProductGraphQLQuery.java    # Typově bezpečný query builder
        └── ProductProjectionRoot.java  # Builder pro výběr polí

Tyto vygenerované typy jsou to, co vaše DGS data fetchery vracejí. Nikdy ručně nevytváříte DTO, která zrcadlí schéma — codegen to udělá za vás.

Třívrstvý typový systém

V produkční aplikaci máte typicky tři vrstvy typů a je důležité je udržovat oddělené:

graph LR A["GraphQL Schema
product_types.graphqls"] -->|codegen| B["Generated Types
codegen Product.java"] B ---|"ACL"| ACL1["Mapper"] ACL1 --- C["Domain Model
Product record"] C ---|"ACL"| ACL2["Mapper"] ACL2 --- D["Database Entities
ProductNode.java"] style A fill:#4a9eff,stroke:#2171c7,color:#fff style B fill:#7c4dff,stroke:#5e35b1,color:#fff style ACL1 fill:#ffd54f,stroke:#f9a825,color:#333 style C fill:#00bfa5,stroke:#00897b,color:#fff style ACL2 fill:#ffd54f,stroke:#f9a825,color:#333 style D fill:#ff7043,stroke:#e64a19,color:#fff
  1. Vygenerované typy — to, co DGS vrací klientům. Automaticky generované, nikdy ručně needitované.
  2. Doménové typy — váš business model. Prosté Java records, nezávislé na frameworku. Předáváte je mezi službami.
  3. Databázové entity — anotované pro vaši persistence vrstvu (JPA, R2DBC apod.).

Mappery jako anti-corruption vrstvy

Mappery mezi těmito vrstvami nejsou pouhý boilerplate — implementují vzor Anti-Corruption Layer (ACL) z Domain-Driven Designu. Myšlenka je přímočará: když se dva modely potkají (vaše doména a vnější záležitost jako databázové schéma nebo veřejný API kontrakt), vložíte mezi ně překladovou vrstvu, aby změny v jednom modelu nenarušily ten druhý.

V DGS aplikaci máte dvě ACL hranice:

ACL 1: Doména <-> GraphQL (hranice API)

GraphQL schéma je veřejný kontrakt. Klienti závisí na názvech polí, typech a struktuře. Váš doménový model by naopak měl odrážet business pravidla a volně se vyvíjet. Mapper mezi vygenerovanými typy a doménovými typy je ACL, která je odděluje:

Java
@Mapper(componentModel = "spring")
public interface ProductMapper {

    // Domain → GraphQL (pro odpovědi)
    com.example.generated.types.Product toGraphQL(com.example.domain.Product product);

    // GraphQL input → Domain (pro mutace)
    com.example.domain.Product toDomain(CreateProductInput input);
}

Bez tohoto ACL přejmenování doménového pole znamená přejmenování GraphQL pole — což je breaking change API. S mapperem přejmenujete doménové pole a aktualizujete mapování. API kontrakt zůstane stabilní.

ACL 2: Doména <-> Databáze (hranice persistence)

Vaše databázové schéma má své vlastní záležitosti: konvence pojmenování sloupců, denormalizace pro výkon dotazů, audit sloupce (created_at, updated_by), soft-delete příznaky. Nic z toho nepatří do vašeho doménového modelu. Persistence mapper tyto rozdíly absorbuje:

Java
@Mapper(componentModel = "spring")
public interface ProductPersistenceMapper {

    // Databázová entita → Doména
    com.example.domain.Product toDomain(ProductEntity entity);

    // Doména → Databázová entita
    ProductEntity toEntity(com.example.domain.Product product);
}

To znamená, že můžete restrukturalizovat databázi (rozdělit tabulku, přidat cachovací vrstvu, migrovat z SQL na document store) bez toho, abyste se dotkli jediného řádku business logiky.

Proč nepřeskočit doménovou vrstvu?

Týmy někdy zkracují cestu mapováním přímo z databázových entit na GraphQL typy — dvě vrstvy místo tří. U malých API to funguje, ale vytváří to skrytou vazbu: váš veřejný API kontrakt je nyní svázaný s databázovým schématem. Každé přejmenování sloupce, každá restrukturalizace tabulky, každá optimalizace persistence riskuje rozbití API.

Doménová vrstva uprostřed je stabilní jádro. Neví o GraphQL anotacích ani databázových sloupcích. Je to místo, kde žijí vaše business pravidla, kde unit testy ověřují chování a kde operují vaše event-sourced agregáty (pokud je používáte). Dvě ACL na obou stranách ji chrání před infrastrukturními změnami.

Text
Změny v DB  →  ACL absorbuje  →  Doména beze změn  →  ACL absorbuje  →  API beze změn

V praxi jsou náklady na údržbu dvou mapper interfaces nízké — obzvlášť když MapStruct generuje implementaci v compile time. Náklady na jejich absenci se projeví poprvé, když potřebujete vyvíjet databázi nebo API nezávisle.

Jak to celé funguje dohromady

Zde je kompletní flow pro dotaz product(id: "123"):

sequenceDiagram participant Client participant Spring as Spring for GraphQL participant DGS participant Fetcher as ProductDataFetcher participant Service as ProductService participant DB as Database Client->>Spring: GraphQL query Spring->>DGS: Route to resolver DGS->>Fetcher: product(id: "123") Note over DGS,Fetcher: @InputArgument binds "123" Fetcher->>Service: findById("123") Service->>DB: SELECT * FROM products DB-->>Service: ProductEntity Service-->>Fetcher: Domain Product Note over Fetcher: Mapper → Generated Type Fetcher-->>DGS: Generated Product DGS-->>Spring: Serialized JSON Spring-->>Client: HTTP Response

V každém kroku je vynucena typová bezpečnost — schéma definuje kontrakt, codegen vytváří typy a kompilátor zachytí nesoulady.

Co bude dál

Ve 2. dílu se pustíme do nejvlivnější DGS funkce pro produkční výkon: data loadery a field resolvery. Naučíte se, jak předcházet problémům N+1 dotazů a udržet vaše GraphQL API rychlé, jak schéma roste.


Titulní foto: Jakub Zerdzicki na Unsplash.

Další z blogu