Caching AEM GraphQL queries with content fragment


Problem Statement:

How can I persist query?

How to cache my query results?
How to Update my queries?


Requirement:

Provide details on how to add the persist graphql query, cache the results from graphql and update the persisted query

Provide curl commands to execute in terminal or on postman


Introduction:

Persisted Queries (Caching)

After preparing a query with a POST request, it can be executed with a GET request that can be cached by HTTP caches or a CDN.

This is required as POST queries are usually not cached, and if using GET with the query as a parameter there is a significant risk of the parameter becoming too large for HTTP services and intermediates.

Persisted queries must always use the endpoint related to the appropriate Sites configuration; so, they can use either, or both:

  • Specific Sites configuration and endpoint

Creating a persisted query for a specific Sites configuration requires a corresponding Sites-configuration-specific endpoint (to provide access to the related Content Fragment Models).

For example, to create a persisted query specifically for the SampleGraphQL Sites configuration:

a corresponding SampleGraphQL-specific Sites configuration

  • Go to the tools section for the aem and general section and select Configuration Browser as shown below
Configuration browser
  • Add select the conf folder and go to the properties and make GraphQL Persistent Queries checkbox is checked
Enable persistent queries

a SampleGraphQL-specific endpoint must be created in advance.

  • Go to tools section for the aem and assets section and select GraphQL as shown below
assets -> graphql
  • Add the new end point as shown below:
endpoint

Add the following CORS configurations for the GraphQL API calls:

CORS config

Register graphql search path:

Register Servlet path

Here are the steps required to persist a given query:

Prepare the query by putting it to the new endpoint URL /graphql/persist.json/<config>/<persisted-label>.

For example, create a persisted query:

curl -u admin:admin -X PUT 'http://localhost:4502/graphql/persist.json/SampleGraphQL/cities' \
--header 'Content-Type: application/json' \
--data-raw '{
  cityList {
    items {
      _path
      name
      country
      population
    }
  }
}'
  • At this point, check the response.

For example, check for success:

{
    "action": "create",
    "configurationName": "SampleGraphQL",
    "name": "cities",
    "shortPath": "/SampleGraphQL/cities",
    "path": "/conf/SampleGraphQL/settings/graphql/persistentQueries/cities"
}

You can then replay the persisted query by getting the URL /graphql/execute.json/<shortPath>.

For example, use the persisted query:

curl -u admin:admin -X GET 'http://localhost:4502/graphql/execute.json/SampleGraphQL/cities' \
--header 'Authorization: Basic YWRtaW46YWRtaW4='

Update a persisted query by POSTing to an already existing query path.

For example, use the persisted query:

curl -u admin:admin -X POST 'http://localhost:4502/graphql/persist.json/SampleGraphQL/cities' \
--header 'Content-Type: application/json' \
--data-raw '{
  cityList {
    items {
      _path
      name
      country
      population
    }
  }
}'

Create a wrapped plain query.

For example:

curl -u admin:admin -X PUT 'http://localhost:4502/graphql/persist.json/SampleGraphQL/plain-cities' \
--header 'Content-Type: application/json' \
--data-raw '{ "query": "{cityList { items { _path name country country population } } }"}'

Create a wrapped plain query with cache control.

For example:

curl -u admin:admin -X PUT 'http://localhost:4502/graphql/persist.json/SampleGraphQL/plain-cities-max-age' \
--header 'Content-Type: application/json' \
--data-raw '{ "query": "{cityList { items { _path name country country population } } }", "cache-control": { "max-age": 300 }}'

Create a persisted query with parameters:

For example:

curl -u admin:admin -X PUT 'http://localhost:4502/graphql/persist.json/SampleGraphQL/plain-cities-query-parameters' \
--header 'Content-Type: application/json' \
--data-raw \
'query GetAsGraphqlModelTestByPath($apath: String!) {
    cityByPath(_path: $apath) {
        item {
        _path
        name
        country
        population
        }
    }
  }'

Executing a query with parameters.

For example:

curl -u admin:admin -X POST \
    -H "Content-Type: application/json" \
    "http://localhost:4502/graphql/execute.json/SampleGraphQL/plain-cities-query-parameters;apath=%2Fcontent%2Fdam%2Fsample-content-fragments%2Fcities%2Fberlin"

curl -u admin:admin -X GET \
    "http://localhost:4502/graphql/execute.json/SampleGraphQL/plain-cities-query-parameters;apath=%2Fcontent%2Fdam%2Fsample-content-fragments%2Fcities%2Fberlin"

AEM Resource Resolver Commit/Save – Best Practices

Problem Statement:

When to commit or save node (Resource) using resource resolver? Is it good save node inside a loop?

Requirement:

Save 50 nodes with some properties

Introduction:

