Laravel多对多关系下按最新状态筛选订单并实现分页
解决Laravel多对多关系下筛选订单最新状态并分页的问题
你的核心问题在于:直接用with('latestStatuses')预加载关联无法实现基于最新状态的精准筛选,且如果在加载后过滤会破坏分页逻辑(分页是基于未过滤的全量订单计算的)。要解决这个问题,需要在主查询中直接关联并筛选每个订单的最新状态记录,确保分页是基于筛选后的结果。
解决方案:子查询关联筛选最新状态
方法1:使用whereNotExists获取最新状态记录
通过子查询找到每个订单的最新状态变更记录(即不存在比它更晚的状态记录),再将其与订单表关联,筛选目标状态ID:
$targetStatusIds = [1, 2]; $pageSize = 10; // 子查询:获取每个订单的最新状态ID $latestStatusSubquery = DB::table('order_status as os1') ->select('os1.order_id', 'os1.status_id') ->whereNotExists(function ($query) { $query->from('order_status as os2') ->whereColumn('os2.order_id', 'os1.order_id') ->where('os2.created_at', '>', 'os1.created_at'); }); // 关联子查询并筛选,同时执行分页 $orders = Order::query() ->joinSub( $latestStatusSubquery, 'latest_status', 'latest_status.order_id', '=', 'orders.id' ) ->whereIn('latest_status.status_id', $targetStatusIds) ->select('orders.*') // 仅选择订单表字段,避免字段冲突 ->latest('orders.id') ->paginate($pageSize); // 预加载最新状态数据(可选,用于后续业务逻辑) $orders->load('latestStatuses');
方法2:使用窗口函数(MySQL 8.0+ 支持)
利用ROW_NUMBER()窗口函数给每个订单的状态变更记录按创建时间倒序排名,取排名为1的最新记录:
$targetStatusIds = [1, 2]; $pageSize = 10; $orders = Order::query() ->joinSub( DB::table('order_status') ->select( 'order_id', 'status_id', DB::raw('ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY created_at DESC) as rn') ), 'status_ranked', 'status_ranked.order_id', '=', 'orders.id' ) ->where('status_ranked.rn', 1) // 仅取最新状态 ->whereIn('status_ranked.status_id', $targetStatusIds) ->select('orders.*') ->latest('orders.id') ->paginate($pageSize); $orders->load('latestStatuses');
为什么原方法失效?
你定义的latestStatuses关联用了limit(1),它仅用于预加载单个最新状态数据,但无法用于筛选:
- 如果用
whereHas('latestStatuses', function($q) use ($targetStatusIds) { ... }),逻辑是判断订单是否存在任意一个状态在目标集合中,而非最新的那个状态在集合中。 - 若先查询所有订单再过滤最新状态,分页会基于全量订单计算,导致分页结果不准确(比如每页10条,过滤后可能只剩5条,分页逻辑混乱)。
优化模型关联(可选)
如果需要频繁获取订单最新状态,可以在Order模型中定义一个单数关联latestStatus,直接关联最新状态:
public function latestStatus() { return $this->hasOne(Status::class) ->joinSub( DB::table('order_status') ->select('order_id', 'status_id') ->whereNotExists(function ($query) { $query->from('order_status as os2') ->whereColumn('os2.order_id', 'order_status.order_id') ->where('os2.created_at', '>', 'order_status.created_at'); }), 'latest_os', function ($join) { $join->on('latest_os.order_id', '=', 'orders.id') ->on('latest_os.status_id', '=', 'statuses.id'); } ); }
之后可以简化查询:
$orders = Order::query() ->with('latestStatus') ->whereHas('latestStatus', function($q) use ($targetStatusIds) { $q->whereIn('id', $targetStatusIds); }) ->latest('id') ->paginate($pageSize);
内容的提问来源于stack exchange,提问作者Jafrul Hossain
相关产品推荐
相关产品推荐

