Skip to content

Commit

Permalink
Merge pull request #2771 from apollographql/ms/query-planner-keys-note
Browse files Browse the repository at this point in the history
docs: add note about query planner key usage
  • Loading branch information
Meschreiber authored Sep 11, 2023
2 parents 2caf312 + 272311d commit 621b93c
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 42 deletions.
104 changes: 64 additions & 40 deletions docs/source/entities-advanced.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,41 @@ title: Advanced topics on federated entities

This article describes complex behaviors of federated entities beyond those covered in [entity basics](./entities/).

## Advanced `@key`s
## Using advanced `@key`s

A single entity can have multiple `@key`s. Additionally, a `@key` can include multiple fields, and even arbitrarily nested fields.
Depending on your entities' fields and usage, you may need to use more advanced `@key`s. For example, you may need to define a [compound `@key`](#compound-keys) if multiple fields are required to uniquely identify an entity. If different subgraphs interact with different fields an entity, you may need to define [multiple](#multiple-keys)—and sometimes [differing](#differing-keys-across-subgraphs)—`@key`s for the entity.

### Compound `@key`s

A single `@key` can consist of multiple fields, the combination of which uniquely identifies an entity. This is called a **compound** or composite key. In the following example, the combination of both `username` and `domain` fields is required to uniquely identify the `User` entity:

```graphql {1} title="Users subgraph"
type User @key(fields: "username domain") {
username: String!
domain: String!
}
```

#### Nested fields in compound `@key`s

Compound keys can also include _nested_ fields. In the following example, the `User` entity's primary key consists of both a user's `id` _and_ the `id` of that user's associated `Organization`:

```graphql {1} title="Users subgraph"
type User @key(fields: "id organization { id }") {
id: ID!
organization: Organization!
}

type Organization {
id: ID!
}
```

### Multiple `@key`s

You can define more than one `@key` for an entity, when applicable.
When different subgraphs interact with different fields of an entity, you may need to define multiple `@key`s for the entity. For example, a Reviews subgraph might refer to products by their ID, whereas an Inventory subgraph might use SKUs.

In this example, a `Product` entity can be uniquely identified by either its `id` _or_ its `sku`:
In the following example, the `Product` entity can be uniquely identified by _either_ its `id` _or_ its `sku`:

```graphql {1} title="Products subgraph"
type Product @key(fields: "id") @key(fields: "sku") {
Expand All @@ -22,10 +48,34 @@ type Product @key(fields: "id") @key(fields: "sku") {
price: Int
}
```
<blockquote>

**Note:** If you include multiple sets of `@key` fields, the query planner uses the most efficient set for entity resolution. For example, suppose you allow a type to be identified by `@key(fields: "id")` _or_ `@key(fields: "id sku")`:

```graphql {1}
type Product @key(fields: "id") @key(fields: "id sku") {
# ...
}
```

That means either `id` or (`id` _and_ `sku`) is enough to uniquely identify the entity. Since `id` alone is enough, the query planner will use only that field to resolve the entity, and `@key(fields: "id sku")` is effectively ignored.

</blockquote>

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 ID, whereas an Inventory subgraph might use SKUs.
#### Referencing entities with multiple keys

