Prefetching records
The problem
GraphQL naive implementations often suffer from the "N+1" problem.
Consider a request where a user attached to a post must be returned:
{
posts {
id
user {
id
}
}
}
A naive implementation will do this:
- 1 query to fetch the list of posts
- 1 query per post to fetch the user
Assuming we have "N" posts, we will make "N+1" queries.
There are several ways to fix this problem. Assuming you are using a relational database, one solution is to try to look ahead and perform only one query with a JOIN between "posts" and "users". This method is described in the "analyzing the query plan" documentation.
But this can be difficult to implement. This is also only useful for relational databases. If your data comes from a NoSQL database or from the cache, this will not help.
Instead, GraphQLite offers an easier to implement solution: the ability to fetch all fields from a given type at once.
The "prefetch" method
#[Type]
class PostType {
/**
* @param mixed $prefetchedUsers
* @return User
*/
#[Field]
public function getUser(#[Prefetch("prefetchUsers")] $prefetchedUsers): User
{
// This method will receive the $prefetchedUsers as first argument. This is the return value of the "prefetchUsers" method below.
// Using this prefetched list, it should be easy to map it to the post
}
/**
* @param Post[] $posts
* @return mixed
*/
public static function prefetchUsers(iterable $posts)
{
// This function is called only once per GraphQL request
// with the list of posts. You can fetch the list of users
// associated with this posts in a single request,
// for instance using a "IN" query in SQL or a multi-fetch
// in your cache back-end.
}
}
When a #[Prefetch]
attribute is detected on a parameter of #[Field]
attribute, the method is called automatically.
The prefetch callable must be one of the following:
- a static method in the same class:
#[Prefetch('prefetchMethod')]
- a static method in a different class:
#[Prefetch([OtherClass::class, 'prefetchMethod')]
- a non-static method in a different class, resolvable through the container:
#[Prefetch([OtherService::class, 'prefetchMethod'])]
The first argument of the method is always an array of instances of the main type. It can return absolutely anything (mixed).
Input arguments
Field arguments can be set either on the #[Field]
annotated method OR/AND on the prefetch methods.
For instance:
#[Type]
class PostType {
/**
* @param mixed $prefetchedComments
* @return Comment[]
*/
#[Field]
public function getComments(#[Prefetch("prefetchComments")] $prefetchedComments): array
{
// ...
}
/**
* @param Post[] $posts
* @return mixed
*/
public static function prefetchComments(iterable $posts, bool $hideSpam, int $filterByScore)
{
// Parameters passed after the first parameter (hideSpam, filterByScore...) are automatically exposed
// as GraphQL arguments for the "comments" field.
}
}