title | description |
---|---|
Entities |
Reference and extend types across subgraphs |
import {CodeColumns, ExpansionPanel} from 'gatsby-theme-apollo-docs';
In Apollo Federation, an entity is an object type that you define canonically in one subgraph and can then reference and extend in other subgraphs. Entities are the core building block of a federated graph.
Types besides object types (unions, interfaces, etc.) cannot be entities.
In a GraphQL schema, you can designate any object type as an entity by adding a @key
directive to its definition, like so:
type Product @key(fields: "upc") {
upc: String!
name: String!
price: Int
}
The @key
directive defines the entity's primary key, which consists of one or more of the type's fields
. In the example above, the Product
entity's primary key is its upc
field. The gateway's query planner uses an entity's primary key to identify a given instance of the type.
An entity's
@key
cannot include fields that return a union or interface.
If an entity can be uniquely identified by more than one combination of fields, you can define more than one primary key for that entity.
In the following example, a Product
entity can be uniquely identified by either its upc
or its sku
:
type Product @key(fields: "upc") @key(fields: "sku") {
upc: String!
sku: String!
price: String
}
This pattern is helpful when different subgraphs interact with different fields of an entity. For example, a reviews
subgraph might refer to products by their UPC, whereas an inventory
subgraph might use SKUs.
A single primary key can consist of multiple fields, and even nested fields.
The following example shows a primary key that consists of both a user's id
and the id
of that user's associated organization:
type User @key(fields: "id organization { id }") {
id: ID!
organization: Organization!
}
type Organization {
id: ID!
}
After you define an entity in one subgraph, other subgraphs can then reference that entity. If a products
subgraph defines the Product
entity above, a reviews
subgraph can then add a field of type Product
to its Review
type, like so:
type Review {
product: Product
}
# This is a "stub" of the Product entity (see below)
extend type Product @key(fields: "upc") {
upc: String! @external
}
Because the Product
entity is defined in another subgraph, the reviews
subgraph needs to define a stub of it to make its own schema valid. The stub includes just enough information for the subgraph to know how to interact with a Product
:
- The
extend
keyword indicates thatProduct
is an entity that is defined in another subgraph (in this case, theproducts
subgraph). - The
@key
directive indicates thatProduct
uses theupc
field as its primary key. This value must match the value of exactly one@key
defined in the entity's originating subgraph, even if the entity defines multiple primary keys. - The
upc
field must be included in the stub because it is part of the specified@key
. It also must be annotated with the@external
directive to indicate that the field originates in another subgraph.
This explicit syntax has several benefits:
- It is standard GraphQL grammar.
- It enables you to run the
reviews
subgraph standalone with a valid schema, including aProduct
type with a singleupc
field. - It provides strong typing information that lets you catch mistakes at schema composition time.
In our example, the reviews
subgraph needs to define its own resolver for the Product
entity. The reviews
subgraph doesn't know much about Product
s, but fortunately, it doesn't need to. All it needs to do is return enough information to uniquely identify a given Product
, like so:
{
Review: {
product(review) {
return { __typename: "Product", upc: review.upc };
}
}
}
This return value is a representation of a Product
entity. Subgraphs use representations to reference entities from other subgraphs. A representation requires only an explicit __typename
definition and values for the entity's primary key fields.
The gateway provides this representation to the entity's originating subgraph to fetch the full object. For this to work, the originating subgraph (in this case, products
) must define a reference resolver for the Product
entity:
{
Product: {
__resolveReference(reference) {
return fetchProductByUPC(reference.upc);
}
}
}
Reference resolvers are a special addition to Apollo Server that enable entities to be referenced by other subgraphs. They are called whenever a query references an
entity
across subgraph boundaries. To learn more about__resolveReference
, see the API docs.
With this model, each implementing subgraph's schema represents a true subset of the complete graph. This prevents the need for defining foreign-key fields in individual schemas, and enables clients to transparently execute a query like the following, which hits both the products
and reviews
subgraphs:
{
reviews {
product {
name
price
}
}
}
A subgraph can add fields to an entity that's defined in another subgraph. This is called extending the entity.
When a subgraph extends an entity, the entity's originating subgraph is not aware of the added fields. Only the extending subgraph (along with the gateway) knows about these fields.
Each field of an entity should be defined in exactly one subgraph. Otherwise, a schema composition error will occur.
Let's say we want to add a reviews
field to the Product
entity. This field will hold a list of reviews for the product. The Product
entity originates in the products
subgraph, but it makes much more sense for the reviews
subgraph to resolve this particular field.
To handle this case, we can extend the Product
entity in the reviews
subgraph, like so:
extend type Product @key(fields: "upc") {
upc: String! @external
reviews: [Review]
}
This definition is nearly identical to the stub we defined for the Product
type in Referencing. All we've added is the reviews
field. We don't include an @external
directive, because this field does originate in the reviews
subgraph.
Whenever a subgraph extends an entity with a new field, it is also responsible for resolving the field. The gateway is automatically aware of this responsibility. In our example, the generated query plan will fetch the upc
field for each Product
from the products
subgraph and pass those to the reviews
subgraph, where you can then access these fields on the object passed into your reviews
resolver:
{
Product: {
reviews(product) {
return fetchReviewsForProduct(product.upc);
}
}
}
Let's say we want to be able to query for the inStock
status of a product. That information lives in an inventory
subgraph, so we'll add the type extension there:
extend type Product @key(fields: "upc") {
upc: ID! @external
inStock: Boolean
}
{
Product: {
inStock(product): {
return fetchInStockStatusForProduct(product.upc);
}
}
}
Similar to the reviews
relationship example above, the gateway fetches the required upc
field from the products
subgraph and passes it to the inventory
subgraph, even if the query didn't ask for the upc
:
query GetTopProductAvailability {
topProducts {
inStock
}
}
In Apollo Federation, the Query
and Mutation
base types originate in the graph composition itself and all of your subgraphs are automatically treated as extending these types to add the operations they support without explicitly adding the extends
keyword.
For example, the products
subgraph might extend the root Query
type to add a topProducts
query, like so:
type Query {
topProducts(first: Int = 5): [Product]
}
As your federated graph grows, you might decide that you want an entity (or a particular field of an entity) to originate in a different subgraph. This section describes how to perform these migrations.
Let's say our Payments subgraph defines a Bill
entity:
# Payments subgraph
type Bill @key(fields: "id") {
id: ID!
amount: Int!
}
type Payment {
# ...
}
Then, we add a dedicated Billing subgraph to our federated graph. It now makes sense for the Bill
entity to originate in the Billing subgraph instead. When we're done migrating, we want our deployed subgraph schemas to look like this:
# Payments subgraph
type Payment {
# ...
}
# Billing subgraph
type Bill @key(fields: "id") {
id: ID!
amount: Int!
}
The exact steps depend on how you perform schema composition:
-
In the Billing subgraph's schema, define the
Bill
entity just as it's defined in the Payments subgraph (do not extend it):# Payments subgraph type Bill @key(fields: "id") { id: ID! amount: Int! } type Payment { # ... }
# Billing subgraph type Bill @key(fields: "id") { id: ID! amount: Int! }
- Note that if you perform composition at this point, it produces an error because the
Bill
entity can't originate in more than one subgraph. That's okay! We aren't running composition yet, and we'll resolve this error in a couple of steps.
- Note that if you perform composition at this point, it produces an error because the
-
In the Billing subgraph, define resolvers for each field of
Bill
that currently originates in the Payments subgraph. This subgraph should resolve those fields with the exact same logic as the resolvers in the Payments subgraph. -
Deploy the updated Billing subgraph to your environment.
- Again, this technically deploys a composition error. However, your gateway isn't aware of this! It's still using the original supergraph schema, which indicates that
Bill
originates only in the Payments subgraph.
- Again, this technically deploys a composition error. However, your gateway isn't aware of this! It's still using the original supergraph schema, which indicates that
-
In the Payments subgraph, remove the
Bill
entity and its associated resolvers (do not deploy this change yet):# Payments subgraph type Payment { # ... }
# Billing subgraph type Bill @key(fields: "id") { id: ID! amount: Int! }
- This takes care of the composition error in your local environment.
-
Compose an updated supergraph schema with your usual configuration using
rover supergraph compose
.- This updated supergraph schema indicates that
Bill
originates in the Billing subgraph.
- This updated supergraph schema indicates that
-
Assuming CI completes successfully, deploy an updated version of your gateway with the new supergraph schema.
- When this deployment completes, the gateway begins resolving
Bill
fields in the Billing subgraph instead of the Payments subgraph.
- When this deployment completes, the gateway begins resolving
-
Deploy an updated version of your Payments subgraph without the
Bill
entity.- At this point it's safe to remove this definition, because the gateway is never resolving
Bill
with the Payments subgraph.
- At this point it's safe to remove this definition, because the gateway is never resolving
We're done! Bill
now originates in a new subgraph, and it was resolvable during each step of the migration process.
-
In the Billing subgraph's schema, define the
Bill
entity just as it's defined in the Payments subgraph (do not extend it):# Payments subgraph type Bill @key(fields: "id") { id: ID! amount: Int! } type Payment { # ... }
# Billing subgraph type Bill @key(fields: "id") { id: ID! amount: Int! }
-
Publish the updated Billing subgraph schema to Apollo.
- Note that you're publishing a composition error, because the
Bill
entity now originates in more than one subgraph. This creates a failed launch in Apollo Studio. That's okay! Apollo Uplink continues to serve the most recent valid supergraph schema to your gateway.
- Note that you're publishing a composition error, because the
-
In the Billing subgraph, define resolvers for each field of
Bill
that currently originates in the Payments subgraph. This subgraph should resolve those fields with the exact same logic as the resolvers in the Payments subgraph. -
Deploy the updated Billing subgraph to your environment.
- This change is invisible to your gateway, which is not yet aware that the Billing subgraph defines the
Bill
entity.
- This change is invisible to your gateway, which is not yet aware that the Billing subgraph defines the
-
In the Payments subgraph, remove the
Bill
entity and its associated resolvers:# Payments subgraph type Payment { # ... }
# Billing subgraph type Bill @key(fields: "id") { id: ID! amount: Int! }
-
Deploy the updated Payments subgraph to your environment.
- This resolves the composition error, because
Bill
now originates in a single subgraph. Apollo composes an updated supergraph schema, which your gateway automatically obtains when it polls the Apollo Uplink.
- This resolves the composition error, because
We're done! Bill
now originates in a new subgraph, and it was resolvable during each step of the migration process.
鈿狅笍 We strongly recommend against usingIntrospectAndCompose
in production. For details, see Limitations ofIntrospectAndCompose
.
When you provide IntrospectAndCompose
to ApolloGateway
, it performs composition itself on startup after fetching all of your subgraph schemas. If this runtime composition fails, the gateway fails to start up, resulting in downtime.
To minimize downtime for your graph, you need to make sure all of your subgraph schemas successfully compose whenever your gateway starts up. When migrating an entity, this requires a coordinated deployment of your modified subgraphs and a restart of the gateway itself.
-
In the Billing subgraph's schema, define the
Bill
entity just as it's defined in the Payments subgraph (do not extend it):# Payments subgraph type Bill @key(fields: "id") { id: ID! amount: Int! } type Payment { # ... }
# Billing subgraph type Bill @key(fields: "id") { id: ID! amount: Int! }
-
In the Billing subgraph, define resolvers for each field of
Bill
that currently originates in the Payments subgraph. This subgraph should resolve those fields with the exact same logic as the resolvers in the Payments subgraph. -
In the Payments subgraph's schema, remove the
Bill
entity and its associated resolvers:# Payments subgraph type Payment { # ... }
# Billing subgraph type Bill @key(fields: "id") { id: ID! amount: Int! }
-
Bring down all instances of your gateway in your deployed environment. This downtime prevents inconsistent behavior during a rolling deploy of your subgraphs.
-
Deploy the updated Payments and Billing subgraphs to your environment. When these deployments complete, bring your gateway instances back up and confirm that they start up successfully.
The steps for migrating an individual field are nearly identical in form to the steps for migrating an entire entity.
Let's say our Products subgraph defines a Product
entity, which includes the boolean field inStock
:
# Products subgraph
type Product @key(fields: "id") {
id: ID!
inStock: Boolean!
}
Then, we add an Inventory subgraph to our federated graph. It now makes sense for the inStock
field to originate in the Inventory subgraph instead, like this:
# Products subgraph
type Product @key(fields: "id") {
id: ID!
}
# Inventory subgraph
extend type Product @key(fields: "id") {
id: ID! @external
inStock: Boolean!
}
We can perform this migration with the following steps (additional commentary on each step is provided in Entity migration):
-
In the Inventory subgraph's schema, extend the
Product
entity to add theinStock
field:# Products subgraph type Product @key(fields: "id") { id: ID! inStock: Boolean! }
# Inventory subgraph extend type Product @key(fields: "id") { id: ID! @external inStock: Boolean! }
- If you're using managed federation, register this schema change with Apollo.
-
In the Inventory subgraph, add a resolver for the
inStock
field. This subgraph should resolve the field with the exact same logic as the resolver in the Products subgraph. -
Deploy the updated Inventory subgraph to your environment.
-
In the Products subgraph's schema, remove the
inStock
field and its associated resolver:# Products subgraph type Product @key(fields: "id") { id: ID! }
# Inventory subgraph extend type Product @key(fields: "id") { id: ID! @external inStock: Boolean! }
- If you're using managed federation, register this schema change with Studio.
-
If you're using Rover composition, compose a new supergraph schema. Deploy a new version of your gateway that uses the updated schema.
- Skip this step if you're using managed federation.
-
Deploy the updated Products subgraph to your environment.
When you extend an entity, you can define fields that depend on fields in the entity's originating subgraph. For example, a shipping
subgraph might extend the Product
entity with a shippingEstimate
field, which is calculated based on the product's size
and weight
:
extend type Product @key(fields: "sku") {
sku: ID! @external
size: Int @external
weight: Int @external
shippingEstimate: String @requires(fields: "size weight")
}
As shown, you use the @requires
directive to indicate which fields (and subfields) from the entity's originating subgraph are required.
You cannot require fields that are defined in a subgraph besides the entity's originating subgraph.
In the above example, if a client requests a product's shippingEstimate
, the gateway will first obtain the product's size
and weight
from the products
subgraph, then pass those values to the shipping
subgraph. This enables you to access those values directly from your resolver:
{
Product: {
shippingEstimate(product) {
return computeShippingEstimate(product.sku, product.size, product.weight);
}
}
}
If a computed field @requires
a field that returns an object type, you also specify which subfields of that object are required. You list those subfields with the following syntax:
extend type Product @key(fields: "sku") {
sku: ID! @external
dimensions: ProductDimensions @external
shippingEstimate: String @requires(fields: "dimensions { size weight }")
}
In this modification of the previous example, size
and weight
are now subfields of a ProductDimensions
object. Note that the ProductDimensions
object must be defined in both the entity's extending subgraph and its originating subgraph, either as an entity or as a value type.
Sometimes, multiple subgraphs are capable of resolving a particular field for an entity, because all of those subgraphs have access to a particular data store. For example, an inventory
subgraph and a products
subgraph might both have access to the database that stores all product-related data.
When you extend an entity in this case, you can specify that the extending subgraph @provides
the field, like so:
type InStockCount {
product: Product! @provides(fields: "name price")
quantity: Int!
}
extend type Product @key(fields: "sku") {
sku: String! @external
name: String @external
price: Int @external
}
This is a completely optional optimization. When the gateway plans a query's execution, it looks at which fields are available from each subgraph. It can then attempt to optimize performance by executing the query across the fewest subgraphs needed to access all required fields.
Keep the following in mind when using the @provides
directive:
- Each subgraph that
@provides
a field must also define a resolver for that field. That resolver's behavior must match the behavior of the resolver in the field's originating subgraph. - When an entity's field can be fetched from multiple subgraphs, there is no guarantee as to which subgraph will resolve that field for a particular query.
- If a subgraph
@provides
a field, it must still list that field as@external
, because the field originates in another subgraph.