stancl/laravel-hasmanywithinverse

在 Laravel 中定义 HasMany 关系的同时设置反向关系。

v1.4.2 2023-02-16 09:38 UTC

This package is auto-updated.

Last update: 2024-09-16 13:47:28 UTC


README

为什么?

Jonathan Reinink 写了一篇关于 优化 Laravel 中循环关系 的优秀博客文章。

通过手动将 (belongsTo) 关系设置到相关 (hasMany) 子模型上的父模型,您可以在子模型需要父模型实例时节省不必要的查询。

这可能听起来很复杂,所以请直接阅读博客文章。它非常好。

Jonathan 的方法建议使用以下类似的方法

$category->products->each->setRelation('category', $category);

这可以工作,但它并不干净,而且有些情况下它不起作用。例如,在模型创建时。

如果您在子模型的 creatingsaving 事件中访问父模型,那么使用 ->each->setRelation() 的方法对您没有任何帮助。(如果您正在构建一个复杂的应用程序,并且使用 Laravel Nova,那么您很可能正在使用大量此类事件。)

实际示例及基准测试

我有一个电子商务应用程序,其中 Order 模型有子模型:OrderProductOrderStatusOrderFee(例如,运费、支付费用等)。

当这些模型中的一些正在被创建时(creating Eloquent 事件),它们正在访问父模型。

例如,OrderProduct 将其价格转换为 $this->order->currencyOrderFee 检查其他订单费用,如果存在具有相同代码的费用,它们将阻止创建自己(这样您就不能重复计算运费等)。等等。

这导致订单创建变得昂贵,导致大量 n+1 查询。

基准测试

我没有运行大量的测试,所以在这里不会展示时间差异。我只会谈论数据库查询次数。

我创建了一个包含 6 个产品的订单。

这是使用常规 hasMany() 执行的查询数量

Query count with hasMany()

现在我只是在 Order 模型内部替换所有这些调用

return $this->hasMany(...);

为这些调用

return $this->hasManyWithInverse(..., 'order');

在这个位置

这是使用 hasManyWithInverse() 执行的查询数量

Query count with hasManyWithInverse()

可以看到查询次数的减少。

在我的机器上,持续时间也从 114 毫秒减少到 45 毫秒,但请注意,我没有运行一百万次来计算平均持续时间,所以这个基准测试可能不是很准确。

这对于只需要更改几个简单的调用到类似方法的免费改进来说是非常令人印象深刻的。

但请注意,这并不是解决所有 n+1 查询的万能药。正如您所看到的,即使实现了这个功能,我的应用程序仍然有许多重复的查询。(尽管不是所有都是无意的 n+1,因为有几个 $this->refresh() 调用来在状态转换后保持订单的更新。)

安装

支持 Laravel 9.x 和 10.x。

composer require stancl/laravel-hasmanywithinverse

用法

namespace App;

use Stancl\HasManyWithInverse\HasManyWithInverse;

class Order extends Model
{
    use HasManyWithInverse;

    public function products()
    {
        // 'order' is the name of the relationship in the other model, see below
        return $this->hasManyWithInverse(OrderProduct::class, 'order');
    }
}

class OrderProduct extends Model
{
    public function order()
    {
        return $this->belongsTo(Order::class);
    }
}

您还可以在基 Eloquent 模型中使用该特性,然后使用 $this->hasManyWithInverse(),无需考虑特定模型中的特性。

详细信息

包的(简单)内部实现只是从Eloquent源代码复制的方法,并在其中添加了几行代码。方法签名 hasManyWithInverse()hasMany() 相同(你可以设置 $foreignKey$localKey),只是增加了第二个参数($inverse),让你定义子模型上的关系名称,并增加了最后一个参数($config),让你配置关系设置的行为。

此包在创建子项时($child = $parent->children()->create())和解析父项的子项时($children = $parent->children)都会设置子项的父关系。 你可以为每个关系自定义此行为。

要禁用创建子项时设置关系,请这样做

class Parent extends Model
{
    public function children()
    {
        return $this->hasManyWithInverse(Child::class, 'parent', null, null, ['setRelationOnCreation' => false]);
    }
}

要禁用解析子项时设置关系,请这样做

class Parent extends Model
{
    public function children()
    {
        return $this->hasManyWithInverse(Child::class, 'parent', null, null, ['setRelationOnResolution' => false]);
    }
}

你也可以将可调用对象作为配置值传递。如果你想在某些请求中禁用此行为,这很有用。下面是一个示例。

Laravel Nova

对于Nova请求,禁用解析时设置关系是一个好主意。它们倾向于进行大量查询,这可能会减慢页面速度(或导致502错误)。

以下是一个使用基本模型并添加配置以过滤Nova请求的示例实现。

abstract class Model extends EloquentModel
{
    use HasManyWithInverse {
        hasManyWithInverse as originalHasManyWithInverse;
    }

    public function hasManyWithInverse($related, $inverse, $foreignKey = null, $localKey = null, $config = [])
    {
        $config = array_merge(['setRelationOnResolution' => function () {
            if (request()->route() && in_array('nova', request()->route()->middleware())) {
                return false;
            }
        }], $config);

        return $this->originalHasManyWithInverse($related, $inverse, $foreignKey, $localKey, $config);
    }
}