Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clarify entity fetcher contract #192

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 42 additions & 17 deletions docs/federation.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

Federation is based on the [Federation spec](https://www.apollographql.com/docs/apollo-server/federation/federation-spec/).

A DGS is federation-compatible out of the box with the ability to reference and extend federated types.
Expand All @@ -7,7 +6,6 @@ A DGS is federation-compatible out of the box with the ability to reference and
* Read the [Federation Spec](https://www.apollographql.com/docs/graphos/reference/federation/subgraph-spec/).
* Check out [Federated Testing](./advanced/federated-testing.md) to learn how to write tests for federated queries.


## Federation Example DGS

This is a DGS example that demonstrates how to implement a federated type, and test federated queries.
Expand All @@ -20,16 +18,17 @@ The example project has the following set up:
2. The [Shows DGS](https://github.com/Netflix/dgs-federation-example/tree/master/shows-dgs) defines and owns the `Show` type.
3. The [Reviews DGS](https://github.com/Netflix/dgs-federation-example/tree/master/reviews-dgs) adds a `reviews` field to the `Show` type.


!!!info
If you are completely new to the DGS framework, please take a look at the [DGS Getting Started](./index.md) guide, which also contains an introduction video.
The remainder of the guide on this page assumes basic GraphQL and DGS knowledge, and focuses on more advanced use cases.

### Defining a federated type
The Shows DGS defines the `Show` type with fields id, title and releaseYear.
Note that the `id` field is marked as the key.

The Shows DGS defines the `Show` type with fields id, title and releaseYear.
Note that the `id` field is marked as the key.
The example has one key, but you can have multiple keys as well `@key(fields:"fieldA fieldB")`
This indicates to the gateway that the `id` field will be used for identifying the corresponding Show in the Shows DGS and must be specified for federated types.

```graphql
type Query {
shows(titleFilter: String): [Show]
Expand Down Expand Up @@ -58,6 +57,7 @@ type Review {
starRating: Int
}
```

When redefining a type, only the id field, and the fields you're adding need to be listed.
Other fields, such as `title` for `Show` type are provided by the Shows DGS and do not need to be specified unless you are using it in the schema.
Federation makes sure the fields provided by all DGSs are combined into a single type for returning the results of a query.
Expand All @@ -66,12 +66,15 @@ Federation makes sure the fields provided by all DGSs are combined into a single
Don't forget to use the @external directive if you define a field that doesn't belong to your DGS, but you need to reference it.

## Implementing a Federated Type

The very first step to get started is to generate Java types that represent the schema.
This is configured in `build.gradle` as described in the [manual](./generating-code-from-schema.md).
When running `./gradlew build` the Java types are generated into the `build/generated` folder, which are then automatically added to the classpath.

### Provide an Entity Fetcher

Let's go through an example of the following query sent to the gateway:

```graphql
query {
shows {
Expand All @@ -84,6 +87,7 @@ query {
```

The gateway first fetches the list of all the shows from the Shows DGS containing the title and id fields.

```graphql
query {
shows {
Expand All @@ -92,10 +96,10 @@ query {
title
}
}

```

Next, the gateway sends the following `_entities` query to the Reviews DGS using the list of `id`s from the first query:

```graphql
query($representations: [_Any!]!) {
_entities(representations: $representations) {
Expand All @@ -105,14 +109,15 @@ query($representations: [_Any!]!) {
}
}
}
}
}
```

This query comes with the following variables:

```json
{
"representations": [
{
"representations": [
{
"__typename": "Show",
"id": 1
},
Expand All @@ -133,22 +138,41 @@ This query comes with the following variables:
"__typename": "Show",
"id": 5
}
]
}
]
}
```

The Reviews DGS needs to implement an `entity fetcher` to handle this query.
An entity fetcher is responsible for creating an instance of a `Show` based on the representation in the `_entities` query above.
The DGS framework does most of the heavy lifting, and all we have to do is provide the following:
The Reviews DGS needs to implement an entity fetcher to handle this query.

- **Entity fetcher**: A method annotated with [`@DgsEntityFetcher`](https://javadoc.io/doc/com.netflix.graphql.dgs/graphql-dgs/latest/com/netflix/graphql/dgs/DgsEntityFetcher.html) that takes a key and returns a single instance of the entity or null.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what are the implications of returning the entity or null? which one is preferred?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it also makes sense to mention that the entity resolver must return some value for every key.


Our entity fetcher gets the `id` field and returns a `Show` instance:

[Full code](https://github.com/Netflix/dgs-federation-example/blob/master/reviews-dgs/src/main/java/com/example/demo/datafetchers/ReviewsDatafetcher.java)

```java
@DgsEntityFetcher(name = "Show")
public Show movie(Map<String, Object> values) {
return new Show((String) values.get("id"), null);
String showId = (String) values.get("id")
return new Show(showId, null);
}
```

In this case, we're not doing any data fetching: our `Show` instance only has an `id` field, and we implement a `Show.reviews` datafetcher in the [next section](#providing-data-with-a-data-fetcher).

However, we could instead fetch data in the entity fetcher. If our DGS served multiple fields, and they all came from the same data source, we could fetch them all at once in the entity fetcher instead of writing separate datafetchers for each field:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would leave this out from the docs - don't think we want to necessarily recommend this as a pattern.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So is it an anti-pattern to build the whole entity object? Should we always return the partial entity (only id field)?
If it is, maybe it makes sense to mentioned that it is not recommended.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO it's a valid thing to do, and sometimes the better thing to do, and many won't realize it's an option unless we say it is. Maybe can change/add language around it to say only use it in this circumstance, or "here is why you often don't want to do it?"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I guess if it's providing multiple federated fields - so would be good to highlight that.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 that this is a valid pattern


```java
@DgsEntityFetcher(name = "Show")
public Show movie(Map<String, Object> values, DataFetchingEnvironment env) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should be CompletableFuture<Show>

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? I don't think it needs to be?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't dataLoader.load(showId) return a Future?

DataLoader<String, Show> dataLoader = env.getDataLoader("shows");
String showId = (String) values.get("id")
return dataLoader.load(showId);
}
```

If there is no such Show with the given id in the database, the entity fetcher should return `null`.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

null or new Show(showId)?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe explain a bit further what this means for the federated request. E.g. will this show as an error


!!!tip
Remember that the Show Java type here is generated by codegen.
It's generated from the schema, so it only has the fields our schema specifies.
Expand All @@ -163,6 +187,7 @@ public Show movie(Map<String, Object> values) {
Now the DGS knows how to create a Show instance when an `_entities` query is received, we can specify how to hydrate data for the reviews field.

[Full code](https://github.com/Netflix/dgs-federation-example/blob/master/reviews-dgs/src/main/java/com/example/demo/datafetchers/ReviewsDatafetcher.java#L37)

```java
@DgsData(parentType = "Show", field = "reviews")
public List<Review> reviews(DgsDataFetchingEnvironment dataFetchingEnvironment) {
Expand All @@ -173,7 +198,7 @@ public List<Review> reviews(DgsDataFetchingEnvironment dataFetchingEnvironment)

### Testing a Federated Query

You can always manually test federated queries by running the gateway and your DGS locally.
You can always manually test federated queries by running the gateway and your DGS locally.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should flip this text around and focus on testing without running a gateway, so:

  1. Write a test
  2. Test by sending a federated query directly to the DGS (e.g. using graphiql)
  3. Run your gateway locally

You can also manually test a federated query against just your DGS, without the gateway, using the `_entities` query to replicate the call made to your DGS by the gateway.

For automated tests, the [QueryExecutor](./query-execution-testing.md) gives a way to run queries from unit tests, with very little startup overhead (in the order of 500ms).
Expand Down Expand Up @@ -257,7 +282,7 @@ public class FederationResolver extends DefaultDgsFederationResolver {
//The Show type is represented by the ShowId class.
types.put(ShowId.class, "Show");
}

@Override
public Map<Class<?>, String> typeMapping() {
return types;
Expand Down