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, what it's used for, and how to define it
- Convert the
Listing
type into an entity
Reviews and listings
Remember our dream query?
query GetListingAndReviews {listing(id: "listing-1") {titledescriptionnumOfBedsamenities {namecategory}overallRatingreviews {idtext}}}
As this query shows, we want reviews
and overallRating
to be fields on the Listing
type.
The big problem with that? As we've previously discussed, the reviews
subgraph has no idea about the types in the listings
subgraph—much less that there's a Listing
type it should be adding a field to! 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.
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!
In our example, we want to supplement our Listing
type with reviews-related data. That makes the Listing
type a perfect candidate to become an entity; it needs to have fields that are defined in both of our subgraphs. This lets us build a more powerful representation of what a "listing" is in our API, and the different data that makes it up.
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. Just like in a database, this is the information that lets us tell one thing from another. When we talk about listings, we use their id
field to identify which listing we're talking about.
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!
![Illustration showing three entities with unique ids](https://res.cloudinary.com/apollographql/image/upload/e_sharpen:50,c_scale,q_90,w_1440,fl_progressive/odyssey/dgs-and-hotchocolate/entities-recipe-keys.png)
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's able to 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 specific instance of an entity. A representation 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,Listing.__typename
returns "Listing".The
@key
field: The key-value pair that a subgraph can use to identify the instance of an entity. For example, if we defined theListing
entity using the "id" field as a primary key, then our entity representation would include an "id" property with a value like "listing-3".
An entity representation passed to each subgraph resolving fields for a particular Listing
might look like this:
{"__typename": "Listing","id": "listing-3"}
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 listing will give our reviews
subgraph all the information it needs (nothing more, nothing less!) to contribute the relevant reviews data.
Creating the Listing
entity
Bringing this all back to our dream query! We want to make the Listing
type an entity so that the reviews
subgraph can contribute two fields to it: overallRating
and reviews
.
Let's get to it!
In the listings
subgraph
With your rover dev
process still running, open up the listings
subgraph and navigate to schema.graphql
. Scroll down to find the Listing
type.
Here we'll apply the @key
directive, passing in a fields
property set to "id"
.
type Listing @key(fields: "id") {id: ID!# ... other Listing fields}
When we save our changes, rover dev
will automatically pick up on the new schema and restart the composition process. Make sure it composes successfully before moving on to the reviews
subgraph.
In the reviews
subgraph
Open up your code editor to the reviews
subgraph, and navigate to the src/schema.graphql
file. We'll start by adding a basic stub of the Listing
type: this consists of the typename, the @key
directive, and the field assigned as its primary key, id
.
type Listing @key(fields: "id") {id: ID!}
Notice that we don't include any other fields from the Listing
entity as defined in the listings
subgraph. We can keep our reviews
subgraph clean and concerned only with the data it has the responsibility of providing.
Let's add that reviews
field to the Listing
type. We'll give it a return type of [Review!]!
. We'll also define the overallRating
, which returns a nullable Float
type.
type Listing @key(fields: "id") {id: ID!"The submitted reviews for this listing"reviews: [Review!]!"The overall calculated rating for a listing"overallRating: Float}
Testing our schema changes
Our rover dev
process should still be running, so let's jump back to http://localhost:4002
.
Now we'll see that the reviews
field is available under the Listing
type. Our small schema changes have made it possible to construct our dream query!
query GetListingAndReviews {listing(id: "listing-1") {titledescriptionnumOfBedsamenities {namecategory}overallRatingreviews {idtext}}}
Though it's lovely to look at, our query can't actually return data yet. While the listings
subgraph can easily resolve its listing fields (id
, title
, description
, numOfBeds
, and amenities
), we still haven't given the reviews
subgraph any idea of what to do when the router requests a particular listing's reviews
or overallRating
.
In fact, if we run the query, we'll see just a bunch of gnarly errors.
Practice
Drag items from this box to the blanks above
router
more than one
@key
@primary
reference resolver
@unique
only one
fields
keys
subgraph
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).
Up next
In the next lesson, we'll equip our reviews
subgraph with logic to actually return data.
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.