Docs
Launch GraphOS Studio
You're viewing documentation for a previous version of this software. Switch to the latest stable version.

Entities in Apollo Federation

Reference and extend types across subgraphs


In , an entity is an that you define canonically in one and can then reference and extend in other .

Entities are the core building block of a federated .

Defining entities

In a , you can designate any as an by adding a @key to its definition, like so:

products
type Product @key(fields: "upc") {
upc: String!
name: String!
price: Int
}

Types besides (such as unions and interfaces) cannot be entities.

The @key defines the 's primary key, which consists of one or more of the type's fields. Like primary keys in other systems, an 's primary key must uniquely identify a particular instance of that .

In the example above, the Product 's primary key is its upc . The gateway uses an 's primary key to match data from different to the same object instance.

An 's @key cannot include that return a union or interface.

Multiple primary keys

You can define more than one primary key for an , when applicable.

In the following example, a Product can be uniquely identified by either its upc or its sku:

products
type Product @key(fields: "upc") @key(fields: "sku") {
upc: String!
sku: String!
price: String
}

This pattern is helpful when different interact with different of an . For example, a reviews might refer to products by their UPC, whereas an inventory might use SKUs.

Compound primary keys

A single primary key can consist of multiple , 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:

directory
type User @key(fields: "id organization { id }") {
id: ID!
organization: Organization!
}
type Organization {
id: ID!
}

Referencing entities

After you define an in one , other can then reference that in their schema.

For example, let's say we have a products that defines the following Product :

products
type Product @key(fields: "upc") {
upc: String!
name: String!
price: Int
}

A reviews can then add a of type Product to its Review type, like so:

reviews
type Review {
score: Int!
product: Product!
}
# This is a required "stub" of the Product entity (see below)
extend type Product @key(fields: "upc") {
upc: String! @external
}

To reference an that originates in another , the reviews needs to define a stub of that to make its own schema valid. The stub includes just enough information for the to know how to uniquely identify a particular Product:

  • The extend keyword indicates that Product is an that's defined in another .
  • The @key indicates that Product uses the upc as its primary key. This value must match the value of exactly one @key defined in the 's originating (even if the defines
    multiple primary keys
    ).
  • The upc must be present because it's part of the specified @key. It also requires the @external to indicate that it originates in another .

This explicit syntax has several benefits:

  • It's standard grammar.
  • It enables you to run the reviews standalone with a valid schema, including a Product type with a single upc .
  • It provides strong typing information that lets you catch mistakes at schema time.

Resolving entities

Let's say our reviews from

defines the following Query type:

reviews
type Query {
latestReviews: [Review!]
}

That means the following is valid against our federated :

query GetReviewsAndProducts {
latestReviews {
score
product {
upc
price # Not defined in reviews!
}
}
}

Now we have a problem: this starts its execution in the reviews (where latestReviews is defined), but that doesn't know that Product entities have a price ! Remember, the reviews only knows about its stub of Product.

Because of this, the gateway needs to fetch price from the products instead. But how does the gateway know which products it needs to fetch the prices for?

To solve this, we add a to each :

Entity representations

In our example, the reviews needs to define a for its stub version of the Product . The reviews doesn't know much about Products, but fortunately, it doesn't need to. All it needs to do is return data for the it does know about, like so:

resolvers.js
// Reviews subgraph
const resolvers = {
Review: {
product(review) {
return {
__typename: "Product",
upc: review.upc
};
}
},
// ...
}

This 's return value is a representation of a Product (because it represents an from another ). A representation always consists of:

  • A __typename
  • Values for the 's primary key (upc in this example)

Because an can be uniquely identified by its primary key , this is all the information the gateway needs to fetch additional fields for a Product object.

Reference resolvers

As a reminder, here's the example we're executing across our :

query GetReviewsAndProducts {
latestReviews {
score
product {
upc
price # Not defined in reviews!
}
}
}

The gateway knows it can't fetch Product.price from the reviews , so first it executes the following on reviews:

query {
latestReviews {
score
product { # List of Product representations
__typename
upc
}
}
}

Notice that this omits price but adds __typename, even though it wasn't in the original string! This is because the gateway knows it needs all of the in each Product's representation, including __typename.

With these representations available, the gateway can now execute a second on the products to fetch each product's price. To support this special , the products needs to define a reference resolver for the Product :

resolvers.js
// Products subgraph
const resolvers = {
Product: {
__resolveReference(reference) {
return fetchProductByUPC(reference.upc);
}
},
// ...
}

In the example above, fetchProductByUPC is a hypothetical function that fetches a Product's full details from a data store based on its upc.

A reference (always called __resolveReference) provides the gateway direct access to a particular 's , without needing to use a custom to reach that entity. To use a reference , the gateway must provide a valid

, which is why we created the in the reviews first!

To learn more about __resolveReference, see the

.

