4. Datafetchers
10m

Overview

But before we can our server for some playlist data, we need to define exactly HOW that data should be retrieved.

In this lesson, we will:

  • Learn about datafetchers and how they work in DGS
  • Explore DGS code generation
  • Return some hard-coded mock data

🛠 Datafetcher first steps

We're well on our way to building a that can:

  1. Receive an incoming from our client
  2. Validate that against our schema
  3. Retrieve the data for the queried schema s
  4. And return the data as a response

We've already defined our schema, so DGS can handle steps #1 and #2 out of the box.

Our task now is to actually define how data is retrieved and returned when a is queried. We'll wrap up all of these instructions in a method called a datafetcher. (In other frameworks, you might see datafetchers described as resolver functions.)

We use datafetchers to map in our schema to logic that can fulfill them. In other words, we can write individualized methods that are called when certain schema fields—such as our Query type's featuredPlaylist —are queried.

type Query {
featuredPlaylists: [Playlist!]!
}

These methods have the responsibility of returning data in the shape that our schema expects—for example, the datafetcher we write for featuredPlaylists should return a list of objects that match the Playlist type.

Let's define a class to contain our datafetcher methods.

✏️ Writing a datafetcher

To keep our code organized, let's create a new datafetchers directory in our java/com.example.soundtracks package.

📂 main
┣ 📂 java
┃ ┣ 📂 com.example.soundtracks
┃ ┃ ┃ ┣ 📂 datafetchers
┃ ┃ ┃ ┣ 📄 SoundtracksApplication
┃ ┃ ┃ ┣ 📄 WebConfiguration

Inside, we can create a new class file called PlaylistDataFetcher.

datafetchers/PlaylistDataFetcher
package com.example.soundtracks.datafetchers;
public class PlaylistDataFetcher {
}

Our empty class is ready for some methods to do actual data-fetching, but we first need to denote it with a special annotation that tells DGS to include it when assembling the pieces of our API.

This annotation is called @DgsComponent, and all we need to do is import it from our DGS package and stick it on top of our class definition. Now it's officially a member of the DGS team!

package com.example.soundtracks.datafetchers;
import com.netflix.graphql.dgs.DgsComponent;
@DgsComponent
public class PlaylistDataFetcher {
}

Next, let's give this class a basic method called featuredPlaylists, to match the Query in our schema.

public void featuredPlaylists() {
// specific featuredPlaylist-fetching logic goes here
}

To work as a datafetcher, a method needs to specify which it's responsible for. To clarify this, we have additional DGS annotations that do most of the heavy lifting for us.

