You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Laravel多关联查询去重:如何保留分页结构解决重复数据问题

解决Spatie Laravel Query Builder过滤/排序时的重复数据问题(保留分页)

你遇到的核心问题是多表关联查询导致主表accommodations记录重复,直接用->unique()又会丢失分页结构。下面给你几个实用的解决方案,从根源优化到应急处理都覆盖:

一、根源优化:用exists子查询替代join(推荐)

你的FilterPrice和FilterDate scope使用了leftjoin,这会因为一个住宿对应多个房间、价格或容量记录,导致主表数据被重复查询。换成exists子查询可以彻底避免这个问题,同时保证分页计数准确:

重构scopeFilterPrice

public function scopeFilterPrice(Builder $query, $start_price, $end_price): Builder {
    return $query->whereExists(function ($subQuery) use ($start_price, $end_price) {
        $subQuery->select(DB::raw(1))
            ->from('accommodation_rooms as ar')
            ->join('room_pricing_histories as rph', 'ar.id', '=', 'rph.accommodation_room_id')
            ->whereColumn('ar.accommodation_id', 'accommodations.id')
            ->where('rph.sales_price', '>', $start_price)
            ->where('rph.sales_price', '<', $end_price);
    });
}

重构scopeFilterDate

public function scopeFilterDate(Builder $query,$from_date,$to_date): Builder{
    return $query->whereExists(function ($subQuery) use ($from_date, $to_date) {
        $subQuery->select(DB::raw(1))
            ->from('accommodation_rooms as br')
            ->join('room_capacity_histories as rch', 'br.id', '=', 'rch.accommodation_room_id')
            ->whereColumn('br.accommodation_id', 'accommodations.id')
            ->whereDate('rch.from_date', '>', $from_date)
            ->whereDate('rch.to_date', '<', $to_date);
    });
}

二、排序逻辑优化:用子查询获取排序字段

你原来的排序通过join关联房间和价格表,同样会导致主表重复。改成子查询获取每个住宿的对应价格,再基于这个值排序:

重构自定义排序类(以PriceSort为例)

use Illuminate\Database\Eloquent\Builder;

class PriceSort implements \Spatie\QueryBuilder\Sorts\Sort
{
    public function __invoke(Builder $query, bool $descending)
    {
        $direction = $descending ? 'DESC' : 'ASC';
        
        return $query->select('accommodations.*')
            // 子查询获取该住宿的最低销售价(可根据需求改成MAX)
            ->selectSub(function ($sub) {
                $sub->selectRaw('MIN(rph.sales_price)')
                    ->from('accommodation_rooms as ar')
                    ->join('room_pricing_histories as rph', 'ar.id', '=', 'rph.accommodation_room_id')
                    ->whereColumn('ar.accommodation_id', 'accommodations.id');
            }, 'sort_price')
            ->orderBy('sort_price', $direction);
    }
}

这样排序时不会关联主表产生重复,分页的总条数和返回数据都是准确的。

三、应急方案:返回Resource前重构分页实例(不推荐但可用)

如果暂时无法调整查询逻辑,可以在返回Resource前,对分页集合的items去重,同时保留分页的元数据(总条数、页码等):

public function scopeFilter() { 
    $data = QueryBuilder::for(Accommodation::class) 
        ->allowedFilters([ 
            // ... 你的过滤器配置
        ]) 
        ->allowedAppends(['cheapestroom']) 
        ->allowedIncludes(['gallery','city','accommodationRooms','accommodationRooms.roomPricingHistorySearch','discounts','prices']) 
        ->allowedSorts([ 
            AllowedSort::custom('discount', new DiscountSort() ,'amount'),
            AllowedSort::custom('price', new PriceSort() ,'price'),
        ]) 
        ->paginate(12); 

    // 基于ID去重items
    $uniqueItems = $data->items()->unique('id');
    
    // 重构分页实例,保留原分页的元数据
    $paginatedData = new \Illuminate\Pagination\LengthAwarePaginator(
        $uniqueItems,
        $data->total(),
        $data->perPage(),
        $data->currentPage(),
        [
            'path' => request()->path(),
            'query' => request()->query(),
        ]
    );

    return FilterResource::collection($paginatedData); 
}

⚠️ 注意:这个方法的缺陷是分页总条数还是包含重复记录的统计值,可能导致最后一页返回的数量少于每页设定的条数。所以优先推荐前两种查询层优化方案。

内容的提问来源于stack exchange,提问作者Farshad

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:27:01