2. Operation caching
5m

Overview

Before our server can resolve the data for a , it first needs to extract and validate the operation itself. This involves a few steps that must take place before any datafetching occurs.

In this lesson, we will:

  • Review how an is received, parsed, and validated by a
  • Learn about the PreparsedDocumentProvider interface
  • Discuss the best practice of using when caching s

GraphQL operations and the server

Let's start by reviewing what exactly happens behind the scenes when our server receives a .

The process begins when a client submits an in an HTTP POST or GET request. When our server receives the HTTP request, it first extracts the string with the operation. It parses and transforms it into something it can better manipulate: a tree-structured called an AST (Abstract Syntax Tree). With this AST, the server validates the against the types and in our schema.

If anything is off (e.g. a requested is not defined in the schema or the is malformed), the server throws an error and sends it right back to the app.

Hand-drawn illustration depicting server-land with the GraphQL server receiving a query and going through the necessary steps

If the looks good, the server proceeds to "execute" it: for each in the operation, the server invokes that field's datafetcher method. When all the fields have been resolved, the server packages up the data in a single JSON object that matches the shape of the original . And back to the client it goes!

This process is efficient, but what happens when the server receives the same more than once? Well, it does what it has always done: it extracts the operation, parses it into an AST, and validates the result against the schema. Only after these steps are taken care of does the server proceed with invoking each 's datafetcher method. This means even if the server has already parsed and validated a particular once or twice before, it will continue to repeat these steps for each new request.

We can improve upon this process with operation caching.

Caching operation strings

When we equip our server to cache the strings it receives, we can avoid repeating two potentially expensive steps in the server's process: parsing and validating the request string. By creating a cache specifically for our operations, the server can parse and validate a unique operation once, then refer to the cached when it receives the same in the future.

To bring this functionality into our DGS server, we'll use the Java library's PreparsedDocumentProvider interface. Before diving into our actual implementation, let's get an overview of how this interface works.

The PreparsedDocumentProvider interface

The PreparsedDocumentProvider interface works by storing (referred to within the interface as Documents) in a cache instance that we provide (more on that in the next lesson!).

Whenever our server receives an incoming string, the PreparsedDocumentProvider implementation uses its getDocumentAsync method to check whether the same already exists in the cache.

The PreparsedDocumentProvider interface from graphql-java
public interface PreparsedDocumentProvider {
CompletableFuture<PreparsedDocumentEntry> getDocumentAsync(
ExecutionInput executionInput,
Function<ExecutionInput, PreparsedDocumentEntry> parseAndValidateFunction);
}

The getDocumentAsync method takes two parameters: the first is the current "execution input", an object that contains all sorts of data about the being executed, including the string.

The second is a function, parseAndValidateFunction, which is called if the provided string is not present in the cache. This function takes care of the necessary parsing and validation steps before sending the operation on its way to the datafetcher methods. But in the process, the operation itself gets cached for next time!

The CachingPreparsedDocumentProvider class

Let's take care of creating the class that will implement our PreparsedDocumentProvider interface.

In the com.example.listings package, right alongside the ListingsApplication file, create a new file called CachingPreparsedDocumentProvider.java.

πŸ“¦ com.example.listings
┣ πŸ“‚ datafetchers
┣ πŸ“‚ dataloaders
┣ πŸ“‚ datasources
┣ πŸ“‚ models
┣ πŸ“„ CachingPreparsedDocumentProvider
┣ πŸ“„ ListingsApplication
β”— πŸ“„ WebConfiguration

To start, we can import the Spring framework's Component annotation, along with PreparsedDocumentProvider and PreparsedDocumentEntry from the graphql package. (We'll use PreparsedDocumentEntry shortly!)

import org.springframework.stereotype.Component;
import graphql.execution.preparsed.PreparsedDocumentProvider;
import graphql.execution.preparsed.PreparsedDocumentEntry;

Next, we'll update our class definition to implement the PreparsedDocumentProvider interface. And let's not forget to apply the @Component annotation so that this file is detected as a bean in our application (a class instance managed by the Spring container).

Here's how your class should look.

CachingPreparsedDocumentProvider
package com.example.listings;
import org.springframework.stereotype.Component;
import graphql.execution.preparsed.PreparsedDocumentProvider;
import graphql.execution.preparsed.PreparsedDocumentEntry;
@Component
public class CachingPreparsedDocumentProvider implements PreparsedDocumentProvider {
// TODO
}

Great! That's our boilerplate taken care of. You might be seeing some red squigglies right now, but not to worry: we'll build out the remainder of our class in the next lesson.

Caching tip: Use query variables

We consider the use of query variables a best practice in in general, but this convention becomes extra important when using the PreparsedDocumentProvider to cache .

Take a look at the following .

GetListingAndAmenities: listing-1
query GetListingAndAmenities {
listing(id: "listing-1") {
title
description
numOfBeds
amenities {
name
category
}
overallRating
reviews {
id
text
}
}
}

There's nothing wrong with this syntaxβ€”but stored in our cache, this is valid for one listing only: the listing with ID listing-1!

If we ran the same again, swapping in a different listing ID, we wouldn't see any benefits from caching the first . By defining its listing ID in-line, our cached operation is too specific: it'll save us time on the parsing and validation steps only when we for the exact same listing again.

GetListingAndAmenities: listing-2
query GetListingAndAmenities {
listing(id: "listing-2") {
title
description
numOfBeds
amenities {
name
category
}
overallRating
reviews {
id
text
}
}
}

With , we can abstract away the specifics and focus instead on the generic : the it includes and the names of the involved.

GetListingAndAmenities, with a generic $listingId variable
query GetListingAndAmenities($listingId: ID!) {
listing(id: $listingId) {
title
description
numOfBeds
amenities {
name
category
}
overallRating
reviews {
id
text
}
}
}

Now if our client were to send this twice, one for listing-1 and another for listing-2, we'll see the benefits of caching: the second time the server receives a request, the parsing and validation steps will be skipped.

Note: What about the actual values passed at runtime? They can be found on the execution input object that gets passed into the interface's getDocumentAsync method. We'll explore this object in the next lesson.

Practice

Which of these are actions that our GraphQL server takes when it receives a request?
Which of the following is a reason it's important to use query variables when caching operations?

Key takeaways

  • We can implement the PreparsedDocumentProvider interface to do the following:
    • Retrieve that have already been parsed and validated from a cache
    • Parse and validate that are not yet in the cache, caching them in the process
  • By using (rather than in-line values), we can store generic in the cache that can be retrieved and used for multiple different values. (For instance, one operation can serve up data for any listing ID we provide it!)

Up next

In the next lesson, we'll finish our class implementation by adding our methods and a high-performance cache implementation.

Previous

Share your questions and comments about this lesson

This course is currently in

beta
. Your feedback helps us improve! If you're stuck or confused, let us know and we'll help you out. All comments are public and must follow the Apollo Code of Conduct. Note that comments that have been resolved or addressed may be removed.

You'll need a GitHub account to post below. Don't have one? Post in our Odyssey forum instead.