Because the featuredPlaylists method is responsible for the featuredPlaylists on the Query type, we can use the @DgsQuery annotation. (Don't forget to import it from our DGS package!)

package com.example.soundtracks.datafetchers;
import com.netflix.graphql.dgs.DgsComponent;
import com.netflix.graphql.dgs.DgsQuery;
@DgsComponent
public class PlaylistDataFetcher {
@DgsQuery
public void featuredPlaylists() {
// specific featuredPlaylist-fetching logic goes here
}
}

For @DgsQuery to work out of the box, we need to make sure that our method has the SAME name as the Query it resolves. This gives DGS all the info it needs to mark the PlaylistDataFetcher.featuredPlaylists as the official datafetcher for the Query.featuredPlaylists schema .

There's still one big problem: our featuredPlaylists method isn't actually returning anything. But in our schema, we said that a for the featuredPlaylists should return a list of Playlist types!

type Query {
"Playlists hand-picked to be featured to all users."
featuredPlaylists: [Playlist!]!
}

We need to tweak our method so that the featuredPlaylists actually returns the type of data we said it would. But outside of our schema.graphqls file, where do we actually get these type definitions for our datafetchers to use?

Code generation

We know the properties our Playlist type needs, so we could define a corresponding Playlist class to represent each object in Java. This approach lets us set all of the properties, including getter and setter methods, while maintaining fine-tuned control over any transformations we might need to do when the data is en route to our clients.

Another option is to use DGS' Code Generation plugin. This tool reads in our schema file and generates classes from the types we've written. With this approach, on a GraphQL type—like a Playlist's name or description—become gettable and settable properties on the corresponding class.

There are benefits to each approach, so we'll steal a bit from both. We're going to use code generation as a starting point, and attach custom logic to our classes as we go along. Let's get started!

Generating our basic classes

Open up your project's build.gradle.kts file and check out plugins at the top. The package that enables code generation in DGS, dgs.codegen, is already listed here.

build.gradle.kts
plugins {
id("org.springframework.boot") version "3.0.12"
id("io.spring.dependency-management") version "1.1.3"
id("com.netflix.dgs.codegen") version "6.0.3"
}

The plugin runs automatically as part of our code's build process, and it actually takes effect as soon as we run our code.

Start up your server again, either by using your IDE's run button or running the following command in the terminal:

./gradlew bootRun
Task!

In the build.generated.sources folder we'll find a few new packages, including dgs-codegen and dgs-codegen-generated-examples.

📂 build
┣ 📂 classes
┣ 📂 generated
┃ ┣ 📂 sources
┃ ┃ ┣ 📂 annotationProcessor
┃ ┃ ┣ 📂 dgs-codegen
┃ ┃ ┣ 📂 dgs-codegen-generated-examples
┃ ┃ ┣ 📂 headers
┣ 📂 resources
┗ 📂 tmp

The dgs-codegen package contains all of the Java types the plugin generated from reading our schema.graphqls file. If we drill down into the dgs-codegen.com.example.soundtracks.generated.types file, we'll see the generated Playlist file.

📂 dgs-codegen
┣ 📂 com.example.soundtracks
┃ ┣ 📂 codegen
┃ ┣ 📂 generated
┃ ┃ ┣ 📂 client
┃ ┃ ┣ 📂 types
┃ ┃ ┃ ┣ 📄 Playlist

A closer look at the generated Playlist class reveals the collection of properties, getters, and setters that make it possible to create objects that match the specification of the Playlist type in our schema. We'll also find a number of other methods that the plugin has added for us to make working with the class and building new instances even easier.

Snippet of the Playlist class
package com.example.soundtracks.generated.types;
import java.lang.Object;
import java.lang.Override;
import java.lang.String;
/**
* A curated collection of tracks designed for a specific activity or mood.
*/
public class Playlist {
/**
* The ID for the playlist.
*/
private String id;
/**
* The name of the playlist.
*/
private String name;
/**
* Describes the playlist, what to expect and entices the user to listen.
*/
private String description;
// other properties, getters, setters, and methods
}

We're going to build on top of this generated Playlist class, and extend it with custom functionality in a child class.

Let's create a place in our code for this child class to live. Back in java/com.example.soundtracks, we'll define a new package called models to sit next to datafetchers.

Next, we'll define a new class called MappedPlaylist. This class will extend the generated Playlist class. Here's what it looks like:

models/MappedPlaylist
package com.example.soundtracks.models;
import com.example.soundtracks.generated.types.Playlist;
public class MappedPlaylist extends Playlist {
// custom logic will live here
}

We can put this MappedPlaylist class to work right away—back in our featuredPlaylists datafetcher.

Returning data

Returning now to our datafetchers/PlaylistDataFetcher file, we'll import the models/MappedPlaylist class, along with the Java List utility.

datafetchers/PlaylistDataFetcher
import com.example.soundtracks.models.MappedPlaylist;
import java.util.List;

Right away, we can update the return type for our method to be a List of MappedPlaylist types.

@DgsQuery
public List<MappedPlaylist> featuredPlaylists() {
}

First, let's create some new MappedPlaylist instances from our freshly-generated class.

@DgsQuery
public List<MappedPlaylist> featuredPlaylists() {
MappedPlaylist rockPlaylist = new MappedPlaylist();
}

We need to give our MappedPlaylist instance some attributes, such as id, name, and description; we can chain those on just by calling the property setter methods. (Make up whatever values you'd like for these , just remember to match the types that we gave them in our schema!)

MappedPlaylist rockPlaylist = new MappedPlaylist();
rockPlaylist.setId("1");
rockPlaylist.setName("Rock n' Roll");
rockPlaylist.setDescription("A rock n' roll playlist");

Great! Here's a new MappedPlaylist instance, with all its properties ready to go.

To make our List a bit more robust, we'll add at least one more MappedPlaylist instance.

@DgsQuery
public List<MappedPlaylist> featuredPlaylists() {
MappedPlaylist rockPlaylist = new MappedPlaylist();
rockPlaylist.setId("1");
rockPlaylist.setName("Rock n' Roll");
rockPlaylist.setDescription("A rock n' roll playlist");
MappedPlaylist popPlaylist = new MappedPlaylist();
popPlaylist.setId("2");
popPlaylist.setName("Pop");
popPlaylist.setDescription("A pop playlist");
return List.of(rockPlaylist, popPlaylist);
}

And with that, we've got our very first datafetcher set up, ready to be queried. Here's how your entire datafetcher file should look when you're done:

Practice

What is the primary purpose of a datafetcher?

Key takeaways

  • We use datafetcher methods to return data when a particular schema is queried.
  • The DGS code generation plugin gives us a helpful starting point for the Java classes that map to our types.

Up next

Whew! In the next lesson, we'll see how all the pieces come together—by sending our first queries!

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.