Solving GraphQL N+1 in PHP

Jose Manuel Cardona
Softonic Engineering
2 min readMay 11, 2018

The webonyx/graphql-php package solves the N+1 problem using their deferred responses. The solution is good, but you will repeat this boilerplate in every resolve.

<?php
...
'resolve' => function($blogStory) use ($repository) {
Authors::add($blogStory['authorId']);

return new GraphQL\Deferred(function () use ($blogStory) {
Authors::fetchData();
return Authors::get($blogStory['authorId']);
});
}

The resolver will always resolve in the same way:

  1. Store all the root objects that in this case are $blockStory variables.
  2. Resolve the deferred object when it is needed.

As we can see in that steps there isn’t any specific logic for the resolver. Is in the closure passed to the deferred object where we get all the buffered data and returns the specific data for the given $blogStory (AKA root).

To avoid this generic part and concentrate in how to get the author information and send it to the specific blog stories, I have created joskfg/graphql-bulk-resolver.

How to solve N+1 using joskfg/graphql-bulk-resolver

The package is simple and it provides you a DeferredResolverTrait to be used in your graphql types. The trait allows you to extract your resolve loginc into a different class. This is the same code than before using the trait.

<?php
...
'resolve' => $this->deferredResolver(new Authors());

Now that we have attached a resolver to our resolve field, we just need to create the resolver that actually do the work.

The resolver must implement the DeferredResolverInterface that provides two method:

  • Fetch: Step where you have all the roots and can retrieve all the information at once.
  • Pluck: Step where you are going to choose between all the fetched data which is needed for the specific root provided.

The fetch method is executed once, so all the heavy tasks should be done here. The pluck method is executed once per root, so if you are listing 100 items it will be called 100 times. Due to this, it is important to do all the hard work in the fetch step and do pluck as simple as possible.

Here we can see a DeferredResolverInterface example:

<?php
use ...
class Authors implements DeferredResolverInterface {
/**
* @param array $roots All the stories to fetch authors.
*/
public function fetch($roots, $args, $context, $info) {
/**
* $roots All blog stories.
* Authors::fetchData returns authors data in an assoc array
* based in authorId value. Example: ['joskfg' => 'data']
*/
return Authors::fetchData($roots);
}
/**
* @param mixed $root Specific blogStory to fet their author info
* @param mixed $data All author info returned in fetch method
*/
public function pluck($root, $data) {
// $data contains the fetch method response.
return $data[$root['authorId']];
}
}

You can see that now we only code the logic based in how to retrieve data in bulk and how to split the data between the different roots. All the global status is managed by the trait and we program the resolver itself.

I hope this is useful for you and save you a lot of time.

--

--