Doctrine N plus one detector
Motivation
When working with an orm you need to deal with the n+1 queries problem. If you are not familiar with it, you can read more about it here. In a few words it's when the orm does n+1 queries as result of your code accessing lazily loaded entities.
The remediation for the n+1 is rather easy, it consists in eager loading the relations causing the n+1 (either by fetch joining them or for doctrine even better with a partial hydration). Those adhoc fixes are in the parts of the application that fetch the root entity for the specific use case. What to eager load can't be statically defined at load time because it really depends on how the fetched entities are going to be used later on.
While the n+1 problem is well understood by most developers, due to the nature of the problem is rather easy to let slip an n+1 while doing some application changes. Think about cases where people use entities in their template engine, and they just fulfil a new requirement showing a bit more information triggering a new n+1. Or think about cases where a central entity is refactored to replace a scalar property with an x-to-one relation, thus you need to perform a careful analysis of each use of the refactored method to see if you need n+1 fixes. That's where doctrine n+1 detector comes into place. Once enabled it will listen to some doctrine events detecting n+1 queries triggered by doctrine and eventually it will log them so that the user can inspect them later (or maybe create alarms around those logs if he prefers it). In other words you still need to eager load to avoid n+1 queries, but in case some n+1 slip inadvertently in production you have a tool to make you aware about it so that you can fix them sooner than later.
Installation
You can install the package with composer:
composer require danydev/n-plus-one-detector
Usage
You need to initialize the detector and call start
as soon as you want to start detecting n+1, finally later on the request lifecycle you can inspect all the n+1 detected.
// Start the n+1 detector at the start of our request handler.
$nPlusOneDetector = new NPlusOneDetector($entityManager);
$nPlusOneDetector->start();
//... normal request lifecycle happens here...
// Inspect n+1 detected just before returning the response
$result = $nPlusOneDetector->getDetectedNPlusOne();
foreach($result->getCollectionStats() as $stat) {
error_log('[N+1-detected] on ' . $stat->getOwnerClass() . ' due to collection of ' . $stat->collectionElementsClass());
}
foreach($result->getProxyStats() as $stat) {
error_log('[N+1-detected] on ' . $stat->getClass());
}
TODO
- PSR-15 middleware that does the start and stop, configurable with an optional PSR-3 logger.
- Symfony bundle to integrate with their HttpKernel.
- Detect n+1 generated by non-owning side one-to-one (this n+1 is by design in doctrine, but it may be valuable for the user to be notified about it).
- Detect n+1 generated by *-to-many used in classes with an abstract parent (this n+1 is by design in doctrine, but it may be valuable for the user to be notified about it).
- Write some tests.
- Apply/enforce phpcs, psalm/phpstan on ci.