Overview
Time to add a new tool to our developer tool belts: entities!
In this lesson, we will:
- Learn what an entity is, what it's used for, and how to define it
- Learn how the router uses entity representations and the query plan to connect data from multiple subgraphs
- Learn how entity representations and reference resolvers work together
Recipes and soundtracks
Remember our dream query?
query GetRecipeAndRecommendedSoundtracks {randomRecipe {idnamedescriptioningredients {text}instructionsrecommendedPlaylists {idnamedescriptiontracks {idnameexplicitdurationMs}}}}
We envision generating a random recipe and immediately getting recommendations for the perfect soundtracks 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. It's the fundamental building block of a federated graph architecture, used to connect data between subgraphs while still adhering to the separation of concerns principle.
A subgraph that defines an entity 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.
How to create an entity
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.
The @key
directive needs a property called fields
, which we'll set to the field we want to use as the entity's primary key.
type EntityType @key(fields: "id") {}
In Hot Chocolate, this is represented by the [Key]
attribute.
Defining a reference resolver function
Each subgraph that contributes fields to an entity also needs to define a special resolver function for that entity called a reference resolver. The reference resolver is responsible for returning a particular instance of an entity.
To define a reference resolver function in Hot Chocolate, we use the attribute [ReferenceResolver]
.
To help return an instance of an entity, the reference resolver will have access to what's called an entity representation.
What's an entity representation?
An entity representation is an object that the router uses to identify a specific instance of an entity. A representation always includes the typename for that entity and the @key
field for the specific instance.
The
__typename
field: This field exists on all GraphQL types automatically. It always returns the name of its containing type, as a string. For example,Recipe.__typename
returns "Recipe".The
@key
field: The key-value pair that a subgraph can use to identify the instance of an entity. For example, if we defined theRecipe
entity using the "id" field as a primary key, then our entity representation would include an "id" property with a value like "rec3j49yFpY2uRNM1".
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.
How the router resolves data using entities and the query plan
Let's take a pared-down version of our dream query as an example.
query GetRecipeWithPlaylists {randomRecipe {namedescriptionrecommendedPlaylists {name}}}
The client will send this operation over to the router.
Step 1: Building the query plan
The router begins by building a query plan that indicates which requests to send to which subgraphs.
The router starts with the incoming query's top-level field, randomRecipe
. With the help of the supergraph schema, the router sees that randomRecipe
is defined in the recipes
subgraph.
So the router starts the query plan with a request to the recipes
subgraph.
The router continues like this, checking each field in the query against the supergraph schema, and adding it to the query plan. The description
field also belongs to the recipes
subgraph.
But when the router reaches the recommendedPlaylists
field for a particular Recipe
, it sees from the supergraph schema that Recipe.recommendedPlaylists
can only be resolved by the soundtracks
subgraph (because that's where the`Recipe.recommendedPlaylists field is defined).
That means the router is going to have to connect data between subgraphs.
To do this, the router needs some more information from the recipes
subgraph: the entity representation for the Recipe
object.
Remember that entity representations are what the router uses to track a specific object between subgraphs. To make an entity representation for a Recipe
object, the router needs the recipe's typename and its primary key (which in this case is the id
field).
The router can get both these fields from the recipes
subgraph.
From there, the router adds another operation to its query plan to request for each playlist's name
from the soundtracks
subgraph.
With that, all the fields in the query have been accounted for in the query plan. It's time to move on to the next step: executing the plan.
Step 2: Querying the recipes
subgraph
The router begins by requesting data from the recipes
subgraph.
The recipes
subgraph resolves all the requested fields as it normally would, including the entity representations for all the requested Recipe
objects.
This subgraph doesn't know that the router plans to do anything special with the recipe's id or typename. It just sends back the data to the router like it was asked.
With that, the router's taken care of the first part of the query plan! The next step is to retrieve the Playlist.name
field from the soundtracks
subgraph.
Step 3: Querying the soundtracks
subgraph
Remember the _entities
field that showed up in our subgraph when we enabled federation? This is where it comes back into the story!
The router builds a request using the _entities
field.
This field takes in an argument called representations
, which takes in, well, a list of entity representations! This is where the entity representations that the router received from the recipes
subgraph will go.
In the same request, the router adds the rest of the fields left in the query plan (in this case, each playlist's name
).
The router sends this request to the soundtracks
subgraph.
To resolve the _entities
field, the soundtracks
subgraph uses its reference resolver. Remember this is a special resolver function used to return all the entity fields that this subgraph contributes.
The soundtracks
subgraph looks at the __typename
value of each reference object to determine which entity's reference resolver to use. In this case, because typename is "Recipe", the soundtracks
subgraph knows to use the Recipe
entity's reference resolver.
The Recipe
reference resolver runs once for each entity representation in the query. Each time, it uses the entity representation's primary key to return the corresponding Recipe
object.
After the soundtracks
subgraph finishes resolving the request, it sends the data back to the router.
That's it for the executing phase!
Step 4: Sending the final response to the client
Now, the router combines all the data it received from the recipes
and soundtracks
subgraphs into a single JSON object. And at last, the router sends the final object back to the client.
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 function for that entity. This
__resolveReference
resolver is called whenever the router needs to access fields of the entity from within another subgraph. - 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
Let's put this theory into practice! We'll contribute fields to the Recipe
entity in the soundtracks
subgraph.
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.