GraphQL performance

A common issue when querying data is the N+1 problem. It means that querying multiple items of the same type triggers an additional query for each item, thus potentially leading to performance issues.

There are two ways to solve this in our code:

  • Eager-loading with our ORM

  • Deferred loading with the GraphQL library

You can see more about both methods below. 

Eager-loading with our ORM

The ORM comes with advanced handling of relationships (see Entity relationships), which can help reducing the number of queries triggered in GraphQL requests.

To make use of it, you would make the query (or mutation) code responsible for fetching the data it will need (or potentially needs) when resolving the types using relationships. This requires your code to make use of the relationships, and thus has its limitations.

Also, as different GraphQL queries can use different fields of the same type, it is not guaranteed that the query/mutation code caters for every use case. However, in a lot of cases it can help to reduce the number of queries drastically.

Deferred loading with the GraphQL library

The webonyx GraphQL library comes with built-in support to defer loading of data (see https://webonyx.github.io/graphql-php/data-fetching/#solving-n1-problem), but it requires code to make use of this feature.

Currently, we provide a solution which works with the entities and relationships of the ORM, called 'entity buffer'.

It uses the mechanics described in the webonyx documentation to build up a buffer of entities to load. It will buffer all throughout the request, and will trigger only one query per entity class when the final promises of the GraphQL request are resolved.

The advantage of using this method is that you can generically cover all scenarios and combinations triggered by all queries and mutations, and you do not need to make sure the queries themselves make sure the data is preloaded.

However, the current buffer only works for entities and not with other classes, such as models, for example. It represents a working example of how a buffer can be implemented.

The usage is as simple as passing the entity instance and the name of the relation you want to defer to the buffer::defer() function of the buffer. The function will return a Closure, which can be passed on to the Deferred class the GraphQL library provides:

return new Deferred(buffer::defer($entity, 'my_relation_name'));

Here is a full example:

Example
public static function resolve(string $field, $value, array $args, execution_context $ec) {
    if (!$value instanceof pathway_achievement_entity) {
        throw new \coding_exception('Please pass a pathway_achievement entity');
    }

    $format = $args['format'] ?? null;

    if (!self::authorize($field, $format)) {
        return null;
    }

    switch ($field) {
        case 'scale_value':
            return new Deferred(buffer::defer($value, 'scale_value'));
        case 'pathway':
            return new Deferred(buffer::defer($value, 'pathway'));
        case 'user':
            return new Deferred(buffer::defer($value, 'user'));
        case 'achieved':
            // This is an alias for has_scale_value
            $field = 'has_scale_value';
            break;
    }

    $formatter = new formatter\pathway_achievement($value, context_system::instance());
    return $formatter->format($field, $format);
}