0Pricing
GraphQL APIs with Spring Boot · 课时

实现批处理与缓存

将 DataLoaders 集成到 Spring Boot 解析器中,优化数据获取并减少数据库往返次数。

实现批处理与缓存 是 CoddyKit 上的免费 GraphQL APIs with Spring Boot 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 GraphQL APIs with Spring Boot 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 GraphQL APIs with Spring Boot 课程共包含 4 节课。

本课时的部分内容尚未翻译,以英文显示。

DataLoaders: Time to Implement!

Welcome back! In previous lessons, we learned about the N+1 problem and how DataLoaders provide an elegant solution through batching and caching.

Today, we'll get hands-on and integrate DataLoaders into a Spring Boot GraphQL application. We'll see how to define a BatchLoader, register it, and use it in our GraphQL resolvers.

Simulating a Data Service

First, let's set up a simple mock service that simulates fetching users from a database. This service will be called by our DataLoaders.

Notice the System.out.println, which will help us observe when the actual 'database' call happens, demonstrating batching later.

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

// A simple User data class
class User {
  String id;
  String name;
  User(String id, String name) { 
    this.id = id; this.name = name; 
  }
  public String getId() { return id; }
  public String getName() { return name; }
}

// Mock service to fetch users (simulates DB call)
class MockUserService {
  private final Map<String, User> users = Map.of(
    "1", new User("1", "Alice"),
    "2", new User("2", "Bob"),
    "3", new User("3", "Charlie"),
    "4", new User("4", "David")
  );

  public List<User> findAllByIds(List<String> ids) {
    System.out.println("DB Call: Fetching users for IDs: " + ids);
    return ids.stream()
              .map(users::get)
              .filter(user -> user != null)
              .collect(Collectors.toList());
  }
}

public class Main {
  public static void main(String[] args) {
    MockUserService service = new MockUserService();
    List<User> foundUsers = service.findAllByIds(List.of("1", "3"));
    System.out.println("Found: " + foundUsers.size() + " users.");
  }
}

The BatchLoader Interface

The core of DataLoader's batching mechanism is the BatchLoader interface. It defines a single method: load(List<K> keys).

  • It takes a list of keys (e.g., user IDs).
  • It returns a CompletionStage (a future-like object) of a list of values (e.g., users).
  • Crucially, this method is called once for all keys requested in a short window, allowing you to fetch them in a single optimized operation (like a single database query).

Implementing Your BatchLoader

Now, let's create our UserBatchLoader. It will implement the BatchLoader<String, User> interface, meaning it takes a String (user ID) as a key and returns a User object.

It uses our MockUserService to perform the actual batch fetch.

import org.dataloader.BatchLoader;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;

// (Assume User and MockUserService are defined elsewhere)
// For runnable example, they would be in the same file.

public class UserBatchLoader implements BatchLoader<String, User> {
  private final MockUserService userService;

  public UserBatchLoader(MockUserService userService) {
    this.userService = userService;
  }

  @Override
  public CompletionStage<List<User>> load(List<String> userIds) {
    // This method is called once with all requested IDs.
    // We delegate to our mock service to fetch them in one go.
    return CompletableFuture.supplyAsync(
      () -> userService.findAllByIds(userIds)
    );
  }
}

public class Main {
  public static void main(String[] args) {
    MockUserService userService = new MockUserService();
    UserBatchLoader batchLoader = new UserBatchLoader(userService);
    System.out.println("UserBatchLoader created!");
  }
}

The DataLoaderRegistry

In a Spring Boot GraphQL application, DataLoader instances are managed within a DataLoaderRegistry. This registry acts as a container for all DataLoaders available during a GraphQL request execution.

  • Each DataLoader is registered with a unique name (e.g., "userDataLoader").
  • Resolvers retrieve the necessary DataLoader from this registry using its name.
  • Spring for GraphQL typically handles the lifecycle and scope of this registry per request.

Setting up DataLoaders

Here's how you create a DataLoader instance from your BatchLoader and register it in a DataLoaderRegistry. This setup is crucial for making DataLoaders accessible to your resolvers.

import org.dataloader.DataLoader;
import org.dataloader.DataLoaderRegistry;

// (Assume User, MockUserService, UserBatchLoader are defined)

public class Main {
  public static void main(String[] args) {
    MockUserService userService = new MockUserService();
    UserBatchLoader userBatchLoader = new UserBatchLoader(userService);

    // Create a DataLoader instance
    DataLoader<String, User> userDataLoader =
      DataLoader.newDataLoader(userBatchLoader);

    // Register it in a DataLoaderRegistry
    DataLoaderRegistry registry = new DataLoaderRegistry();
    registry.register("userDataLoader", userDataLoader);

    System.out.println("DataLoader registered as 'userDataLoader'!");
    // In a real app, this registry would be part of the GraphQLContext.
    // Resolvers would then retrieve DataLoaders from it.
  }
}

Using DataLoader in Resolvers

Once your DataLoader is registered, you can use it within your GraphQL resolvers. Instead of directly calling your service, you'll call dataLoader.load(id).

  • The load() method returns a CompletionStage, not the direct object.
  • The DataLoader collects all load() calls within the current execution frame before dispatching them to the BatchLoader.
  • Spring for GraphQL automatically dispatches the DataLoaders at the end of a data fetching cycle.

