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
相关产品推荐
相关产品推荐

