Odyssey

Data loaders with Java & DGS
beta

Course overview and setupThe n+1 problemData loaders under the hoodRegistering a data loaderUsing a data loader
5. Using a data loader
4m

Overview

Our data loader is ready to batch together our amenity IDs and handle the request to the REST endpoint. It's considered officially registered in our application, but not yet being used anywhere.

In this lesson, we will:

  • Update our datafetcher to delegate responsibility to the data loader

Using our data loader

We've taken care of the first two steps in our plan to bring data loaders into our application.

  1. We created a data loader class.
  2. We gave our class a load method.

Now we'll update our datafetcher method for the Listing.amenities field. Rather than calling our ListingService methods directly, it will delegate responsibility to the data loader to gather up, and provide amenity data for, all of the listings in our query.

Step 3 - Updating our datafetcher

Back in datafetchers/ListingDataFetcher, we have a few changes to make.

Let's start with some additional imports we'll need.

datafetchers/ListingDataFetcher
import org.dataloader.DataLoader;
import java.util.concurrent.CompletableFuture;

Down in our method, let's remove the line that calls out to the ListingService's amenitiesRequest.

@DgsData(parentType = "Listing")
public List<Amenity> amenities(DgsDataFetchingEnvironment dfe) throws IOException {
ListingModel listing = dfe.getSource();
String id = listing.getId();
Map<String, Boolean> localContext = dfe.getLocalContext();
if (localContext.get("hasAmenityData")) {
return listing.getAmenities();
}
- return listingService.amenitiesRequest(id);
}

Now, we'll update our datafetcher to call the data loader instead. We can access any registered data loader (any class that has the @DgsDataLoader annotation applied) in our application by calling the dfe.getDataLoader() method, and providing a name.

if (localContext.get("hasAmenityData")) {
return listing.getAmenities();
}
dfe.getDataLoader("amenities");

The DgsDataFetchingEnvironment is an optional parameter that is passed to each of our application's datafetcher methods. It contains a lot of information about the query being executed, the server's context, as well as the value returned by the previous datafetcher in the chain.

Learn more about this optional parameter in our lesson on resolver chains.

This returns a DataLoader type, with two type variables: the first describes the type of data for the identifiers that are passed into the data loader. The second describes the type of data that is returned for each identifier. We're passing in our listing IDs of type String, and for each identifier, we expect to get a List of Amenity types back.

DataLoader<String, List<Amenity>> amenityDataLoader = dfe.getDataLoader("amenities");

Now we can call the load method of our amenityDataLoader, passing in the id of the listing we're resolving amenity data for.

DataLoader<String, List<Amenity>> amenityDataLoader = dfe.getDataLoader("amenities");
return amenityDataLoader.load(id);

Pretty quickly we'll see a red squiggly on the new line that we've added. Our IDE is complaining because our method's return type is List<Amenity>, but our data loader's load method returns a type of CompletableFuture<List<Amenity>>.

We can't just update the method's return type to CompletableFuture<List<Amenity>>, because our method has the possibility of returning a List<Amenity> type if that data is already available. To mitigate this mismatch, we'll instead update our method's return type to Object.

@DgsData(parentType = "Listing") {1}
public Object amenities(DgsDataFetchingEnvironment dfe) throws IOException {
ListingModel listing = dfe.getSource();
String id = listing.getId();
Map<String, Boolean> localContext = dfe.getLocalContext();
if (localContext.get("hasAmenityData")) {
return listing.getAmenities();
}
DataLoader<String, List<Amenity>> amenityDataLoader = dfe.getDataLoader("amenities");
return amenityDataLoader.load(id);
}

Our method has two possible types it can return, but Java doesn't allow us to put more than one. For this reason, we have to abstract both possibilities down to a generic Object.

Prior to graphql-java v22, all methods were automatically wrapped in CompletableFuture, so we would have avoided this type mismatch: all of our method's possible return types would be the same! However, with the default CompletableFuture removed for performance reasons, we need to find a common return type we can use for our method. Object, in this case, fits.

Stepping back, we can see the new flow of this Listing.amenities datafetcher. We still have two paths:

  • If we already have access to amenity data (localContext.get("hasAmenityData")), we can simply return the amenities property on our Listing instance.
  • Otherwise, we need to make a follow-up request for amenity data, our datafetcher will pass the listing id to the data loader to be batched together in one big request for the amenities of all the listings in our query.

Before, our datafetcher called our data source in a new request for every listingId it received. Now, it passes each listing's id through to our data loader class, which does the work of batching all of the IDs it receives together in a single request.

