[Study Notes] GraphQL With Spring WebFlux — Basics

GraphQL Basics

Jay Kim
2 min readAug 9, 2023

Scalar Types

  • Int
  • Float
  • String
  • Boolean
  • ID (Unique value serialized as String)

Non-nullable fields are marked with !.

Collection Types

  • [Int]: List<Integer>

Special Types

  • Query: Get
  • Mutation: Post, Put, Delete, Patch
  • Subscription: SSE, WebSockets

Enum

Enums don’t always have to be mapped to Java Enum. They could be mapped to Java Strings as well.

enum Brand {
KIA
HYUNDAI
}

Nested Objects Schema Design

type Customer {
id: ID!
name: String
age: Int
city: String
orders: [Order]
}

type Order {
id: ID!
description: String
}

Input Types

Used to when passing in a complex objects as arguments.

input AgeRangeFilter {
from: Int!
to: Int!
}

Data Resolver

Overriding a field is possible by creating a separate @SchemaMapping with method name matching the name of the field.

@Controller
public class GraphQLController {

private final CustomerService customerService;
private final OrderService orderService;

public Part04Controller(CustomerService customerService, OrderService orderService) {
this.customerService = customerService;
this.orderService = orderService;
}

@QueryMapping
public Flux<Customer> customers() {
return customerService.customers();
}

// override age field in type Customer
@SchemaMapping(typeName = "Customer")
public Mono<Integer> age() {
return Mono.just(99);
}
}

N + 1 Issues

Use @BatchMapping.

The number of items and the order of items must be the same between the input and the output. Use flatMapSequential() over flatMap() to return Flux<List<V>>.

@Controller
public class GraphQLController {

private final CustomerService customerService;
private final OrderService orderService;

public Part04Controller(CustomerService customerService, OrderService orderService) {
this.customerService = customerService;
this.orderService = orderService;
}

@QueryMapping
public Flux<Customer> customers() {
return customerService.customers();
}

@BatchMapping(typeName = "Customer")
public Flux<List<Order>> orders(List<Customer> customers) {
System.out.println("orders invoked");
var ids = customers.stream()
.map(Customer::id)
.toList();
return orderService.ordersByIds(ids);
}
}
@Service
public class OrderService {
private final Map<Integer, List<Order>> map = Map.of(
// ... data
);

public Flux<List<Order>> ordersByIds(List<Integer> ids) {
return Flux.fromIterable(ids)
.flatMapSequential(id -> fetchOrders(id)
.defaultIfEmpty(emptyList()));
}

private Mono<List<Order>> fetchOrders(Integer id) {
return Mono.justOrEmpty(map.get(id))
.delayElement(Duration.ofMillis(ThreadLocalRandom.current().nextInt(0, 500)));
}
}

Or just return as a map like Map<Mono<K, List<V>>>.

@Controller
public class GraphQLController {

private final CustomerService customerService;
private final OrderService orderService;

public Part04Controller(CustomerService customerService, OrderService orderService) {
this.customerService = customerService;
this.orderService = orderService;
}

@QueryMapping
public Flux<Customer> customers() {
return customerService.customers();
}

@BatchMapping(typeName = "Customer")
public Mono<Map<Customer, List<Order>>> orders(List<Customer> customers) {
System.out.println("orders invoked");
return orderService.ordersByIdsAsMap(customers);
}
}
@Service
public class OrderService {
private final Map<Integer, List<Order>> map = Map.of(
// ... data
);

public Mono<Map<Customer, List<Order>>> ordersByIdsAsMap(List<Customer> customers) {
return fetchOrdersAsMap(customers);
}

private Mono<Map<Customer, List<Order>>> fetchOrdersAsMap(List<Customer> customers) {
return Flux.fromIterable(customers)
.map(c -> Tuples.of(c, map.getOrDefault(c.id(), emptyList())))
.collectMap(Tuple2::getT1, Tuple2::getT2);
}
}

Field Alias

Example of changing the representation of amount and accountType to balance and type.

{
customers {
name
age
address {
street
city
}
account {
id
balance: amount
type: accountType
}
}
}

Field alias can be used when there is a fields conflict.

{
c1: customersById(id: 1) {
... CustomerDetails
}
c2: customersById(id: 2) {
... CustomerDetails
}
}

fragment CustomerDetails on Customer {
id
name
age
city
}

--

--