Overview
It's time to witness the true strength of federation; we're about to join the pieces and make our queries more powerful and dynamic than ever before.
In this lesson, we will:
- Learn what an entity type is and where to find entities in our supergraph using Studio
- Contribute fields to an existing entity defined in another subgraph
Recipes and soundtracks
Remember our dream query?
query GetRecipeAndRecommendedSoundtracks {recipe(id: "rec3j49yFpY2uRNM1") {namedescriptioningredients {text}instructionsrecommendedPlaylists {idnamedescriptiontracks {explicitidnameuridurationMs}}}}
We envision selecting a particular recipe and immediately getting recommendations for the perfect soundtrack to pair with it. We'll call this field: recommendedPlaylists.
The recommendedPlaylists
field needs to live in the soundtracks
subgraph, because it's related to music data, but it belongs to the Recipe
type.
The big problem with that? The soundtracks
subgraph has no idea what the Recipe
type is! Let's fix that—with entities.
What's an entity?
An entity is an object type with fields split between multiple subgraphs. When working with an entity, each subgraph can do one or both of the following:
- Contribute different fields to the entity
- Reference an entity, which means using it as a return type for another field defined in the subgraph
Contributing vs. referencing
To differentiate between subgraphs that contribute to an entity, and those that reference an entity, think of it this way: a subgraph that contributes fields is actually adding new data capabilities from its own domain to the entity type.
This is in contrast to a subgraph that merely references the entity; it's essentially just "mentioning" the existence of the entity, and providing it as the return type for another field.
In federation, we use entities to create cohesive types that aren't confined to just one subgraph or another; instead, they can span our entire API!
We already have an entity in our supergraph. Let's head over to Studio to our supergraph page and navigate to the Schema page.
Select Objects from the left-hand list. See that green E label beside the Recipe
type? E stands for entity!
Note: We saw that same label in Explorer, in the Documentation panel, beside any field that returned the Recipe
type.
To create an entity, a subgraph needs to provide two things: a primary key and a reference resolver.
Defining a primary key
An entity's primary key is the field (or fields) that can uniquely identify an instance of that entity within a subgraph. The router uses primary keys to collect data from across multiple subgraphs and associate it with a single entity instance. It's how we know that each subgraph is talking about—and providing data for—the same object!
For example, a recipe entity's primary key is its id
field. This means that the router can use a particular recipe's id
to gather its data from multiple subgraphs.
We use the @key
directive, along with a property called fields
to set the field we want to use as the entity's primary key.
type EntityType @key(fields: "id")
Reference resolvers and entity representations
Because an entity's fields can be divided across subgraphs, each subgraph that provides fields needs a way to identify the instance the router is gathering data for. When it can identify this instance, the subgraph can provide additional data. Identifying a particular instance of an entity takes the form of a method called a reference resolver.
This method receives a basic object called an entity representation. An entity representation is an object that the router uses to identify a particular entity instance. It always includes the typename for the entity type, along with the @key
field and its corresponding value.
Note: The __typename
field exists on all GraphQL types automatically. It always returns the name of the type, as a string. For example, Recipe.__typename
returns "Recipe".
An entity representation for a recipe might look like this:
{"__typename": "Recipe","id": "rec3j49yFpY2uRNM1"}
You can think of the entity representation as the minimum basic information the router needs to associate data from multiple subgraphs, and ensure that each subgraph is talking about the same object.
As we'll see shortly, the entity representation for a particular recipe will give our soundtracks
subgraph all the information it needs (nothing more, nothing less!) to contribute some recommended playlists.
Contributing fields to an entity
The Recipe
type is an entity; this means that even though it's originally defined in the recipes
subgraph, we can use it—and build onto it—in our soundtracks
subgraph.
To add to the Recipe
entity, we need it to exist in our soundtracks
subgraph.
Open up your code editor, and navigate to the src/main/resources/schema/schema.graphqls
file. We'll start by adding a basic stub of the Recipe
type: this consists of the type name, the @key
directive, and the field assigned as its primary key, id
.
type Recipe @key(fields: "id") {id: ID!}
Notice that we don't need to include any other fields from the Recipe
entity as defined in the recipes
subgraph. We can keep our soundtracks
subgraph clean and concerned only with the data it requires to do its job.
Next, let's add that recommendedPlaylists
field to the Recipe
type. We'll give it a return type of [Playlist!]!
.
type Recipe @key(fields: "id") {id: ID!"A list of recommended playlists for this particular recipe. Returns 1 to 3 playlists."recommendedPlaylists: [Playlist!]!}
Our rover dev
process should still be running, so let's jump back to http://localhost:4000
and build a new operation. Now we'll see that the recommendedPlaylists
field is available under the Recipe
type. Our small schema changes have made it possible to construct our dream query!
query GetRecipeAndRecommendedSoundtracks {recipe(id: "rec3j49yFpY2uRNM1") {namedescriptioningredients {text}instructionsrecommendedPlaylists {idnamedescriptiontracks {explicitidnameuridurationMs}}}}
Though it's lovely to look at, our query can't actually return data yet. This is because while the recipes subgraph can easily fulfill details about a recipe's name, ingredients, and instructions, we still haven't given soundtracks
any idea of what to do when the router requests its recipe-themed recommendations.
In fact, if we run the query, we'll see just that: recipes
-provided details return as expected, but we've got some gnarly errors when we reach the recommendedPlaylists
field.
Let's jump back into our code.
The RecommendationDataFetcher
class
We'll start by restarting our DGS server, and letting our code generation refresh with the new Recipe
type we've added to the schema.
To help the soundtracks
subgraph understand what it needs to provide when we ask for recommendedPlaylists
, we need to give it a new datafetcher method to handle that field.
To keep our code clean, we'll add a new datafetcher class in the datafetchers
directory called RecommendationDataFetcher
.
package com.example.soundtracks.datafetchers;import com.netflix.graphql.dgs.DgsComponent;@DgsComponentpublic class RecommendationDataFetcher {}
And let's also bring in a few more imports we'll need shortly.
import com.example.soundtracks.generated.types.Playlist;import com.example.soundtracks.generated.types.Recipe;import com.netflix.graphql.dgs.DgsEntityFetcher;import java.util.Map;import java.util.List;
The recommendedPlaylists
method
Next, we'll add a method that maps to the recommendedPlaylists
field, and returns an instance of the generated Recipe
class. Because Recipe
is an entity, we also need to use an annotation called @DgsEntityFetcher
and pass it our type name. This annotation tells DGS to create an instance of Recipe
based on the entity representation it receives from the router.
@DgsComponentpublic class RecommendationDataFetcher {@DgsEntityFetcher(name="Recipe")public Recipe recommendedPlaylists() {// TODO}}
Returning the Recipe
object
You might be asking yourself this question right now: why does the recommendedPlaylists
method return a Recipe
and not a list of Playlist
types?
According to our schema, the Recipe.recommendedPlaylists
field should return a list of Playlist
types, right? And that's just what we want it to do! So why does our recommendedPlaylists
method here return a Recipe
type instead?
First, we should distinguish between the GraphQL Playlist
and Recipe
types and the generated Recipe
Java class that we're returning here. Jump into the generated code file for Recipe
; we'll see that it's a class made up of only the methods and properties that we included in our soundtracks
schema. (None of the other Recipe
fields provided by the recipes
subgraph exist here!)
public class Recipe {private String id;/*** A list of recommended playlists to accompany the recipe*/private List<Playlist> recommendedPlaylists;// other methods}
The recommendedPlaylists
method we're writing serves as our soundtracks
subgraph's reference resolver for Recipe
. It receives an entity representation from the router that includes a recipe's __typename
and id
fields.
The job of this method is to return a Recipe
instance—with the recommendedPlaylists
field populated! The router then takes the objects it's collected from both the recipes
and soundtracks
subgraphs, composes them together, and lets us query all the Recipe
GraphQL type's fields at once (regardless of how they're divided between subgraphs!).
Working with entity representations
Speaking of entity representations, let's make sure our method is hooked up to receive the object the router will pass it. (Remember that the router will send the soundtracks
subgraph just the information it needs about the recipe that has been queried.)
In this case, all we'll receive is an object with _typename
and id
, the Recipe
entity's primary key!
We can receive this in our method as a Map<String, String>
type called representation
.
@DgsEntityFetcher(name="Recipe")public Recipe recommendedPlaylists(Map<String, String> representation) {// TODO}
If we were to run our dream query now, even though soundtracks
isn't yet contributing any new data, we'd see representation
has the following shape and contents.
{__typename=Recipe, id=rec3j49yFpY2uRNM1}
Cool! That's enough information to work with for now.
The first thing we should do inside of our method is create a new Recipe
instance we can build onto.
@DgsEntityFetcher(name="Recipe")public Recipe recommendedPlaylists(Map<String, String> representation) {Recipe recipe = new Recipe();}
Then, let's make sure that our method is working by creating a couple of mock playlists.
Playlist rockPlaylist = new Playlist();rockPlaylist.setId("1");rockPlaylist.setName("Rock n' Roll");rockPlaylist.setDescription("A rock n' roll playlist");Playlist popPlaylist = new Playlist();popPlaylist.setId("2");popPlaylist.setName("Pop");popPlaylist.setDescription("A pop playlist");List<Playlist> playlists = List.of(rockPlaylist, popPlaylist);
We'll set them as the value for the recommendedPlaylists
property on our Recipe
object, then return the recipe.
recipe.setRecommendedPlaylists(playlists);return recipe;
Time to recompile!
Our running rover dev
process will pick up the changes that we've made, recompose our supergraph schema, and restart the local router. So let's try out our query at http://localhost:4000
. Our mocked playlists don't return any track data just yet, so we'll pare down our query slightly.
query GetRecipeAndRecommendedSoundtracks {recipe(id: "rec3j49yFpY2uRNM1") {namedescriptioningredients {text}instructionsrecommendedPlaylists {idnamedescription}}}
Let's check out the query plan. We'll see that first, the router will fetch data from recipes
, and then use that data to build out its request to soundtracks
. The last step is flattening the response from both subgraphs into a single instance of a Recipe
type for the recipe
field we've queried!
Let's try it out, and... we've got data!
Still seeing recommendedPlaylists: null
? Try restarting your DGS server! The rover dev
process running our router on port 4000
will automatically refresh.
Practice
Key takeaways
- An entity is a type that can resolve its fields across multiple subgraphs.
- To create an entity, we can use the
@key
directive to specify which field(s) can uniquely identify an object of that type. - We can use entities in two ways:
- As a return type for a field (referencing an entity).
- Defining fields for an entity from multiple subgraphs (contributing to an entity).
- Any subgraph that contributes fields to an entity needs to define a reference resolver method for that entity. This method is called whenever the router needs to access fields of the entity from within another subgraph.
- In DGS, we denote a method as a reference resolver using the
@DgsEntityFetcher
annotation, passing in thename
of the type it resolves. This method receives an entity representation from the router. - An entity representation is an object that the router uses to represent a specific instance of an entity. It includes the entity's type and its key field(s).
Up next
Awesome! Our recipes
and soundtracks
services are now collaborating on the same Recipe
entity. Each subgraph contributes its own fields, and the router packages up the response for us.
However, our musical recommendations are still pretty weak. In fact, they're not much like recommendations at all—they're hardcoded playlist objects! We haven't actually used any recipe-specific data to determine the playlists we recommend to our potential chef users. In the next lesson, we'll dive deeper into federation-specific directives and fortify our soundtrack suggestions.
Share your questions and comments about this lesson
This course is currently in
You'll need a GitHub account to post below. Don't have one? Post in our Odyssey forum instead.