Resolver Integration in Action

This example simulates a GraphQL query where multiple user IDs are requested. Watch the "DB Call" message in the output to see how DataLoaders batch these requests into a single operation, even if they appear separate in the resolver logic.

import org.dataloader.DataLoader;
import org.dataloader.DataLoaderRegistry;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.stream.Collectors;

// User, MockUserService, UserBatchLoader definitions (from previous scenes)
class User {
  String id; String name;
  User(String id, String name) { this.id = id; this.name = name; }
  public String getId() { return id; }
  public String getName() { return name; }
}

class MockUserService {
  private final Map<String, User> users = Map.of(
    "1", new User("1", "Alice"), "2", new User("2", "Bob"),
    "3", new User("3", "Charlie"), "4", new User("4", "David")
  );
  public List<User> findAllByIds(List<String> ids) {
    System.out.println("DB Call: Fetching users for IDs: " + ids);
    return ids.stream().map(users::get)
              .filter(user -> user != null).collect(Collectors.toList());
  }
}

class UserBatchLoader implements BatchLoader<String, User> {
  private final MockUserService userService;
  public UserBatchLoader(MockUserService userService) { this.userService = userService; }
  @Override
  public CompletionStage<List<User>> load(List<String> userIds) {
    return CompletableFuture.supplyAsync(() -> userService.findAllByIds(userIds));
  }
}

// Simulate a GraphQL Query Resolver method
public class UserResolver {
  private final DataLoaderRegistry dataLoaderRegistry;

  public UserResolver(DataLoaderRegistry registry) {
    this.dataLoaderRegistry = registry;
  }

  public CompletionStage<User> getUserById(String id) {
    DataLoader<String, User> userDataLoader =
      dataLoaderRegistry.getDataLoader("userDataLoader");
    return userDataLoader.load(id);
  }

  public static void main(String[] args) {
    MockUserService userService = new MockUserService();
    UserBatchLoader userBatchLoader = new UserBatchLoader(userService);
    DataLoader<String, User> userDataLoader =
      DataLoader.newDataLoader(userBatchLoader);

    DataLoaderRegistry registry = new DataLoaderRegistry();
    registry.register("userDataLoader", userDataLoader);

    UserResolver resolver = new UserResolver(registry);

    System.out.println("Requesting User 1 and User 2 concurrently...");

    // Simulate two concurrent GraphQL field fetches
    CompletableFuture<User> user1Future =
      (CompletableFuture<User>) resolver.getUserById("1");
    CompletableFuture<User> user2Future =
      (CompletableFuture<User>) resolver.getUserById("2");

    CompletableFuture.allOf(user1Future, user2Future)
      .thenRun(() -> {
        try {
          System.out.println("Fetched User 1: " + user1Future.join().getName());
          System.out.println("Fetched User 2: " + user2Future.join().getName());
        } catch (Exception e) {
          e.printStackTrace();
        }
        // For this manual demo, we dispatch the DataLoader.
        // In Spring GraphQL, this is handled automatically.
        userDataLoader.dispatch();
      }).join();
  }
}

DataLoader's Built-in Caching

Beyond batching, DataLoaders also provide request-scoped caching out-of-the-box. This means:

  • If dataLoader.load("1") is called multiple times within the same GraphQL request, the BatchLoader's fetch function for ID "1" will only be invoked once.
  • Subsequent calls for the same ID within that request will return the already fetched (cached) result.
  • This prevents redundant database calls for the same data within a single GraphQL operation, further boosting performance.

Check Your Understanding

You've learned how to implement DataLoaders for batching and caching. Let's test your knowledge!

Recap: Batching & Caching

Great job! You've successfully learned how to implement DataLoaders in Spring Boot GraphQL.

  • We created a MockUserService to simulate data fetching.
  • We built a UserBatchLoader to handle batching multiple ID requests.
  • We saw how to initialize and register DataLoader instances in a DataLoaderRegistry.
  • Finally, we integrated DataLoaders into a resolver, observing how requests are batched and how caching prevents redundant calls.

By using DataLoaders, you significantly optimize your GraphQL API's performance, especially when dealing with complex data graphs and nested objects.

常见问题解答

「实现批处理与缓存」课时是免费的吗?

是的 — 「实现批处理与缓存」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 GraphQL APIs with Spring Boot 课程的其余内容,请升级到 CoddyKit PRO。 GraphQL APIs with Spring Boot 课程共包含 4 节课。

「实现批处理与缓存」这节课中我会学到什么?

将 DataLoaders 集成到 Spring Boot 解析器中,优化数据获取并减少数据库往返次数。 你通过在浏览器中直接运行的动手代码来练习 GraphQL APIs with Spring Boot,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 GraphQL APIs with Spring Boot 需要有经验吗?

无需任何先前经验。CoddyKit 上的 GraphQL APIs with Spring Boot 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「实现批处理与缓存」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 GraphQL APIs with Spring Boot 课中编写并运行代码吗?

能。每节 GraphQL APIs with Spring Boot 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. N+1 问题详解
  2. 认识 GraphQL DataLoaders
  3. 实现批处理与缓存
  4. 结合 Spring 上下文与异步处理使用 DataLoaders
← 返回 GraphQL APIs with Spring Boot