3. Registering a data loader
10m

Overview

It's time to introduce data loaders to our application.

In this lesson, we will:

  • Implement and register our data loader

Adding the data loader

We have a new method on our SpotifyClient class that's responsible for fetching data for multiple artists. However, it won't be enough to just update our Track.artist datafetcher to use this method.

Imagine we did something like the following, replacing the call to artistRequest with multipleArtistsRequest.

datafetchers/TrackDataFetcher
@DgsData(parentType="Track", field="artist")
public MappedArtist getArtist(DgsDataFetchingEnvironment dfe) {
MappedTrack track = dfe.getSource();
String artistId = track.getArtistId();
- return spotifyClient.artistRequest(artistId);
+ return spotifyClient.multipleArtistsRequest(artistId);
}

Though this change would utilize our new endpoint, it wouldn't be enough to make our more performant. The Track.artist datafetcher, which is called for every track in need of artist data, will still trigger an individual network request for every artist it attempts to resolve.

In effect, we'd be doing the same thing as beforeβ€”calling a REST endpoint for each individual artist ID. No performance gains here!

A diagram showing the followup request needed for each track's artist

This is where our data loader class comes in. Here's how it will all work together.

  1. We'll create a data loader class.
  2. We'll give our class a load method. This method is responsible for gathering all of the necessary keys that data needs to be fetched for (those will be our artist IDs for every track in the ).
  3. We'll update our datafetcher method for the Track.artist . Rather than calling our SpotifyClient methods directly, it will delegate responsibility to the data loader to gather up, and provide data for, all of the necessary track artists in our . All at once!

We'll tackle the first two steps in this lesson, and bring everything home in the next. Let's get started!

Step 1 - Creating the data loader class

We'll start by jumping into our code and creating a new directory, dataloaders, to live alongside datafetchers, datasources, and models.

πŸ“‚ com.example.soundtracks
┣ πŸ“‚ datafetchers
┣ πŸ“‚ dataloaders
┣ πŸ“‚ datasources
┣ πŸ“‚ models
┣ πŸ“„ SoundtracksApplication
β”— πŸ“„ WebConfiguration

Inside of the dataloaders directory, create a class called ArtistDataLoader.

dataloaders/ArtistDataLoader.java
package com.example.soundtracks.dataloaders;
public class ArtistDataLoader {
// TODO
}

Adding the @DgsDataLoader annotation

To be "registered" in our application as a data loader, we'll bring in some new imports.

dataloaders/ArtistDataLoader
import com.example.soundtracks.datasources.SpotifyClient;
import com.example.soundtracks.models.MappedArtist;
import com.netflix.graphql.dgs.DgsDataLoader;
import org.dataloader.BatchLoader;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;

Next, we'll mark our class as an official data loader using the @DgsDataLoader annotation. We'll provide it with a name property we can use to identify this data loader elsewhere.

dataloaders/ArtistDataLoader.java
@DgsDataLoader(name = "artists")
public class ArtistDataLoader {
// TODO
}

The BatchLoader interface

To act as a true data loader, our class needs to implement a particular interface: BatchLoader. This utility accepts a list of keys, and loads the corresponding values in a promise.

The BatchLoader interface takes in two parameters:

  1. The type of data for each identifier the batch loader will collect
  2. The type of data that the batch loader is expected to return for each identifier

Let's go ahead and update our class to implement this interface.

For this particular data loader, the BatchLoader will collect artist IDs of type String, and return MappedArtist types.

dataloaders/ArtistDataLoader.java
public class ArtistDataLoader implements BatchLoader<String, MappedArtist> {
// TODO
}

Step 2 - Adding the load method

Next, we'll provide the class' load method. This method has a very specific signature.

The load method signature
public CompletionStage<List<SomeJavaClass>> load(List<DataTypeOfIdentifiers> listOfIdentifiers) {
// logic to fetch data by identifiers
}

It needs to accept a List of keys, and return a CompletionStage type, which accepts a List of whatever class the batch loader resolves to. Let's apply these one by one.

First, we'll write our load method and give it a List<String> parameter called artistIds.

dataloaders/ArtistDataLoader.java
public void load(List<String> artistIds) {
// TODO
}

Next, let's update the return type. To comply with the BatchLoader interface, our method needs to return a CompletionStage type, which accepts a type .

public CompletionStage<> load(List<String> artistIds) {
// TODO
}

Note: We use the CompletionStage interface when working with asynchronous actions. We'll use CompletableFuture, a class that implements the CompletionStage interface, shortly.

For the type , we'll pass the type that we expect our SpotifyClient class' multipleArtistsRequest method to return - namely, a List of MappedArtist types! So, we can update our load method's return type so that CompletionStage accepts a type of List<MappedArtist>.

Here's what that looks like.

public CompletionStage<List<MappedArtist>> load(List<String> artistIds) {
// TODO
}

Finally, we need to make the call to our SpotifyClient class method multipleArtistsRequest, using the artistIds parameter, to actually get our artist data.

But we can't just call the method here, and return the results. Our load method's signature indicates that it returns a CompletionStage type.

To satisfy this, we'll use CompletableFuture, a class that implements the CompletionStage interface, and call one of its methods: supplyAsync. This method accepts a function, which is where we'll actually call the multipleArtistsRequest method, passing in artistIds.

public CompletionStage<List<MappedArtist>> load(List<String> artistIds) {
return CompletableFuture.supplyAsync(() -> spotifyClient.multipleArtistsRequest(artistIds));
}

Our ArtistDataLoader class is nearly complete. For our load method to be valid, we need to apply the @Override annotation (this overrides the BatchLoader interface's implementation). We also need to provide an instance of the SpotifyClient for our data loader to work with. Note that we're using the Spring @Autowired annotation to take advantage of dependency injection. This means that SpotifyClient will automatically be injected when we create an instance of the ArtistDataLoader class.

@DgsDataLoader(name = "artists")
public class ArtistDataLoader implements BatchLoader<String, MappedArtist> {
@Autowired
SpotifyClient spotifyClient;
@Override
public CompletionStage<List<MappedArtist>> load(List<String> artistIds) {
return CompletableFuture.supplyAsync(() -> spotifyClient.multipleArtistsRequest(artistIds));
}
}

And that's everything we need for the data loader to do its job!

Key takeaways

  • To register a data loader in our app, we need to complete three steps:
    • The class needs to have the @DgsDataLoader annotation applied, with a name provided
    • The class needs to implement either the BatchLoader or MappedBatchLoader interface
    • The class needs a load function that follows a very specific signature

Up next

One last step, and our data loader will do the rest of the heavy lifting for us. In the next lesson, we'll finally apply our data loader inside of our datafetcher.

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.