Skip to content

Commit 4b27bc7

Browse files
Merge pull request #6 from liam-wiltshire/add-logger
Add logger
2 parents 734a998 + da6323f commit 4b27bc7

File tree

7 files changed

+260
-27
lines changed

7 files changed

+260
-27
lines changed

README.md

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
# liam-wiltshire/laravel-jit-loader
22

3-
liam-wiltshire/laravel-jit-loader is an extension to the default Laravel Eloquent model to 'very lazy eager load' relationships.
3+
liam-wiltshire/laravel-jit-loader is an extension to the default Laravel Eloquent model to 'very lazy eager load' relationships with performance comparable with eager loading.
4+
5+
# Installation
6+
liam-wiltshire/laravel-jit-loader is available as a composer package:
7+
`composer require liam-wiltshire/laravel-jit-loader`
8+
9+
Once installed, use the `\LiamWiltshire\LaravelJitLoader\Concerns\AutoloadsRelationships` trait in your model, or have your models extend the `\LiamWiltshire\LaravelJitLoader\Model` class instead of the default eloquent model, and JIT loading will be automatically enabled.
410

511
# Very Lazy Eager Load?
612
In order to avoid [N+1 issues](https://secure.phabricator.com/book/phabcontrib/article/n_plus_one/), you'd normally load your required relationships while building your collection:
@@ -23,7 +29,7 @@ In some situations however, this may not be possible - perhaps front-end develop
2329
This change will track if your models belong to a collection, and if they do and a relationship is called that hasn't already been loaded, the relationship will be loaded across the whole collection just in time for use.
2430

2531
# Does This Work?
26-
Yes. At least, it does in our production Laravel app. It's also been tested against a (rather constructed) test, pulling out staff, companies and addresses - while this isn't a 'real life' representation, it should give an idea of what it can do:
32+
This is used in a number of production applications with no issues. It's also been tested against a (rather constructed) test, pulling out staff, companies and addresses - while this isn't a 'real life' representation, it should give an idea of what it can do:
2733

2834
```php
2935
public function handle()
@@ -66,22 +72,45 @@ Yes. At least, it does in our production Laravel app. It's also been tested agai
6672

6773
Running this locally against a database with 200 companies, 1157 addresses and 39685 staff:
6874

69-
## Without JIT loading:
70-
Queries Run: 10739
71-
Execution Time: 16.058979034424
72-
Memory:68MiB
75+
## Without JIT Loading:
76+
Queries Run: 10739<br />
77+
Execution Time: 17.090859889984<br />
78+
Memory: 70MiB
7379

7480

75-
## With JIT loading:
76-
Queries Run: 6
77-
Execution Time: 1.6715261936188
78-
Memory:26MiB
81+
## With JIT Loading:
82+
Queries Run: 3<br />
83+
Execution Time: 1.7261669635773<br />
84+
Memory: 26MiB
7985

80-
# Installation
81-
liam-wiltshire/laravel-jit-loader is available as a composer package:
82-
`composer require liam-wiltshire/laravel-jit-loader`
8386

84-
Once installed, use the `\LiamWiltshire\LaravelJitLoader\Concerns\AutoloadsRelationships` trait in your model, or have your models extend the `\LiamWiltshire\LaravelJitLoader\Model` class instead of the default eloquent model, and JIT loading will be automatically enabled.
87+
## 'Proper' Eager Loading:
88+
Queries Run: 3<br />
89+
Execution Time: 1.659285068512<br />
90+
Memory: 26MiB
91+
92+
# Logging
93+
As you can see the different between JIT loading and traditional eager loading is small (c. 0.067 seconds in our above test), so you can likely rely on JIT loader to protect you.
94+
95+
However, if you want to log when the JIT loader is used so that you can do back and correct them later, you can add a `$logChannel` property to your models to ask the trait to log into that channel as configured in Laravel
96+
97+
```php
98+
class Address extends Model
99+
{
100+
use AutoloadsRelationships;
101+
public $timestamps = false;
102+
103+
/**
104+
* @var string
105+
*/
106+
protected $logChannel = 'jit-logger';
107+
108+
public function company()
109+
{
110+
return $this->belongsTo(Company::class);
111+
}
112+
}
113+
```
85114

86115
# Limitations
87116
This is an early release based on specific use cases. At the moment the autoloading will only be used when the relationship is loaded like a property e.g. `$user->company->name` instead of `$user->company()->first()->name`. I am working on supporting relations loaded in alternate ways, however there is more complexity in that so there isn't a fixed timescale as of yet!

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"require-dev": {
1616
"phpunit/phpunit": "^7.0.0",
1717
"squizlabs/php_codesniffer" : "^3.0.0",
18-
"phpunit/php-code-coverage": "^6.0.0"
18+
"phpunit/php-code-coverage": "^6.0.0",
19+
"illuminate/log": "^5.5.0"
1920
},
2021
"autoload": {
2122
"psr-4": {

src/Concerns/AutoloadsRelationships.php

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
namespace LiamWiltshire\LaravelJitLoader\Concerns;
44

55
use Illuminate\Database\Eloquent\Model;
6+
use Illuminate\Log\LogManager;
67
use LogicException;
78
use Illuminate\Database\Eloquent\Relations\Relation;
89
use Illuminate\Database\Eloquent\Collection;
10+
use Psr\Log\LoggerInterface;
911

1012
/**
1113
* Trait AutoloadsRelationships
@@ -24,6 +26,12 @@ trait AutoloadsRelationships
2426
*/
2527
protected $parentCollection = null;
2628

29+
/**
30+
* @var ?LoggerInterface
31+
*/
32+
protected $logDriver;
33+
34+
2735
/**
2836
* Check to see if we should autoload
2937
* @return bool
@@ -35,6 +43,57 @@ private function shouldAutoLoad(): bool
3543
&& count($this->parentCollection) <= $this->autoloadThreshold);
3644
}
3745

46+
/**
47+
* @codeCoverageIgnore
48+
*/
49+
private function getLogDriver()
50+
{
51+
if (!$this->logDriver) {
52+
/**
53+
* @var LogManager $logManager
54+
*/
55+
$logManager = app(LogManager::class);
56+
$this->logDriver = $logManager->channel($this->logChannel);
57+
}
58+
}
59+
60+
/**
61+
* @param string $file
62+
* @return bool|string
63+
* @codeCoverageIgnore
64+
*/
65+
private function getBlade(string $file)
66+
{
67+
if (strpos($file, "framework/views/") === false) {
68+
return false;
69+
}
70+
71+
$blade = file($file)[0];
72+
return trim(str_replace(["<?php /* ", " */ ?>"], "", $blade));
73+
}
74+
75+
/**
76+
* Log the fact we have used the JIT loader, if required
77+
*
78+
* @param string $relationship
79+
* @param string $file
80+
* @param int $lineNo
81+
* @return bool
82+
*/
83+
private function logAutoload(string $relationship, string $file, int $lineNo)
84+
{
85+
if (!isset($this->logChannel)) {
86+
return false;
87+
}
88+
89+
$this->getLogDriver();
90+
91+
$blade = $this->getBlade($file);
92+
93+
$this->logDriver->info("[LARAVEL-JIT-LOADER] Relationship ". self::class."::{$relationship} was JIT-loaded."
94+
." Called in {$file} on line {$lineNo} " . ($blade ? "view: {$blade})" : ""));
95+
}
96+
3897
/**
3998
* Load the relationship for the given method, and then get a relationship value from a method.
4099
* @param string $method
@@ -53,7 +112,13 @@ public function getRelationshipFromMethod($method)
53112
}
54113

55114
if ($this->shouldAutoLoad()) {
56-
$this->parentCollection->loadMissing($method);
115+
if (!$this->relationLoaded($method)) {
116+
$stack = debug_backtrace()[3];
117+
$this->logAutoload($method, $stack['file'], $stack['line']);
118+
$this->parentCollection->loadMissing($method);
119+
120+
return current($this->parentCollection->getIterator())->relations[$method];
121+
}
57122
}
58123

59124
return tap($relation->getResults(), function ($results) use ($method) {

tests/Concerns/AutoloadsRelationshipsTest.php

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88

99
namespace LiamWiltshire\LaravelJitLoader\Tests\Concerns;
1010

11-
11+
use Illuminate\Log\LogManager;
1212
use LiamWiltshire\LaravelJitLoader\Tests\TestCase;
13+
use LiamWiltshire\LaravelJitLoader\Tests\TraitlessModel;
1314
use LiamWiltshire\LaravelJitLoader\Tests\TraitModel;
15+
use Psr\Log\LoggerInterface;
1416

1517
class AutoloadsRelationshipsTest extends TestCase
1618
{
@@ -47,4 +49,65 @@ public function testGetRelationshipFromMethodUnderThresholdDoesAutoLoad()
4749

4850
$this->assertTrue($models[1]->relationLoaded('myRelationship'));
4951
}
52+
53+
public function testGetRelationshipFromMethodUnderThresholdDoesAutoLoadWithLogging()
54+
{
55+
56+
$driver = $this->getMockBuilder(LoggerInterface::class)->getMock();
57+
$driver->expects($this->atLeastOnce())->method('info')->willReturn(true);
58+
59+
$models = TraitModel::all();
60+
$models[0]->setLogging('jitLogger', $driver);
61+
62+
$related = $models[0]->myRelationship;
63+
64+
$this->assertTrue($models[1]->relationLoaded('myRelationship'));
65+
}
66+
67+
public function testPerformance()
68+
{
69+
$startTime = microtime(true);
70+
$this->db->getConnection()->flushQueryLog();
71+
$models = TraitModel::all();
72+
73+
foreach ($models as $model) {
74+
$model->myRelationship;
75+
}
76+
77+
$traitedCount = count($this->db->getConnection()->getQueryLog());
78+
$traitedTime = microtime(true) - $startTime;
79+
80+
$startTime = microtime(true);
81+
$this->db->getConnection()->flushQueryLog();
82+
83+
$models = TraitlessModel::all();
84+
85+
foreach ($models as $model) {
86+
$model->myRelationship;
87+
}
88+
89+
$traitlessCount = count($this->db->getConnection()->getQueryLog());
90+
$traitlessTime = microtime(true) - $startTime;
91+
92+
$startTime = microtime(true);
93+
$this->db->getConnection()->flushQueryLog();
94+
95+
$models = TraitlessModel::with('myRelationship')->get();
96+
97+
foreach ($models as $model) {
98+
$model->myRelationship;
99+
}
100+
101+
$eagerCount = count($this->db->getConnection()->getQueryLog());
102+
$eagerTime = microtime(true) - $startTime;
103+
104+
105+
106+
$this->messages[] = "Using Trait: {$traitedCount} queries in {$traitedTime}s";
107+
$this->messages[] = "Lazy Loading: {$traitlessCount} queries in {$traitlessTime}s";
108+
$this->messages[] = "Eager Loading: {$eagerCount} queries in {$eagerTime}s";
109+
110+
$this->assertTrue($traitedCount < $traitlessCount);
111+
$this->assertTrue($traitedTime < $traitlessTime);
112+
}
50113
}

tests/TestCase.php

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,17 @@
99
namespace LiamWiltshire\LaravelJitLoader\Tests;
1010

1111

12+
use Illuminate\Contracts\Foundation\Application;
13+
use Illuminate\Contracts\Http\Kernel;
1214
use Illuminate\Database\Capsule\Manager;
1315

1416
class TestCase extends \PHPUnit\Framework\TestCase
1517
{
18+
19+
public $db;
20+
21+
public $messages = [];
22+
1623
public function setUp()
1724
{
1825
$this->configureDatabase();
@@ -30,6 +37,9 @@ protected function configureDatabase()
3037
));
3138
$db->bootEloquent();
3239
$db->setAsGlobal();
40+
41+
$this->db = $db;
42+
$this->db->getConnection()->enableQueryLog();
3343
}
3444

3545
public function migrateIdentitiesTable()
@@ -39,22 +49,46 @@ public function migrateIdentitiesTable()
3949
$table->integer('dummy_model_id');
4050
$table->timestamps();
4151
});
42-
DummyModel::create(array('dummy_model_id' => 5));
43-
DummyModel::create(array('dummy_model_id' => 4));
44-
DummyModel::create(array('dummy_model_id' => 3));
45-
DummyModel::create(array('dummy_model_id' => 2));
46-
DummyModel::create(array('dummy_model_id' => 1));
52+
53+
$x = 100;
54+
while ($x > 0) {
55+
DummyModel::create(array('dummy_model_id' => $x));
56+
$x--;
57+
}
4758

4859
Manager::schema()->create('trait_models', function($table) {
4960
$table->increments('id');
5061
$table->integer('trait_model_id');
5162
$table->timestamps();
5263
});
53-
TraitModel::create(array('trait_model_id' => 5));
54-
TraitModel::create(array('trait_model_id' => 4));
55-
TraitModel::create(array('trait_model_id' => 3));
56-
TraitModel::create(array('trait_model_id' => 2));
57-
TraitModel::create(array('trait_model_id' => 1));
5864

65+
$x = 100;
66+
while ($x > 0) {
67+
TraitModel::create(array('trait_model_id' => $x));
68+
$x--;
69+
}
70+
71+
Manager::schema()->create('traitless_models', function($table) {
72+
$table->increments('id');
73+
$table->integer('traitless_model_id');
74+
$table->timestamps();
75+
});
76+
77+
$x = 100;
78+
while ($x > 0) {
79+
TraitlessModel::create(array('traitless_model_id' => $x));
80+
$x--;
81+
}
82+
83+
}
84+
85+
public function __destruct()
86+
{
87+
if (!empty($this->messages)) {
88+
echo "Performance Data:\n";
89+
echo implode("\n", $this->messages);
90+
echo "\n";
91+
}
92+
parent::tearDown(); // TODO: Change the autogenerated stub
5993
}
6094
}

tests/TraitModel.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
use Illuminate\Database\Eloquent\Model;
77
use LiamWiltshire\LaravelJitLoader\Concerns\AutoloadsRelationships;
8+
use Psr\Log\LoggerInterface;
89

910
class TraitModel extends Model {
1011

@@ -26,4 +27,10 @@ public function setAutoloadThreshold(int $autoloadThreshold)
2627
{
2728
$this->autoloadThreshold = $autoloadThreshold;
2829
}
30+
31+
public function setLogging(string $channel, LoggerInterface $logger)
32+
{
33+
$this->logChannel = $channel;
34+
$this->logDriver = $logger;
35+
}
2936
}

0 commit comments

Comments
 (0)