The N+1 problem makes multiple API calls to load the data which makes the application very inefficient. The solution for the problem is to call the Apple API once and load all the Vedors in another API. This will reduce the N+1 API call to only 2.
Spring Boot GraphQL provides the solution to this problem by @BatchMapping. it will load all the corresponding Vedors in a batch that will reduce the API call.
Let's see it with an example.
package org.wesome.graphql.controllers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.graphql.data.method.annotation.BatchMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
import org.wesome.graphql.entity.Apple;
import org.wesome.graphql.entity.Vendor;
import org.wesome.graphql.service.AppleService;
import org.wesome.graphql.service.VendorService;
import java.util.List;
import java.util.Map;
@Controller
public class AppleControllerGraphQl {
@Autowired
private AppleService appleService;
@Autowired
private VendorService vendorService;
@QueryMapping("apples")
public List<Apple> apples() {
System.out.println("AppleControllerGraphQl.apples");
return appleService.getAll();
}
@BatchMapping
public Map<Apple, List<Vendor>> vendors(List<Apple> apple) {
System.out.println("AppleControllerGraphQl.vendors");
return this.vendorService.getAllAppleVendor(apple);
}
}
package org.wesome.graphql.data;
public enum AppleName {
MACINTOSH("Macintosh"),
FUJI("Fuji"),
GALA("Gala"),
JONAGOLD("Jonagold");
private String appleName;
AppleName(String appleName) {
this.appleName = appleName;
}
}
package org.wesome.graphql.data;
public enum AppleTaste {
SWEET("sweet"), TANGY("tangy"), BITTER("bitter"), SOUR("sour");
private final String appleTaste;
AppleTaste(String appleTaste) {
this.appleTaste = appleTaste;
}
}
package org.wesome.graphql.entity;
public record Apple(Integer appleId, String appleName, String taste, Boolean available) {
}
package org.wesome.graphql.entity;
public record Vendor(Integer vendorId, Integer appleId, String firstName, String lastName, String address) {
}
package org.wesome.graphql.service;
import org.wesome.graphql.entity.Apple;
import java.util.List;
public interface AppleService {
List<Apple> getAll();
}
package org.wesome.graphql.service;
import org.springframework.stereotype.Service;
import org.wesome.graphql.entity.Apple;
import java.util.List;
@Service
public class AppleServiceImpl implements AppleService {
public static List<Apple> appleList;
@Override
public List<Apple> getAll() {
return appleList;
}
}
package org.wesome.graphql.service;
import org.wesome.graphql.entity.Apple;
import org.wesome.graphql.entity.Vendor;
import java.util.List;
import java.util.Map;
public interface VendorService {
//get single vendor
List<Vendor> getOneAppleVendor(Apple apple);
Map<Apple, List<Vendor>> getAllAppleVendor(List<Apple> apple);
}
package org.wesome.graphql.service;
import org.springframework.stereotype.Service;
import org.wesome.graphql.entity.Apple;
import org.wesome.graphql.entity.Vendor;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
public class VendorServiceImpl implements VendorService {
public static Map<Integer, List<Vendor>> vendorsList = new HashMap<>();
@Override
public Map<Apple, List<Vendor>> getAllAppleVendor(List<Apple> apple) {
Map<Apple, List<Vendor>> appleListMap = apple.stream().collect(Collectors.toMap(app -> app, app -> vendorsList.get(app.appleId()), (a, b) -> b));
return appleListMap;
}
@Override
public List<Vendor> getOneAppleVendor(Apple apple) {
return vendorsList.get(apple.appleId());
}
}
package org.wesome.graphql;
import com.github.javafaker.Faker;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.wesome.graphql.data.AppleName;
import org.wesome.graphql.data.AppleTaste;
import org.wesome.graphql.entity.Apple;
import org.wesome.graphql.entity.Vendor;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static org.wesome.graphql.service.AppleServiceImpl.appleList;
import static org.wesome.graphql.service.VendorServiceImpl.vendorsList;
@SpringBootApplication
public class GraphqlProjectApplication implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(GraphqlProjectApplication.class, args);
}
@Override
public void run(String... args) {
Faker faker = new Faker();
appleList = IntStream.rangeClosed(0, 3).mapToObj(app -> {
var apple = new Apple(app, AppleName.values()[app].name(), AppleTaste.values()[app].name(), faker.random().nextBoolean());
vendorsList.put(app, IntStream.rangeClosed(0, 1).mapToObj(ven -> new Vendor(ven, app, faker.name().firstName(), faker.name().lastName(), faker.address().fullAddress())).collect(Collectors.toList()));
return apple;
}).collect(Collectors.toList());
}
}
# apple entity
type Apple{
# primary key of apple
appleId:ID!
appleName:String
taste:String
available:Boolean
vendors:[Vendor]
}
# apple entity
type Vendor{
# primary key of Vendor
vendorId:ID!
appleId:Int
firstName:String
lastName:String
address:String
}
type Query{
# query to get all apples
apples:[Apple]
}
\src\main\resources\application.properties
spring.graphql.graphiql.enabled=truepom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version>
<relativePath/>
</parent>
<groupId>org.wesome</groupId>
<artifactId>spring-boot-graphql</artifactId>
<version>0.0.1-snapshot</version>
<name>spring-boot-graphql</name>
<description>implementing graphql in spring boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.github.javafaker</groupId>
<artifactId>javafaker</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-graphql</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.graphql</groupId>
<artifactId>spring-graphql-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>Rest API
curl --location --request POST 'http://localhost:8080/graphql' \
--header 'Content-Type: application/json' \
--data '{"query":"query Apples { \r\n apples {\r\n appleId\r\n appleName\r\n taste\r\n available\r\n vendors {\r\n vendorId\r\n appleId\r\n firstName\r\n lastName\r\n address\r\n }\r\n }\r\n}\r\n","variables":{}}'GraphiQL
GraphQL provides an inbuild UserInterface GraphiQL, which can be accessed via http://localhost:8080/graphiql or access GraphQL via Postman, in the query section add the below query
query Apples {
apples {
appleId
appleName
taste
available
vendors {
vendorId
appleId
firstName
lastName
address
}
}
}