Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Spring GraphQL @BatchMapping: collapse N+1 lookups without changing field behavior

Last updated: 1 Oct 20264 min read
tutorial
IntermediateBy AITrove Editorial

A GraphQL field resolver called once per parent can turn one page request into N database queries; @BatchMapping groups those parent keys for one fetch.

Count database calls

A query returns 47 parcel records and asks for each parcel's depot. A naïve @SchemaMapping loads one depot for every parcel, making 48 database calls instead of a bounded batch. @BatchMapping receives the parents as a list and can fetch their depot IDs in one query, returning a map from each original parent to its depot. Page bounds still matter: batching 47 rows is manageable, batching 47,000 can exhaust memory or exceed a database parameter limit.

Preserve missing-value semantics

The batch map may omit a parent whose depot was removed. Decide whether the schema field is nullable and whether omission becomes null or a field error. Non-null fields can cause error propagation into the parent response. Use stable equality for parent keys if they are map keys; a mutable entity whose hash changes while loading can lose its result.

Verify the query shape

Request 47 parcels with depots, assert the result IDs match their parents, and measure query count. Repeat with a missing depot and a page boundary. The method below is a mapping sketch; the repository method must include tenant scope and a bounded IN query before it is safe for production.

Implementation sketch

Java
@BatchMapping(typeName = "Parcel", field = "depot")
Map<Parcel, Depot> depot(List<Parcel> parcels, Principal principal) {
    UUID tenantId = tenantResolver.requireTenant(principal);
    Set<Long> depotIds = parcels.stream()
        .map(Parcel::depotId).collect(Collectors.toSet());
    Map<Long, Depot> depots = depotRepository
        .findByTenantIdAndIdIn(tenantId, depotIds).stream()
        .collect(Collectors.toMap(Depot::id, Function.identity()));
    return parcels.stream().filter(parcel -> depots.containsKey(parcel.depotId()))
        .collect(Collectors.toMap(Function.identity(),
            parcel -> depots.get(parcel.depotId())));
}

Cost and verification

One batched lookup lowers round trips but can create a large IN query and a map proportional to page size. Bound the parent list and measure SQL plan changes.

Common Mistakes

  • Do not claim N+1 is fixed if the repository still loops per ID.
  • Do not use mutable parent keys without stable equals and hashCode.
  • Do not ignore how a missing related row interacts with schema nullability.

Read next

Spring GraphQL batch loader tenant scope: never key a shared cache by row ID alone, Spring GraphQL list bounds: reject large arguments before reading rows, Spring GraphQL schema nullability: trace one field failure through the response, Spring JPA fetch plans: measure N+1 queries before changing mappings.

spring
spring-web
graphql-batch-mapping-n-plus-one
Storage details