7. Contributing to an entity
15m

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 type is and where to find entities in our using Studio
  • Contribute to an existing defined in another

Recipes and soundtracks

Remember our dream ?

query GetRecipeAndRecommendedSoundtracks {
recipe(id: "rec3j49yFpY2uRNM1") {
name
description
ingredients {
text
}
instructions
recommendedPlaylists {
id
name
description
tracks {
explicit
id
name
uri
durationMs
}
}
}
}

We envision selecting a particular recipe and immediately getting recommendations for the perfect soundtrack to pair with it. We'll call this : recommendedPlaylists.

The recommendedPlaylists needs to live in the soundtracks , because it's related to music data, but it belongs to the Recipe type.

The big problem with that? The soundtracks has no idea what the Recipe type is! Let's fix that—with entities.

What's an entity?

An entity is an with split between multiple . When working with an , each subgraph can do one or both of the following:

  • Contribute different to the
  • Reference an , which means using it as a return type for another defined in the

Contributing vs. referencing

To differentiate between that contribute to an , and those that reference an , think of it this way: a that contributes is actually adding new data capabilities from its own domain to the type.

This is in contrast to a that merely references the ; 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 or another; instead, they can span our entire API!

We already have an in our . 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 !

studio.apollographql.com

The Schema page, listing Object types with the Recipe entity highlighted

Note: We saw that same label in Explorer, in the Documentation panel, beside any that returned the Recipe type.

To create an , a needs to provide two things: a primary key and a reference resolver.

Defining a primary key

An 's primary key is the (or fields) that can uniquely identify an instance of that within a . The 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 's primary key is its id . This means that the can use a particular recipe's id to gather its data from multiple .

Illustration showing three entities with unique ids

We use the @key , along with a property called fields to set the we want to use as the 's primary key.

Entity syntax
type EntityType @key(fields: "id")

Reference resolvers and entity representations

Because an 's can be divided across , each subgraph that provides needs a way to identify the instance the is gathering data for. When it can identify this instance, the can provide additional data. Identifying a particular instance of an takes the form of a method called a reference resolver.

This method receives a basic object called an representation. An entity representation is an object that the uses to identify a particular instance. It always includes the typename for the type, along with the @key and its corresponding value.

Note: The __typename exists on all types automatically. It always returns the name of the type, as a string. For example, Recipe.__typename returns "Recipe".

An representation for a recipe might look like this:

Example recipe entity representation
{
"__typename": "Recipe",
"id": "rec3j49yFpY2uRNM1"
}

You can think of the representation as the minimum basic information the needs to associate data from multiple , and ensure that each subgraph is talking about the same object.

As we'll see shortly, the representation for a particular recipe will give our soundtracks all the information it needs (nothing more, nothing less!) to contribute some recommended playlists.

Contributing fields to an entity

The Recipe type is an ; this means that even though it's originally defined in the recipes , we can use it—and build onto it—in our soundtracks .

To add to the Recipe , we need it to exist in our soundtracks .

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 , and the assigned as its primary key, id.

schema.graphqls
type Recipe @key(fields: "id") {
id: ID!
}

Notice that we don't need to include any other from the Recipe as defined in the recipes . We can keep our soundtracks clean and concerned only with the data it requires to do its job.

Next, let's add that recommendedPlaylists to the Recipe type. We'll give it a return type of [Playlist!]!.

schema.graphqls
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 . Now we'll see that the recommendedPlaylists is available under the Recipe type. Our small schema changes have made it possible to construct our dream !

http://localhost:4000

Sandbox with our dream query syntax written into the Operation panel

query GetRecipeAndRecommendedSoundtracks {
recipe(id: "rec3j49yFpY2uRNM1") {
name
description
ingredients {
text
}
instructions
recommendedPlaylists {
id
name
description
tracks {
explicit
id
name
uri
durationMs
}
}
}
}

Though it's lovely to look at, our can't actually return data yet. This is because while the recipes 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 requests its recipe-themed recommendations.

In fact, if we run the , we'll see just that: recipes-provided details return as expected, but we've got some gnarly errors when we reach the recommendedPlaylists .

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.

Task!

To help the soundtracks understand what it needs to provide when we ask for recommendedPlaylists, we need to give it a new datafetcher method to handle that .

To keep our code clean, we'll add a new datafetcher class in the datafetchers directory called RecommendationDataFetcher.

datafetchers/RecommendationDataFetcher
package com.example.soundtracks.datafetchers;
import com.netflix.graphql.dgs.DgsComponent;
@DgsComponent
public 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 , and returns an instance of the generated Recipe class. Because Recipe is an , 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 representation it receives from the .

@DgsComponent
public 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 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 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 provided by the recipes 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 's reference resolver for Recipe. It receives an representation from the that includes a recipe's __typename and id .

The job of this method is to return a Recipe instance—with the recommendedPlaylists populated! The then takes the objects it's collected from both the recipes and soundtracks , composes them together, and lets us all the Recipe type's at once (regardless of how they're divided between !).

Working with entity representations

Speaking of representations, let's make sure our method is hooked up to receive the object the will pass it. (Remember that the router will send the soundtracks 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 '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 now, even though soundtracks isn't yet contributing any new data, we'd see representation has the following shape and contents.

the Recipe entity representation
{__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.

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 , and restart the local . So let's try out our at http://localhost:4000. Our mocked playlists don't return any track data just yet, so we'll pare down our slightly.

query GetRecipeAndRecommendedSoundtracks {
recipe(id: "rec3j49yFpY2uRNM1") {
name
description
ingredients {
text
}
instructions
recommendedPlaylists {
id
name
description
}
}
}

Let's check out the . We'll see that first, the 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 into a single instance of a Recipe type for the recipe we've queried!

http://localhost:4000

The operation in Sandbox, with the Query Plan Preview opened, showing a linear path to retrieve data from both subgraphs

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 on port 4000 will automatically refresh.

Up next

Awesome! Our recipes and soundtracks services are now collaborating on the same Recipe . Each contributes its own , and the 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 and fortify our soundtrack suggestions.

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.