Spring Boot GraphQL N+1 Problem Solution

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=true

pom.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 Postmanin the query section add the below query

query Apples { 
    apples {
        appleId
        appleName
        taste
        available
        vendors {
            vendorId
            appleId
            firstName
            lastName
            address
        }
    }
}

follow us on