The ResourceResolver defines the API which may be used to resolve Resource objects and work with such resources as creating, editing or updating them. The resource resolver is available to the request processing servlet through the SlingHttpServletRequest.getResourceResolver() method. A resource resolver can also be created through the ResourceResolverFactory service.

A ResourceResolver is generally not thread safe! As a consequence, an application that uses the resolver, its returned resources and/or objects resulting from adapting either the resolver or a resource, must provide proper synchronization to ensure no more than one thread concurrently operates against a single resolver, resource or resulting objects.

An algorithm is used to resolve and getResource and provide various methods to manage resources like:

OperationDescription
Create(Resource, String, Map)for creating a new resource.
Delete(Resource)to delete a resource.
Adaptable.adaptTo(Class)allows to adapt a resource to a ModifiableValueMap to update a resource.
Move(String, String)to move resources.
Copy(String, String)to copy resources.
Commit()commits all staged changes.
Revert()reverts all staged changes.

All changes are transient and require committing them at the end

Hence as per API documentation, it’s better to stage all the changes before calling commit or revert.

But please make sure we are not trying to save more millions of nodes at a time and also updating nodes takes more time compared to creating a new one as per the adapto conference showcase.

Hence check whether the node already has the property and value before you save it.

Resolution:

Saving resolver inside for loop

For our use case, I am using ResourceUtil.getOrCreateResource() for creating or getting the exiting node and if it creating then it will be saving the node with default properties like jcr:primaryType = un:unstructured

Using ResourceUtil increases code readability and maintainability

Parameters:
resolver – The resource resolver to use for the creation
path – The full path to be created
resourceProperties – The optional resource properties of the final resource to create
intermediateResourceType – THe optional resource type of all intermediate resources
autoCommit – If set to true, a commit is performed after each resource creation.

In the below example I am creating a for loop, and I am creating the node (resource) with default properties, and I am setting auto commit has true. After creating the resource, I am adapting it to ModifiableValueMap and I will be adding a new property name and value as “property+index” and committing the resolver.

public void saveNodes() {
  try (ResourceResolver resourceResolver = resolverFactory.getServiceResourceResolver(
    Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, SERVICE_USER))) {
    for (int index = 0; index <= 50; index++) {
      @NotNull
      Resource savedResource = ResourceUtil.getOrCreateResource(resourceResolver, "/content/" + index,
        defualtNodeProperties, StringUtils.EMPTY, true);
      ModifiableValueMap map = savedResource.adaptTo(ModifiableValueMap.class);
      map.put("name", "property" + index);
      resourceResolver.commit();
    }
  } catch (LoginException | PersistenceException e) {
    LOGGER.error("Error Occured during Login", e.getMessage());
  }
}

Saving resolver outside for loop

In the below example I have to remove auto save as false and the rest of the code remains the same, but I am committing resolver outside for loop. By doing so, I can stage the resource resolver and commit it at last.

public void saveNodes() {
  try (ResourceResolver resourceResolver = resolverFactory.getServiceResourceResolver(
    Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, SERVICE_USER))) {
    for (int index = 0; index <= 50; index++) {
      @NotNull
      Resource savedResource = ResourceUtil.getOrCreateResource(resourceResolver, "/content/" + index,
        defualtNodeProperties, StringUtils.EMPTY, false);
      ModifiableValueMap map = savedResource.adaptTo(ModifiableValueMap.class);
      map.put("name", "property" + index);
    }
    resourceResolver.commit();
  } catch (LoginException | PersistenceException e) {
    LOGGER.error("Error Occured during Login", e.getMessage());
  }
}

What will happen if I rerun the same code?

ResourceUtil would handle getting the existing resource instead of recreating, but I would still be updating the resource and committing the changes, which is a costly process.

Better implementation with Validation

In order to avoid the updating of the node we could validate the property exists and check the value and if and only if the resolver has changes, will commit.

public void saveNodes() {
  try (ResourceResolver resourceResolver = resolverFactory.getServiceResourceResolver(
    Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, SERVICE_USER))) {
    for (int index = 0; index <= 50; index++) {
      @NotNull
      Resource savedResource = ResourceUtil.getOrCreateResource(resourceResolver, "/content/" + index,
        defualtNodeProperties, StringUtils.EMPTY, false);
      ModifiableValueMap map = savedResource.adaptTo(ModifiableValueMap.class);
      if (!map.containsKey("name") || !StringUtils.equals(map.get("name", StringUtils.EMPTY), "property" + index)) {
        map.put("name", index);
      }
    }
    if (resourceResolver.hasChanges()) {
      resourceResolver.commit();
    }
  } catch (LoginException | PersistenceException e) {
    LOGGER.error("Error Occured during Login", e.getMessage());
  }
}