Overview
Even though we've defined the Listing
entity in our reviews
subgraph and contributed fields to it, we haven't told it how to resolve these fields.
In this lesson, we will:
- Create a reference resolver
- Populate the
reviews
andoverallRating
fields with data - Learn about the
@DgsEntityFetcher
annotation
The reference resolver
When we query for a particular listing and its review data, we need a way to tell the reviews
subgraph which listing we're talking about. Then the reviews
subgraph will understand what data to provide!
To do exactly this, the router passes along the entity representation we talked about in the last lesson: the minimum information required for the reviews
subgraph to put together some idea of which listing it's fetching data for.
{"__typename": "Listing","id": "listing-3"}
But where in the reviews
subgraph does this entity representation actually go?
This is just the piece we're missing: the reference resolver. The reference resolver is the special method that can receive an entity representation from the router, create a new instance of a Listing
from that representation data, and return it.
Let's see what this looks like in code, and walk through the process step-by-step.
Adding resolveListingReference
Let's open up our datafetcher class in the reviews
subgraph, under com.example.reviews/datafetchers/ReviewsDataFetcher
.
Right away, let's take care of adding some imports at the top of the file. We'll use these soon.
import java.util.Map;import com.example.reviews.generated.types.Listing;
Note: Not seeing the Listing
type in your generated
folder? Try restarting the server!
Make some space down in the body of the class for our new method. To be extra clear about its job, we'll give it the name resolveListingReference
. (But you can call this method whatever you want!)
public void resolveListingReference() {// TODO}
This method will receive the entity representation, which looks something like the following snippet: just the __typename
and the field we set as the entity's primary key, id
.
{__typename=Listing, id=listing-1}
Let's give our method a parameter to hold this representation. It's considered a Map<String, Object>
type, and we'll call it entityRepresentation
. Let's print out entityRepresentation.toString()
so we can see what this method receives.
public void resolveListingReference(Map<String, Object> entityRepresentation) {System.out.println(entityRepresentation.toString());}
The @DgsEntityFetcher
annotation
In order for our DGS server to know that this is the method that should receive an entity from the router, we need to mark it with a specific annotation: @DgsEntityFetcher
.
This annotation has a property called name
, which we'll use to define the name of the entity that it resolves: Listing
!
Let's apply this annotation to our method.
@DgsEntityFetcher(name = "Listing")public void resolveListingReference(Map<String, Object> entityRepresentation) {System.out.println(entityRepresentation.toString());}
Now it's extra clear that when the router brings a Listing
entity representation to our reviews
subgraph, this is the method that we'll use to resolve which listing we need to provide data for!
Go back to your terminal and restart the reviews
server. Our rover dev
process should still be running on port 4000
, so let's return there and try out our query again.
query GetListingAndReviews {listing(id: "listing-1") {titledescriptionnumOfBedsamenities {namecategory}overallRatingreviews {idtext}}}
When we run the query, we still won't get any review-related data back; but when we check the terminal of our reviews
subgraph, we'll see that our entity representation has arrived and is printed out!
{__typename=Listing, id=listing-1}
Our reviews
subgraph is successfully receiving the listing representation from the router. Now, we just need to make sure our method returns a new Listing
instance that other datafetcher methods can access and attach data to.
Delete the print statement, and replace it by instantiate a new Listing
instance. We can also update our method's return type to Listing
.
@DgsEntityFetcher(name = "Listing")public Listing resolveListingReference(Map<String, Object> entityRepresentation) {Listing listing = new Listing();}
Next, let's pull the "id"
value from our entityRepresentation
. We can cast it as a String
type with the following line.
String id = (String) entityRepresentation.get("id");
Lastly, we'll set this as the id
on our listing
, and return it.
listing.setId(id);return listing;
Great! We can consider our reference to the listing "resolved". We're ready to provide those review-relevant fields—reviews
and overallRating
—with their own datafetcher methods.
Adding Listing.reviews
Let's tackle the reviews
method first.
Inside the ReviewsDataFetcher
class, and let's define a new method called reviews
. This is a regular datafetcher, so we'll use the @DgsData
annotation, setting our parentType
as Listing
.
@DgsData(parentType="Listing")public void reviews() {}
In our GraphQL schema, the Listing.reviews
field returns a type of [Review!]!
. So let's update our method with the corresponding Java return type of Flux<ReviewDto>
.
public Flux<ReviewDto> reviews() {}
This method should use the id
from the entity representation to find and return all the relevant reviews in the database. Because this datafetcher resolves a field on an entity type, we know that the previous datafetcher in the chain was the resolveListingReference
method we just defined. This means we can access its return value (the Listing
instance) using the DgsDataFetchingEnvironment
parameter.
Let's bring that into our method as a parameter called dfe
.
public Flux<ReviewDto> reviews(DgsDataFetchingEnvironment dfe) {}
We can call the getSource
method on dfe
to get access to the Listing
instance; then call the listing's getId
method to access its id
value.
public Flux<ReviewDto> reviews(DgsDataFetchingEnvironment dfe) {Listing listing = dfe.getSource();String id = listing.getId();}
With our listing id
in hand, we can look up all of the reviews in our database that are associated with that listing. We're using our ReviewController
, which is already instantiated on our class, to access our in-memory data source. It provides a mapping for reviewsForListing
, so let's call that method here and pass in our id
.
return this.reviewController.reviewsForListing(id);
The overallRating
method
Onto the overallRating
datafetcher method! Let's set up the initial structure with the @DgsData
annotation.
@DgsData(parentType="Listing")public void overallRating() {// TODO}
Try this one out on your own, and check out the ReviewController
class for a helpful method you can use to retrieve this data. (Hint: Check out its return type for a helpful cue on what our method should return!)
When you're ready, compare your code to our finished method below.
@DgsData(parentType="Listing")public Mono<Float> overallRating(DgsDataFetchingEnvironment dfe) {Listing listing = dfe.getSource();String id = listing.getId();return this.reviewController.averageRatingForListing(id);}
Note: Because we're coding in the reactive style, we've used the Flux
type so far to describe the stream of data that we return. However, since we're returning just a single value—the average of a listing's ratings, returned as a Float
—we can use the Mono
type instead and pass it the type variable of Float
.
Running our dream query
Time to recompile!
With the rover dev
process still running, let's try out our query at http://localhost:4000
.
query GetListingAndReviews {listing(id: "listing-1") {titledescriptionnumOfBedsamenities {namecategory}overallRatingreviews {idtext}}}
Submit the query, and... we've got data! 🎉 We've associated reviews with a listing and made our dream query come to life!
Reviewing the query plan
Let's check out the query plan to see how this data came together. We'll see that first, the router will fetch data from listings
, and then use that data to build out its request to reviews
. The last step is flattening the response from both subgraphs into a single instance of a Listing
type for the listing
field we've queried!
Still seeing reviews: null
? Try restarting your DGS server! The rover dev
process running our router on port 4000
will automatically refresh.
Practice
Key takeaways
- Any subgraph that contributes fields to an entity needs to define a reference resolver method for that entity. This method is called whenever the router needs to access fields of the entity from within another subgraph.
- In DGS, we denote a method as a reference resolver using the
@DgsEntityFetcher
annotation, passing in thename
of the type it resolves. This method receives an entity representation from the router. - An entity representation is an object that the router uses to represent a specific instance of an entity. It includes the entity's type and its key field(s).
Up next
Awesome! Our listings
and reviews
services are now collaborating on the same Listing
entity. Each subgraph contributes its own fields, and the router launched by our local rover dev
process packages up the response for us. Emphasis on the word local; to get our changes actually "live" (at least in the tutorial sense of the word), we need to tell GraphOS about them!
In the next lesson, we'll take a look at how we can land these changes safely and confidently using schema checks and launches.
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.