This lets the datafetcher continue to fulfill its regular responsibilities—namely, being called for each instance of Listing.amenities it's meant to help resolve—without bogging down performance with multiple network calls. When a call across the network becomes necessary, it passes each ID into the data loader, and DGS takes care of the rest!

Running a query

We've made a lot of changes, so let's stop our running server and relaunch it.

Press the play button, or run the following command.

./gradlew bootRun

Let's jump back into Sandbox and make sure that our running server is connected.

https://studio.apollographql.com/sandbox/explorer

A screenshot of the Apollo Sandbox Explorer, highlighting the connection input with the locally running server's address

We'll run the same query as before, keeping our eyes on the application terminal so we can monitor the number of requests being made through the messages we logged.

A query for featured listings and their amenities
query GetFeaturedListingsAmenities {
featuredListings {
id
title
amenities {
id
name
category
}
}
}

When we run the query, we should see a list of listings and their amenities returned.

{
"data": {
"featuredListings": [
{
"id": "listing-1",
"title": "Cave campsite in snowy MoundiiX",
"amenities": [
{
"id": "am-2",
"name": "Towel",
"category": "Accommodation Details"
},
{
"id": "am-10",
"name": "Oxygen",
"category": "Space Survival"
},
{
"id": "am-11",
"name": "Prepackaged meals",
"category": "Space Survival"
},
{
"id": "am-12",
"name": "SOS button",
"category": "Space Survival"
},
{
"id": "am-13",
"name": "Meteor shower shield",
"category": "Space Survival"
},
{
"id": "am-26",
"name": "Meteor showers",
"category": "Outdoors"
},
{
"id": "am-27",
"name": "Wildlife",
"category": "Outdoors"
},
{
"id": "am-16",
"name": "Panic button",
"category": "Space Survival"
},
{
"id": "am-15",
"name": "Water recycler",
"category": "Space Survival"
},
{
"id": "am-14",
"name": "First-aid kit",
"category": "Space Survival"
},
{
"id": "am-17",
"name": "Emergency life support systems",
"category": "Space Survival"
},
{
"id": "am-18",
"name": "Universal translator",
"category": "Space Survival"
},
{
"id": "am-31",
"name": "Aquatic breathing aid",
"category": "Space Survival"
},
{
"id": "am-20",
"name": "Acid lake access",
"category": "Outdoors"
},
{
"id": "am-24",
"name": "Time travel paradoxes",
"category": "Outdoors"
}
]
},
{
"id": "listing-2",
"title": "Cozy yurt in Mraza",
"amenities": [
{
"id": "am-1",
"name": "Interdimensional wifi",
"category": "Accommodation Details"
},
{
"id": "am-4",
"name": "Adjustable gravity",
"category": "Accommodation Details"
},
{
"id": "am-7",
"name": "Wormhole trash chute",
"category": "Accommodation Details"
},
{
"id": "am-28",
"name": "Multi-planetary cable TV",
"category": "Accommodation Details"
},
{
"id": "am-2",
"name": "Towel",
"category": "Accommodation Details"
},
{
"id": "am-5",
"name": "Quantum microwave",
"category": "Accommodation Details"
},
{
"id": "am-29",
"name": "Cryochamber",
"category": "Accommodation Details"
},
{
"id": "am-3",
"name": "Universal remote",
"category": "Accommodation Details"
},
{
"id": "am-6",
"name": "Retractable moonroof",
"category": "Accommodation Details"
},
{
"id": "am-9",
"name": "Cosmic jacuzzi",
"category": "Accommodation Details"
},
{
"id": "am-30",
"name": "Heated sleeping pods",
"category": "Accommodation Details"
},
{
"id": "am-16",
"name": "Panic button",
"category": "Space Survival"
},
{
"id": "am-17",
"name": "Emergency life support systems",
"category": "Space Survival"
},
{
"id": "am-14",
"name": "First-aid kit",
"category": "Space Survival"
},
{
"id": "am-13",
"name": "Meteor shower shield",
"category": "Space Survival"
},
{
"id": "am-23",
"name": "Space view",
"category": "Outdoors"
},
{
"id": "am-26",
"name": "Meteor showers",
"category": "Outdoors"
},
{
"id": "am-22",
"name": "Hydroponic garden",
"category": "Outdoors"
},
{
"id": "am-12",
"name": "SOS button",
"category": "Space Survival"
},
{
"id": "am-10",
"name": "Oxygen",
"category": "Space Survival"
},
{
"id": "am-15",
"name": "Water recycler",
"category": "Space Survival"
},
{
"id": "am-18",
"name": "Universal translator",
"category": "Space Survival"
}
]
},
{
"id": "listing-3",
"title": "Repurposed mid century aircraft in Kessail",
"amenities": [
{
"id": "am-15",
"name": "Water recycler",
"category": "Space Survival"
},
{
"id": "am-16",
"name": "Panic button",
"category": "Space Survival"
},
{
"id": "am-17",
"name": "Emergency life support systems",
"category": "Space Survival"
},
{
"id": "am-4",
"name": "Adjustable gravity",
"category": "Accommodation Details"
},
{
"id": "am-5",
"name": "Quantum microwave",
"category": "Accommodation Details"
},
{
"id": "am-6",
"name": "Retractable moonroof",
"category": "Accommodation Details"
},
{
"id": "am-7",
"name": "Wormhole trash chute",
"category": "Accommodation Details"
}
]
}
]
}
}

