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
reviewsandoverallRatingfields with data - Learn about the
@DgsEntityFetcherannotation
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
@DgsEntityFetcherannotation, passing in thenameof 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.