jamieleepreece/laravel-custom-relation

当库存关系不足时使用的自定义关系。

1.0.0 2022-10-17 07:25 UTC

This package is auto-updated.

Last update: 2024-09-13 22:03:43 UTC


README

当库存关系不足时,用于自定义关系包装。

我需要自定义关系吗?如果你...

  • 没有任何库存关系符合要求。(例如BelongsToManyThrough等)
  • 你想要/需要你的应用程序执行更优化的查询/关系,而不是执行多个链式关系(减少开销 & N+1)
  • 你想要在可以使用原生Laravel方法(如with(预加载)或whereHas(存在)查询的情况下对可以连接的表有更多控制
  • 你想要控制关系生命周期的每个步骤

用例

基本概述

假设我们有3个模型

  • User
  • Role
  • Permission

假设UserRole之间有一个多对多关系,RolePermission之间也有一个多对多关系。

所以它们的模型可能看起来像这样。(故意保持简短。)

class User
{
    public function roles() {
        return $this->belongsToMany(Role::class);
    }
}
class Role
{
    public function users() {
        return $this->belongsToMany(User::class);
    }

    public function permissions() {
        return $this->belongsToMany(Permission::class);
    }
}
class Permission
{
    public function roles() {
        return $this->belongsToMany(Role::class);
    }
}

如果你想要获取某个User的所有Permission或者所有具有特定PermissionUser,会怎么样? Laravel中没有现成的库存关系可以描述这种情况。我们需要的是BelongsToManyThrough,但在库存Laravel中不存在。

示例

用于获取用户权限的自定义关系

use LaravelCustomRelation\HasCustomRelations;

class User
{
    use HasCustomRelations;

    /**
     * Get the related permissions
     *
     * @return App\Relations\Custom
     */
    public function permissions()
    {
        return $this->customRelationship(
            related: Permission::class,
            baseConstraints: function ($relation)
            {
                # Add base constraints (the base relationship query)
                function ($relation) 
                {
                    $relation->getQuery()
                        // join the pivot table for permission and roles
                        ->join('permission_role', 'permission_role.permission_id', '=', 'permissions.id')
                        // join the pivot table for users and roles
                        ->join('role_user', 'role_user.role_id', '=', 'permission_role.role_id');
                }
            },
            foreignKey: 'role_user.user_id'
        );
    }
}

使用命名参数的简单示例

前两个命名参数是必需的,用于定义自定义关系。related参数是目标Model的NS,baseConstraints是提供自定义关系基础查询的。这不要求任何WHERE约束,因为这些约束是根据被调用的关系动态应用的。

这里的foreignKey是可选的,但被传递以应用关系生命周期中的默认逻辑,例如将模型映射到父项、存在查询和预加载。然而,如果你想要编写自己的处理程序,可以像这样传递额外的闭包

use LaravelCustomRelation\HasCustomRelations;

class User
{
    use HasCustomRelations;

    /**
     * Get the related permissions
     *
     * @return App\Relations\Custom
     */
    public function permissions()
    {
        return $this->custom(

            # The target Model you want to obtain in the relationship
            Permission::class,

            # Add base constraints (the base relationship query)
            function ($relation) 
            {
                $relation->getQuery()
                    // join the pivot table for permission and roles
                    ->join('permission_role', 'permission_role.permission_id', '=', 'permissions.id')
                    // join the pivot table for users and roles
                    ->join('role_user', 'role_user.role_id', '=', 'permission_role.role_id');
            },

            function ($relation) 
            {
                # Specify model ID if if calling on single Model
                if ($this->id)
                {
                    $relation->getQuery()->where('role_user.user_id', $this->id);
                }
            },

            # Add eager constraints
            function ($relation, $models) 
            {
                # Specify where IDs for multiple models
                $relation->getQuery()->whereIn('role_user.user_id', collect($models)->pluck('id'));
            },

            # Map relationship models back into the parent models.
            # This example uses a dictionary for optimised sorting
            function($models, $results, $relation, $relationshipBuilder)
            {
                $dictionary = $relationshipBuilder->buildDictionary($results, 'user_id');

                foreach ($models as $model) {

                    if (isset($dictionary[$key = $model->getAttribute('id')]))
                    {
                        $values = $dictionary[$key];

                        $model->setRelation(
                            $relation, $relationshipBuilder->getRelated()->newCollection($values)
                        );
                    }
                }

                # Must return models
                return $models;
            },

            # Provide columns for existence join
            # For `has` (existence) queries, provide the correct columns for the join
            function($query, $parentQuery)
            {
                return $query->whereColumn(
                    $parentQuery->getModel()->getTable() . '.id', '=', 'role_user.user_id'
                );
            },
        );
    }
}

使用自定义逻辑的每个关系生命周期部分的长期示例

优化查询

Laravel提供了一种轻松访问关系(数据)的方法。然而,远程关系可能会使应用程序变得繁琐,并提供不必要的开销,在某些情况下还会出现N+1问题。

考虑一个现有的数据库不能轻易更改,所需的数据必须通过多个链式关系预加载的情况。假设你想要获取过去一年所有用户的所有打折产品。这种预加载方法如下所示

User::with('orders.lines.product.discounts');

这将执行以下查询

  1. 获取所有用户
  2. 获取所有用户的订单(WHERE orders.user_id user IN (...[IDs])
  3. 获取每个订单的所有行项(WHERE line_items.order_id user IN (...[IDs])
  4. 获取每个行项的所有产品(WHERE products.id user IN (...[IDs])
  5. 获取每个产品的所有折扣(通过枢纽表)(WHERE discounts.id user IN (...[IDs])

在某些情况下,可以对数据库进行重新设计以避免这种情况。然而,如果无法这样做,您可能不得不使用性能较低的获取所需数据的方法。数据库外观还提供了一种手动连接所有上述表并在单独的查询中获取数据的方法。但要注意的是,您必须编写额外的代码来补充您现有的查询,这些查询位于其他急切加载关系之外。

为了纠正这个问题,自定义关系允许在关系包装器中放置复杂的连接。上述关系可以聚合在一起进行一次大的表连接,允许直接将 Discount 模型加载到 User 模型上。这实际上将五个查询和额外的开销减少到两个。

  1. User 模型被收集
  2. 收集所有 Discount 模型并将它们映射到父模型中

示例

use LaravelCustomRelation\HasCustomRelations;

class User
{
    use HasCustomRelations;

    /**
     * Get all distinct products which have been tagged
     *
     * @return App\Relations\Custom
     */
    public function discountedProducts()
    {
        return $this->customRelationship(

            # The target Model you want to obtain in the relationship
            related: Discount::class,

            # Add base constraints (the base relationship query)
            baseConstraints: function ($relation)
            {
                # Query for the discounts table
                $relation->getQuery()
                    ->distinct()
                    ->join('products_discount_pivot', 'discount.id', '=', 'products_discount_pivot.discount_id')
                    ->join('products', 'products_discount_pivot.product_id', '=', 'products.id')
                    ->join('line_items', 'products.id', '=', 'line_items.product_id')
                    ->join('orders', 'line_items.order_id', '=', 'orders.id')
            },

            foreignKey: 'orders.user_id'
        );
    }
}

在一个方便的关系中连接多个表

此查询现在将提供一个优化的方式来收集远程关系,而不会在您的应用程序中增加额外的开销。这只是一个例子,但旨在扩展到需要每个级别优化的应用程序。

通过允许大多数默认逻辑在关系生命周期中运行(如上述示例中所示,不提供可选的 Closures),然后可以以最少的代码和较低的代码重复定义自定义关系。

参数

  1. related:

    • 必需的
    • 目标 Model 的完全限定命名空间(要获取的内容)
  2. baseConstraints:

    • 必需的
    • 自定义关系查询。这是指定关系连接的地方,以及任何其他 SQL 参数,如 DISTINCT 等。在这里可以构建的查询没有限制,但是不应包含外键到主键的 WHERE 子句,因为这是即时应用的。
  3. singleConstraints:

    • 可选的
    • 指定匹配父 ID 到外键的 WHERE 子句
    • 如果未指定,则在外键名称中使用父 ID 进行 WHERE 约束
    • 此逻辑仅在从 单个 Model 执行关系时触发
  4. eagerConstraints:

    • 可选的
    • 指定在急切加载时应用的约束
    • 如果未指定,逻辑是在外键上应用 WHERE IN,包括所有父 ID
    • 此逻辑仅在急切加载时应用,例如 ->with('products')
  5. eagerMatcher:

    • 可选的
    • 指定映射函数以将所有收集的关系模型分配到父模型中
    • 如果未指定,则使用 foreignKeylocalKey 将集合映射到父模型中
    • 此操作仅在急切关系查询运行之后执行
  6. existenceJoin:

    • 可选的
    • 当使用 has(EXISTS)时应用的附加约束
    • 如果未指定,则通过 foreignKeylocalKey 列创建一个连接
    • 仅在执行 has() / whereHas() 时执行此操作
  7. localKey:

    • 可选的
    • 指定用于查询的本地键(主键)列。可以指定表和键,或者只指定列名,例如 'products.id'/'id'
    • 如果未指定,则获取父模型的键
    • 此键将用于所有默认关系逻辑。如果提供了所有其他闭包,则不需要此键,例如 singleConstraints, eagerConstraints, eagerMatcher 和 existenceJoin
  8. foreignKey:

    • 可选的
    • 指定将在所有默认逻辑中使用的 foreignKey。在大多数情况下,需要一个表和列点表示法键,例如 'orders.user_id'
    • 如果未指定,则 foreignKey 将设置为 null,因为无法猜测正确的键。
    • localKey 类似,foreignKey 也将用于所有内部关系生命周期逻辑。

测试关系

在创建您的 baseConstraints 查询后,建议一次测试一种关系类型。您可以从测试单个 Model 上的关系开始,然后进行懒加载等测试。如果默认逻辑不符合要求,您可能需要提供一个自定义闭包来控制关系的那一部分。此包在覆盖基本逻辑方面很灵活,但能够为关系的每个部分提供定制代码。

关系调试

建议使用某种查询调试包,例如 Clockwork,以及闭包等中的典型 dd()