And we see just one line printed out in our terminal:

Terminal output
Calling the /amenities/listings endpoint with listings [listing-1, listing-2, listing-3]

Our listing IDs have successfully been batched together in a single request! 👏👏👏

Practice

Which of the following statements describes how a data loader works with a datafetcher?

Key takeaways

  • To actually use a data loader, we call its load method in a datafetcher method.
  • The DgsDataFetchingEnvironment parameter available to every datafetcher method includes a getDataLoader method.
  • DGS' built-in data loaders come with caching benefits, and will deduplicate the keys they make requests for.

Congratulations!

And with that, you've done it! You've tackled the n+1 problem—with data loaders in your toolbelt, you have the resources to boost your datafetchers' efficiency. We've taken a basic data fetching strategy in our application, and made it into something that can scale with our queries and features. By employing data loaders, we've seen how we can make fewer, more efficient requests across the network, delivering up data much faster than we ever could have before.

Next up: learn how to grow your GraphQL API with an entirely new domain in Federation with Java & DGS. We'll cover the best practices of building a federated graph, along with the GraphOS tools you'll use to make the path toward robust enterprise APIs smooth, observable, and performant!

Thanks for joining us in this course, and we can't wait to see you in the next.

Previous

Share your questions and comments about this lesson

This course is currently in

beta
. Your feedback helps us improve! If you're stuck or confused, let us know and we'll help you out. All comments are public and must follow the Apollo Code of Conduct. Note that comments that have been resolved or addressed may be removed.

You'll need a GitHub account to post below. Don't have one? Post in our Odyssey forum instead.

              field

              A unit of data that belongs to a type in a schema. Every GraphQL query requests one or more fields.

              type Author {
              # id, firstName, and lastName are all fields of the Author type
              id: Int!
              firstName: String
              lastName: String
              }
              query

              A request for specific data from a GraphQL server. Clients define the structure of the response, enabling precise and efficient data retrieval.

              query

              A request for specific data from a GraphQL server. Clients define the structure of the response, enabling precise and efficient data retrieval.

              variables

              A placeholder for dynamic values in an operation allowing parameterization and reusability in requests. Variables can be used to fill arguments or passed to directives.

              query GetUser($userId: ID!) {
              user(id: $userId) {
              firstName
              }
              }

              In the query above, userId is a variable. The variable and its type are declared in the operation signature, signified by a $. The type of variable is a non-nullable ID. A variable's type must match the type of any argument it's used for.

              query

              A request for specific data from a GraphQL server. Clients define the structure of the response, enabling precise and efficient data retrieval.

              launch

              The process of applying a set of updates to a supergraph. Launches are usually triggered by making changes to one of your published subgraph schemas.

              query

              A request for specific data from a GraphQL server. Clients define the structure of the response, enabling precise and efficient data retrieval.

              query

              A request for specific data from a GraphQL server. Clients define the structure of the response, enabling precise and efficient data retrieval.

              GraphQL

              An open-source query language and specification for APIs that enables clients to request specific data, promoting efficiency and flexibility in data retrieval.

              graph

              A schema-based data model representing how different data elements interconnect and can be accessed.

              GraphOS

              A platform for building and managing a supergraph. It provides a management plane to test and ship changes and runtime capabilities to secure and monitor the graph.

              NEW COURSE ALERT

              Introducing Apollo Connectors

              Connectors are the new and easy way to get started with GraphQL, using existing REST APIs.

              Say goodbye to GraphQL servers and resolvers—now, everything happens in the schema!

              Take the course