[Study Notes] GraphQL With Spring WebFlux — Basics
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
}