【问题标题】:whereHas vs join -> applying global scopeswhereHas vs join -> 应用全局范围
【发布时间】:2020-04-29 19:01:17
【问题描述】:

我有一个查询,每个模型都有一些连接和全局范围,例如:

SELECT *
FROM products p
WHERE EXISTS(
  SELECT *
  FROM orders o
  WHERE o.user_id = 4
  AND o.status_id = 1
  AND o.user_id = 3
  AND EXISTS(
    SELECT *
    FROM suborders s
    WHERE s.status_id = 2
  )
);

这意味着我可以简单地编写一些whereHas 语句,并且我的查询将包含一些嵌套的EXIST 子句,但是所有全局范围(如orders 表上的user_id)都将自动应用:

$this->builder->whereHas('orders', function ($q) {
  $q->where('status_id', '=', 1)
    ->whereHas('suborder', function ($q) {
      $q->where('status_id', '=', 2);
    });
});

问题是它很慢,使用普通的JOINs 而不是丑陋的嵌套EXIST 子句会更好:

SELECT *
FROM products p
INNER JOIN orders o ON p.order_id = o.id
INNER JOIN suborders s ON o.id = s.order_id
WHERE o.status_id = 1
AND u.user_id = 3
AND s.status_id = 2;

这个问题是我需要使用查询生成器来加入这些:

$this->builder->join('orders', 'products.order_id', '=', 'orders.id')
              ->join('suborders', 'orders.id', '=', 'suborders.order_id')
              ->where('orders.status_id', 1)
              ->where('suborders.id', 2);

这不包括我在OrderSuborder 模型上的任何全局范围。我需要手动完成:

$this->builder->join('orders', 'products.order_id', '=', 'orders.id')
              ->join('suborders', 'orders.id', '=', 'suborders.order_id')
              ->where('orders.status_id', 1)
              ->where('suborders.id', 2)
              ->where('orders.user_id', 3);

这很糟糕,因为我每次编写这样的查询时都需要复制全局范围逻辑,而 whereHas 会自动应用它们。

有没有办法连接一个表,并自动应用连接模型中的所有全局范围?

【问题讨论】:

    标签: php mysql sql laravel eloquent


    【解决方案1】:

    我之前曾处理过类似的问题并想出了一些方法。

    首先,让我们在服务提供者中为 Illuminate\Database\Eloquent\Builder 定义一个宏:

    use Illuminate\Database\Eloquent\Builder;
    use Illuminate\Database\Query\JoinClause;
    
    class AppServiceProvider
    {
        public function boot()
        {
            Builder::macro('hasJoinedWith', function ($table) {
                return collect(
                    $this->getQuery()->joins
                )
                ->contains(function (JoinClause $joinClause) use ($table) {
                    return $joinClause->table === $table;
                })
            });
        }
    }
    

    然后,让我们定义范围:

    class Product extends Model
    {
        public function scopeOrderStatus($query, $orderStatusId)
        {
            if (! $query->hasJoinedWith('orders')) {
                $query->join('orders', 'products.order_id', '=', 'orders.id');
            }
    
            return $query->where('orders.status_id', $orderStatusId)
        }
    
        public function scopeOrderUser($query, $userId)
        {
            if (! $query->hasJoinedWith('orders')) {
                $query->join('orders', 'products.order_id', '=', 'orders.id');
            }
    
            return $query->where('orders.user_id', $userId)
        }
    
        public function scopeSubOrder($query, $subOrderId)
        {
            if (! $query->hasJoinedWith('orders')) {
                $query->join('orders', 'products.order_id', '=', 'orders.id');
            }
    
            if (! $query->hasJoinedWith('suborders')) {
                $query->join('suborders', 'orders.id', '=', 'suborders.order_id');
            }
    
            return $query->where('suborders.id', $subOrderId)
        }
    }
    

    最后,你可以一起使用作用域:

    Product::orderStatus(1)
        ->subOrder(2)
        ->orderUser(3)
        // This is optional. There are possibly duplicate products.
        ->distinct()
        ->get();
    

    这是我能想到的最好的方法,可能还有更好的方法。

    【讨论】:

      【解决方案2】:

      实际上,您所描述的不可能是真的(将在下面提供证据)。 EXISTS 不要让查询变慢,在 99% 的情况下它会变快! 所以 laravel 不提供从盒子连接关系的能力。

      我在github上看到了不同的解决方案和包,但我不会提供它的链接,因为我在审查逻辑时发现了很多字段选择和罕见情况的问题。

      1. Laravel 不会像您描述的那样使用 EXISTS 生成代码,它会按 ID 向每个 EXISTS 子查询添加关系搜索,例如
      SELECT *
      FROM products
      WHERE EXISTS(
        SELECT *
        FROM orders
        WHERE o.user_id = 4
        AND o.status_id = 1
        AND o.id = p.order_id
        AND EXISTS(
          SELECT *
          FROM suborders s
          WHERE s.status_id = 2
          AND s.id = o.suborder_id
        )
      );
      

      注意AND o.id = p.order_idAND s.id = o.suborder_id

      1. 当您使用连接进行选择时,您应该从主表中准确设置SELECT,以正确填充模型字段。

      2. 全局范围是全局的。目标是真正全球化。如果您有超过 1-2 个地方没有它们,那么您应该找到另一个解决方案而不是全局范围。否则,您的应用程序将很难支持和编写新代码。开发人员不应该每次都记住,可能有他应该关闭的全局范围

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2017-08-18
        • 1970-01-01
        • 2016-05-29
        • 2011-03-17
        • 2014-02-22
        • 2018-03-13
        • 2021-05-03
        相关资源
        最近更新 更多