Skip to content

Commit e216bd1

Browse files
authored
Create README.md
1 parent 901ffd0 commit e216bd1

File tree

1 file changed

+104
-0
lines changed

1 file changed

+104
-0
lines changed

README.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# laravel/hasmanywithinverse
2+
3+
## Why?
4+
5+
[Jonathan Reinink](https://github.com/reinink) wrote a great blog post about [Optimizing circular relationships in Laravel](https://reinink.ca/articles/optimizing-circular-relationships-in-laravel)
6+
7+
By manually setting the (`belongsTo`) relationship to a parent model on related (`hasMany`) child models, you can save unnecessary queries for the parent model -- when the child needs an instance of the parent model.
8+
9+
This probably sounds confusing, so just read the blog post. It's very good.
10+
11+
Jonathan's approach suggests using something like this:
12+
13+
```php
14+
$category->products->each->setRelation('category', $category);
15+
```
16+
17+
This works, but it's not very clean and there are cases when it doesn't work. For example, on model creation.
18+
19+
If you're accessing the parent model in `creating` and `saving` events on the children, the `->each->setRelation()` approach won't help you at all. (And if you're building a complex app with [Laravel Nova](https://nova.laravel.com), there's a high chance you're using lots of such events.)
20+
21+
## Practical Example & Benchmarks
22+
23+
I have an e-commerce application where an `Order` has child models: `OrderProduct`, `OrderStatus` and `OrderFee` (think shipping costs, payment fees, etc).
24+
25+
When some of those models are **being created** (`creating` Eloquent event), they are accessing the parent model.
26+
27+
For example, `OrderProduct`s convert their prices to `$this->order->currency`. `OrderFee`s check for other order fees, and they prevent creating themselves if a fee with the same code already exists (so that you can't have, say, the shipping cost counted twice). Etc.
28+
29+
This results in order creation being expensive, resulting in a large amount of n+1 queries.
30+
31+
### Benchmark
32+
33+
I haven't run a huge amount of tests, so I won't present the time differences here. I will only talk about database query count.
34+
35+
I have created an order with 6 products.
36+
37+
#### This is the amount of queries made with regular `hasMany()`
38+
39+
![Query count with hasMany()](https://i.imgur.com/Swl9zGw.png)
40+
41+
And now I just replace all of these calls:
42+
43+
```php
44+
return $this->hasMany(...);
45+
```
46+
with these calls
47+
```php
48+
return $this->hasManyWithInverse(..., 'order');
49+
```
50+
51+
inside the `Order` model.
52+
53+
#### And this is the amount of queries made with `hasManyWithInverse()`
54+
55+
![Query count with hasManyWithInverse()](https://i.imgur.com/TqeWIa4.png)
56+
57+
See the query count reduction.
58+
59+
The request duration was also decreased from 399ms to 301ms on my machine, though note that I did not run this test a million times to calculate an average request duration, so that benchmark might not be very accurate.
60+
61+
This is pretty impressive for **a free improvement that only requires changing a few simple calls to a similar method**.
62+
63+
But note that this is not a silver bullet for solving all n+1 queries. As you can see, even with this implemented, my app still has many duplicated queries. (Although not all are unintentional n+1s as there are a few `$this->refresh()` calls to keep the order up-to-date after state transitions).
64+
65+
## Installation
66+
67+
Laravel 6.x and 7.x is supported.
68+
69+
```
70+
composer require stancl/laravel-hasmanywithinverse
71+
```
72+
73+
## Usage
74+
75+
```php
76+
namespace App;
77+
78+
use Stancl\HasManyWithInverse\HasManyWithInverse;
79+
80+
class Order extends Model
81+
{
82+
use HasManyWithInverse;
83+
84+
public function products()
85+
{
86+
// 'order' is the name of the relationship in the other model, see below
87+
return $this->hasManywithInverse(App\OrderProduct::class, 'order');
88+
}
89+
}
90+
91+
class OrderProduct extends Model
92+
{
93+
public function order()
94+
{
95+
return $this->belongsTo(Order::class);
96+
}
97+
}
98+
```
99+
100+
You may also want to use the trait in a base Eloquent model and then use `$this->hasManyWithInverse()` without thinking about traits in the specific models.
101+
102+
## Details
103+
104+
The (simple) internals of the package are just methods copied from Eloquent source code, with a few lines added to them. The `hasManyWithInverse()` method signature is the exact same as `hasMany()` (you can set `$foreignKey` and `$localKey`), except the second argument (`$inverse`) was added to let you define the name of the relationship on the child model.

0 commit comments

Comments
 (0)