Setting up GraphQL with PHP: Defer field resolving

The second part of this tutorial is shorter and focuses on deferring field resolving to a later stage so we can solve the n+1 problem

Rcls
5 min readFeb 24, 2020

In the first part of this tutorial we covered how you can setup GraphQL with PHP using Slim Framework. This part covers how to set up DataLoaderPHP for your field resolvers for query batching.

The repository for this demo can be found on Github.

Disclaimer

DataLoader-PHP repository seems abandoned, having it’s last commit made over a year ago. However, for our needs it works just fine. You can search for a more up-to-date repository or build a solution yourself if it does not suit you.

Let’s get started

First off, install DataLoaderPHP using composer:

composer require overblog/dataloader-php

Now we have to add a PromiseAdapter to our GraphQL. We can do this using the GraphQL-PHP library. Add the following lines at the beginning of your method:

$graphQLSyncPromiseAdapter = new SyncPromiseAdapter();
$promiseAdapter = new WebonyxGraphQLSyncPromiseAdapter($graphQLSyncPromiseAdapter);

Now, let’s set our SyncPromiseAdapter for StandardServer

# Create server configuration
$config = ServerConfig::create()
->setSchema($schema)
->setContext($context)
->setQueryBatching(true)
->setPromiseAdapter($graphQLSyncPromiseAdapter);

Note! During Part 1 of this tutorial you might’ve used the executeQuery() function, but as I mentioned in that tutorial, this function does not cover query batching, which we need here. Instead we must use StandardServer.

Now, after this is done we should set up our DataLoaders.

Create your DataLoaders

So what we essentially want to do is, we want to attach the author to our Book, as a field, and fetch the information for each book. We want this to have it’s own field resolver, so we can choose when we want to query this field. This way we can limit the number of trips to the database, on the client side.

However, if we don’t defer resolving this field, what we end up with is a single query, fetching all the books, and for each row returned, GraphQL will resolve the author, executing n+1 database queries. Thus the n+1 problem.

We will be working backwards from this point on, writing the Loaders first, then attaching them to the resolver and finally injecting them into the context. Now, for simplicity, I’ve created a class called DataLoaders inside /src/GraphQL/DataLoaders.php. This classsimply returns an array when calling the build() method, where each key contains a callback function. You can create a solution of your choosing for these.

<?phpnamespace App\GraphQL;
use Overblog\DataLoader\DataLoader;
use Doctrine\DBAL\Connection;
class DataLoaders
{
protected $db;
public function __construct(Connection $connection)
{
$this->db = $connection;
}
/**
* GraphQL DataLoaders which get injected into the context for resolvers to use
*
*
@param $promiseAdapter
*
@return array
*/
public function build($promiseAdapter)
{
return [
'author' => new DataLoader(function ($authorIds) use ($promiseAdapter) {
$map = [];
$query = $this->db->executeQuery("SELECT id, `name` FROM author WHERE id in (?)",
[$authorIds],
[Connection::PARAM_INT_ARRAY]
);
$rows = $query->fetchAll();
foreach ($rows as $r) {
$map[$r['id']] = $r;
}
return $promiseAdapter->createAll(array_values($map));
}, $promiseAdapter)
];
}
}

So inside this array we’ve added a key named author that holds a new instance of DataLoader, with the first parameter being a callback function. This callback function always receives the loaded keys as it’s only parameter, in the form of an array. We use those keys to create a database query where we can fetch all authors in one go using MySQL’s IN() function.

In the example above, the DataLoader will receive the $authorIds from the books resolver function. Once we’re done executing our database query we construct an associative array of data ($map), using the result set, where the author id is set as the key and author name is the value.

Note, that the callback function should return an array of equal size to that of it’s given keys! Meaning if we receive 3 keys (3 book id’s), we should return an array with 3 entries. Also, for GraphQL to map each author to their corresponding book you must use the given key (id) as the array index.

Update resolvers

Open your resolvers.php and refactor your file like so:

<?phpuse Overblog\DataLoader\DataLoader;return [
'Book' => [
'author' => function ($book, $args, $context) {
return $context['loaders']['author']
->load($book['author_id']);
}
],
'Query' => [
'getBooks' => function ($root, $args, $context) {
return $context['db']
->fetchAll("SELECT * FROM book");
}
]
];

So what we did here was first, we deleted the getAuthors resolver. We won’t be fetching authors separately anymore. (We should not have in the first place.)

After this, we created a field resolver for author which will be added as a field for type Book in the next section where we modify the schema. This field receives $book as a parameter from the getBooks resolver function.

author calls DataLoader’s load() function which stores each author_id inside of an array and passes this to the loader’s callback function (remember $authorIds?). You can see that we call ['author']->load() using our context. We’ll get to that soon.

load() returns a Promise for the value represented by that key which our promise adapter will then resolve once we’re done loading (waiting). So once we’re done loading the data, DataLoader executes the callback method we’ve given to it.

Update schema

So, open up your src/GraphQL/schema.graphqls file and modify it like so:

schema {
query: Query
}
type Query {
getBooks: [Book]
}
type Book {
id: ID
title: String!
author: Author
}
type Author {
id: ID!
name: String
}

So, in here we removed the getAuthors from type Query so it won’t work as a separate Query anymore. After this we created a field author for type Book that is of type Author.We just wrote a field resolver for it.

Back to our controller

Now that we’ve set up the DataLoaders class, updated our resolvers and schema, and attached a PromiseAdapter to our StandardServer, we will need to initialize the class and inject the array containing the loaders, into our context! So let’s do that.

Open up your GraphQLController.php file again and extend the index() method, just after the PromiseAdapter initialization.

# Injecting Connection for database access
$dataLoaders = new DataLoaders($this->db);
# Context, objects and data the resolver can then access. In this case the database object.
$context = [
'loaders' => $dataLoaders->build($promiseAdapter),
'db' => $this->db,
'logger' => $this->logger
];

````

So we initiated the DataLoaders and injected the array to our context. All loaders are now available within our resolvers and we should be ready to rock!

Results

You can now launch your app, using php -S localhost:8080 -t public public/index.php at your application root.

Try running the following query against your GraphQL

query {
getBooks {
id
title
author {
name
}
}
}

You should get a response containing a list of books with author names attached. Now open your logs/app.log file and check the queries your app just executed. It should look like this:

{
"1": {
"sql": "SELECT * FROM book",
"params": [],
"types": [],
"executionMS": 0.00023293495178222656
},
"2": {
"sql": "SELECT id, `name` FROM author WHERE id in (?)",
"params": [
[
"1",
"2",
"3"
]
],
"types": [
101
],
"executionMS": 0.0008800029754638672
}
}

That’s just 2 queries! You just batched together the data and deferred resolving author to a later stage so we could do this. Using this solution we can optimize the number of queries we run against our database when implementing GraphQL.

You can also test how field resolvers don’t get called by dropping the author {} from your getBooks query, and see that it never executes the second database query.

--

--

Rcls

Consultant, software architect and developer, freelance UI/UX designer, computer engineer, tech enthusiast, father.