After fetching the price from products via a reference , the gateway can intelligently merge the data it obtained from its two queries into a single result and return that result to the client.

Extending entities

A can add to an that's defined in another subgraph. This is called extending the .

When a extends an , the entity's originating is not aware of the added . Only the extending (along with the gateway) knows about these .

Each of an should be defined in exactly one . Otherwise, a schema error will occur.

Example #1

Let's say we want to add a reviews to the Product . This will return a list of reviews for the product. The Product originates in the products , but it makes more sense for the reviews to resolve this particular .

To handle this case, we can extend the Product in the reviews , like so:

reviews
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

. All we've added is the reviews . We don't include an @external , because this does originate in the reviews .

Whenever a extends an with a new , it's also responsible for resolving that . The gateway is automatically aware of this responsibility. In our example:

  1. The gateway first fetches the upc for each Product from the products .
  2. The gateway then passes those upc values to the reviews , where you can access them on the object passed to your Product.reviews :
{
Product: {
reviews(product) {
return fetchReviewsForProduct(product.upc);
}
}
}

Example #2

Let's say we want to be able to for the inStock status of a product. That information lives in an inventory , so we'll add the type extension there:

inventory
extend type Product @key(fields: "upc") {
upc: ID! @external
inStock: Boolean
}
{
Product: {
inStock(product): {
return fetchInStockStatusForProduct(product.upc);
}
}
}

Similar to the previous example, the gateway fetches the required upc from the products and passes it to the inventory , even if the didn't ask for the upc:

# This query fetches upc from the products subgraph even though
# it isn't a requested field. Otherwise, the inventory subgraph
# can't know which products to return the inStock status for.
query GetTopProductAvailability {
topProducts {
name
inStock
}
}

The Query and Mutation types

In , the Query and Mutation base types originate in the itself and all of your are automatically treated as

these types to add the they support without explicitly adding the extends keyword.

For example, the products might extend the root Query type to add a topProducts , like so:

products
type Query {
topProducts(first: Int = 5): [Product]
}

Migrating entities and fields (advanced)

As your federated grows, you might decide that you want an (or a particular of an entity) to originate in a different . This section describes how to perform these migrations.

Entity migration

Let's say our Payments defines a Bill :

# Payments subgraph
type Bill @key(fields: "id") {
id: ID!
amount: Int!
}
type Payment {
# ...
}

Then, we add a Billing to our federated graph. It now makes sense for the Bill to originate in the Billing instead. When we're done migrating, we want our deployed 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 :

Field migration

The steps for migrating an individual are nearly identical in form to the steps for

.

Let's say our Products defines a Product , which includes the boolean inStock:

# Products subgraph
type Product @key(fields: "id") {
id: ID!
inStock: Boolean!
}

Then, we add an Inventory to our federated graph. It now makes sense for the inStock to originate in the Inventory 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

):

  1. In the Inventory 's schema,

    the Product to add the inStock :

    # 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 , register this schema change with Apollo.
  2. In the Inventory , add a for the inStock . This should resolve the field with the exact same logic as the in the Products subgraph.

  3. Deploy the updated Inventory to your environment.

  4. In the Products 's schema, remove the inStock and its associated :

    # 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 , register this schema change with Studio.
  5. If you're using Rover composition, compose a new . Deploy a new version of your gateway that uses the updated schema.

    • Skip this step if you're using .
  6. Deploy the updated Products to your environment.

Extending an entity with computed fields (advanced)

When you

, you can define that depend on fields in the 's originating . For example, a shipping might extend the Product with a shippingEstimate , which is calculated based on the product's size and weight:

shipping
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 to indicate which (and subfields) from the 's originating are required.

You cannot require that are defined in a besides the '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 , then pass those values to the shipping . This enables you to access those values directly from your :

{
Product: {
shippingEstimate(product) {
return computeShippingEstimate(product.sku, product.size, product.weight);
}
}
}

Using @requires with object subfields

If a computed @requires a that returns an , you also specify which subfields of that object are required. You list those sub with the following syntax:

shipping
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 sub of a ProductDimensions object. Note that the ProductDimensions object must be defined in both the 's extending and its originating , either as an or as a

.

Resolving another subgraph's field (advanced)

Sometimes, multiple are capable of resolving a particular for an , because all of those subgraphs have access to a particular data store. For example, an inventory and a products might both have access to the database that stores all product-related data.

When you

in this case, you can specify that the extending @provides the , like so:

inventory
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 's execution, it looks at which are available from each . 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 :

  • Each that @provides a must also define a for that field. That resolver's behavior must match the behavior of the resolver in the field's originating subgraph.
  • When an 's can be fetched from multiple , there is no guarantee as to which will resolve that for a particular .
  • If a @provides a , it must still list that field as @external, because the originates in another .
Previous
The gateway
Next
Reusing types (value types)
Edit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc.

Privacy Policy

Company