8. Entities and the query plan
5m

Overview

Time to add a new tool to our developer tool belts: entities!

In this lesson, we will:

  • Learn what an is, what it's used for, and how to define it
  • Learn how the uses representations and the to connect data from multiple s
  • Learn how representations and reference work together

Recipes and soundtracks

Remember our dream ?

The dream query
query GetRecipeAndRecommendedSoundtracks {
randomRecipe {
id
name
description
ingredients {
text
}
instructions
recommendedPlaylists {
id
name
description
tracks {
id
name
explicit
durationMs
}
}
}
}

We envision generating a random recipe and immediately getting recommendations for the perfect soundtracks 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 . 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 that defines an 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.

How to create an entity

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.

The @key needs a property called fields, which we'll set to the we want to use as the 's primary key.

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

In Hot Chocolate, this is represented by the [Key] attribute.

Defining a reference resolver function

Each that contributes to an also needs to define a special function for that entity called a reference resolver. The reference is responsible for returning a particular instance of an .

To define a reference function in Hot Chocolate, we use the attribute [ReferenceResolver].

To help return an instance of an , the reference will have access to what's called an entity representation.

What's an entity representation?

An entity representation is an object that the uses to identify a specific instance of an . A representation always includes the typename for that and the @key for the specific instance.

  • The __typename field: This exists on all 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 can use to identify the instance of an . For example, if we defined the Recipe using the "id" as a primary key, then our entity representation would include an "id" property with a value like "rec3j49yFpY2uRNM1".

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.

How the router resolves data using entities and the query plan

Let's take a pared-down version of our dream as an example.

query GetRecipeWithPlaylists {
randomRecipe {
name
description
recommendedPlaylists {
name
}
}
}

The client will send this over to the .

Step 1: Building the query plan

The begins by building a query plan that indicates which requests to send to which .

The starts with the incoming 's top-level , randomRecipe. With the help of the , the sees that randomRecipe is defined in the recipes .

So the starts the with a request to the recipes .

The continues like this, checking each in the against the , and adding it to the . The description also belongs to the recipes .

But when the reaches the recommendedPlaylists for a particular Recipe, it sees from the that Recipe.recommendedPlaylists can only be resolved by the soundtracks (because that's where the`Recipe.recommendedPlaylists is defined).

That means the is going to have to connect data between .

To do this, the needs some more information from the recipes : the representation for the Recipe object.

Remember that representations are what the uses to track a specific object between . To make an entity representation for a Recipe object, the needs the recipe's typename and its primary key (which in this case is the id ).

The can get both these from the recipes .

From there, the adds another to its to request for each playlist's name from the soundtracks .

With that, all the in the have been accounted for in the . It's time to move on to the next step: executing the plan.

Step 2: Querying the recipes subgraph

The begins by requesting data from the recipes .

The recipes resolves all the requested as it normally would, including the representations for all the requested Recipe objects.

This doesn't know that the 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 's taken care of the first part of the ! The next step is to retrieve the Playlist.name from the soundtracks .

Step 3: Querying the soundtracks subgraph

Remember the _entities that showed up in our when we enabled federation? This is where it comes back into the story!

The builds a request using the _entities .

This takes in an called representations, which takes in, well, a list of representations! This is where the entity representations that the received from the recipes will go.

In the same request, the adds the rest of the left in the (in this case, each playlist's name).

The sends this request to the soundtracks .

To resolve the _entities , the soundtracks uses its reference resolver. Remember this is a special function used to return all the that this contributes.

The soundtracks looks at the __typename value of each reference object to determine which 's reference to use. In this case, because typename is "Recipe", the soundtracks knows to use the Recipe 's reference .

The Recipe reference runs once for each representation in the . Each time, it uses the entity representation's primary key to return the corresponding Recipe object.

After the soundtracks finishes resolving the request, it sends the data back to the .

That's it for the executing phase!

Step 4: Sending the final response to the client

Now, the combines all the data it received from the recipes and soundtracks into a single JSON object. And at last, the sends the final object back to the client.

Practice

Where should an entity's reference resolver function be defined?
Which of the following steps do NOT occur as part of how the router builds and executes its query plan?

Key takeaways

  • An is a type that can resolve its across multiple .
  • To create an , we can use the @key to specify which (s) can uniquely identify an object of that type.
  • We can use entities in two ways:
    • As a return type for a (referencing an ).
    • Defining for an from multiple (contributing to an entity).
  • Any that contributes to an needs to define a reference function for that entity. This __resolveReference is called whenever the needs to access of the from within another .
  • An representation is an object that the uses to represent a specific instance of an entity. It includes the entity's type and its key (s).

Up next

Let's put this theory into practice! We'll contribute to the Recipe in the soundtracks .

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.