A subgraph that [references an entity without contributing any fields](./entities/#referencing-an-entity-without-contributing-fields) can include the fields of any `@key` in its stub definition:
A subgraph that [references an entity without contributing any fields](./entities/#referencing-an-entity-without-contributing-fields) can use any `@key` fields in its stub definition. For example, if the Products subgraph defines the `Product` entity like this:

```graphql {1} title="Products subgraph"
type Product @key(fields: "id") @key(fields: "sku") {
id: ID!
sku: String!
name: String!
price: Int
}
```

Then, a Reviews subgraph can use either `id` or `sku` in the stub definition:

```graphql title="Reviews subgraph"
# Either:
Expand All @@ -40,28 +90,9 @@ type Product @key(fields: "sku", resolvable: false) {

```

### Compound `@key`s

A single `@key` can consist of multiple fields, and even _nested_ fields.

In this example, the `User` entity's primary key consists of both a user's `id` _and_ the `id` of that user's associated `Organization`:

```graphql {1} title="Users subgraph"
type User @key(fields: "id organization { id }") {
id: ID!
organization: Organization!
}

type Organization {
id: ID!
}
```

### Differing `@key`s across subgraphs

An entity often has the exact same `@key` field(s) across subgraphs, but this isn't required. For example, you can define a `Product` entity shared between subgraphs, one with `sku` and `upc` as the `@key`s and one with only `upc` as the `@key` field:

<CodeColumns>
Although an entity commonly uses the exact same `@key` field(s) across subgraphs, you can alternatively use different `@key`s with different fields. For example, you can define a `Product` entity shared between subgraphs, one with `sku` and `upc` as its `@key`s, and the other with only `upc` as the `@key` field:

```graphql title="Products subgraph"
type Product @key(fields: "sku") @key(fields: "upc") {
Expand All @@ -79,9 +110,6 @@ type Product @key(fields: "upc") {
}
```

</CodeColumns>


To merge entities between subgraphs, the entity must have at least one shared field between subgraphs. For example, operations can't merge the `Product` entity defined in the following subgraphs because they don't share any fields specified in the `@key` selection set:

<p style="margin-bottom: 0">❌</p>
Expand All @@ -105,9 +133,9 @@ type Product @key(fields: "upc") {

</CodeColumns>

#### Operations with mismatched `@key`s
#### Operations with differing `@key`s

Mismatched keys affect which fields from an entity can be resolved. Requests can resolve an entity's fields _if there is a traversable path from the root query to the fields_.
Differing keys across subgraphs affect which of the entity's fields can be resolved from each subgraph. Requests can resolve fields **if there is a traversable path from the root query to the fields**.

Take these subgraph schemas as an example:

Expand Down Expand Up @@ -136,9 +164,9 @@ type Product @key(fields: "upc") {
```
</CodeColumns>

The queries defined in the products subgraph can always resolve all product fields because the product entity can be joined via the `upc` field present in both schemas.
The queries defined in the Products subgraph can always resolve all product fields because the product entity can be joined via the `upc` field present in both schemas.

On the other hand, queries added to the inventory subgraph can't resolve fields from the products subgraph:
On the other hand, queries added to the Inventory subgraph can't resolve fields from the Products subgraph:

<CodeColumns>

Expand All @@ -164,11 +192,9 @@ type Query {

</CodeColumns>

The `productsInStock` query can't resolve fields from the product subgraph since its `Product` type definition doesn't have `@key(fields: "upc")`, and the `sku` field isn't present in the inventory subgraph.
The `productsInStock` query can't resolve fields from the Products subgraph since the Products subgraph's `Product` type definition doesn't include `upc` as a key field, and `sku` isn't present in the Inventory subgraph.

If the product subgraph includes `@key(fields: "upc")`, all queries from the inventory subgraph can resolve all product fields:

<CodeColumns>
If the Products subgraph includes `@key(fields: "upc")`, all queries from the Inventory subgraph can resolve all product fields:

```graphql title="Products subgraph"
type Product @key(fields: "sku") @key(fields: "upc") {
Expand All @@ -190,8 +216,6 @@ type Query {
}
```

</CodeColumns>

## Migrating entities and fields

As your supergraph grows, you might want to move parts of an entity to a different subgraph. This section describes how to perform these migrations safely.
Expand Down Expand Up @@ -627,4 +651,4 @@ const resolvers = {

A basic implementation of the `fetchProductByID` function might make a database call each time it's called. If we need to resolve `Product.price` for `N` different products, this results in `N` database calls. These calls are made _in addition to_ the call made by the Reviews subgraph to fetch the initial list of reviews (and the `id` of each product). This is where the "N+1" problem gets its name. If not prevented, this problem can cause performance problems or even enable denial-of-service attacks.

This problem is not limited to reference resolvers! In fact, it can occur with any resolver that fetches from a data store. To handle this problem, we strongly recommend using [the dataloader pattern](https://github.com/graphql/dataloader). Nearly every GraphQL server library provides a dataloader implementation, and you should use it in **every resolver**. This is true even for resolvers that _aren't_ for entities and that _don't_ return a list. These resolvers can _still_ cause N+1 issues via [batched requests](/tutorials/voyage-part2/02-monolith-graph-setup).
This problem is not limited to reference resolvers! In fact, it can occur with any resolver that fetches from a data store. To handle this problem, we strongly recommend using [the dataloader pattern](https://github.com/graphql/dataloader). Nearly every GraphQL server library provides a dataloader implementation, and you should use it in **every resolver**. This is true even for resolvers that _aren't_ for entities and that _don't_ return a list. These resolvers can _still_ cause N+1 issues via [batched requests](/technotes/TN0021-graph-security/#batched-requests).
2 changes: 2 additions & 0 deletions docs/source/entities.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ type Product @key(fields: "id") {
> * Fields that return a union or interface
> * Fields that take arguments

Though not strictly required, it's best to use non-nullable fields for keys. If you use fields that return `null` values, GraphOS may encounter issues resolving the entity.

For more information on advanced key options, like how to define [multiple keys](./entities-advanced/#multiple-keys) or [compound keys](/entities-advanced/#compound-keys), see [Advanced topics for federation entities](./entities-advanced).

### 2. Define a reference resolver
Expand Down
2 changes: 1 addition & 1 deletion docs/source/federated-types/federated-directives.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ Examples:
* `"username region"`
* `"name organization { id }"`

See also [Advanced `@key`s](../entities-advanced/#advanced-keys).
See also [Advanced `@key`s](../entities-advanced/#using-advanced-keys).

</td>
</tr>
Expand Down
2 changes: 1 addition & 1 deletion docs/source/federation-2/new-in-federation-2.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ type Bill @key(fields: "id") {

</CodeColumns>

For details, see [Entity migration](../entities-advanced/#entity-migration).
For details, see [Entity migration](../entities-advanced/#migrating-entities-and-fields).

## Interfaces implementing interfaces

Expand Down

0 comments on commit 621b93c

Please sign in to comment.