Laravel基于子表值实现父表高级搜索问题求助
问题
现有products表和custom_details表,两者为一对多关联,custom_details的data_type分为number、date、text三种类型。需要实现一个高级搜索接口,接收客户端传入的过滤条件,仅返回满足所有过滤条件的products,同时支持基于custom_details字段排序。
表结构
products表
------------------------------- |id | name | description | | 1 | product1 | | | 2 | product2 | | | 3 | product3 | | | 4 | product4 | | -------------------------------
custom_details表
------------------------------------------------------------------------------------- | id | detailable_id | detailable_type | data_type | name | value | | 1 | 1 | App\Models\Product | number | length | 10 | | 2 | 1 | App\Models\Product | number | height | 19 | | 3 | 1 | App\Models\Product | text | factory | usa | | 4 | 1 | App\Models\Product | date | release_date | 2024-01-01 | | 5 | 2 | App\Models\Product | number | length | 20 | | 6 | 2 | App\Models\Product | number | height | 50 | | 7 | 2 | App\Models\Product | text | factory | india | | 8 | 2 | App\Models\Product | date | release_date | 2024-02-01 | | 9 | 3 | App\Models\Product | number | length | 30 | | 10 | 3 | App\Models\Product | number | height | 33 | | 11 | 3 | App\Models\Product | text | factory | philippines | | 12 | 3 | App\Models\Product | date | release_date | 2024-03-01 | | 13 | 4 | App\Models\Product | number | length | 22 | | 14 | 4 | App\Models\Product | number | height | 68 | | 15 | 4 | App\Models\Product | text | factory | hawai | | 16 | 4 | App\Models\Product | date | release_date | 2024-04-01 | -------------------------------------------------------------------------------------
客户端过滤条件示例
'filters' => [ [ 'name' => 'length', 'type' => 'number', 'min' => 15, 'max' => 30, 'order' => 'asc', ], [ 'name' => 'factory', 'type' => 'text', 'values' => ['usa', 'hawai'], ], [ 'name' => 'release_date', 'type' => 'date', 'min' => '2024-02-01', 'max' => '2024-05-06', ], ],
当前实现问题
当前代码用orWhere导致返回满足至少一个过滤条件的产品,改成where后无结果返回。原代码如下:
$query ->select("products.*") ->with(['customDetails', 'category']) ->join('custom_details', function ($query) use ($filters) { $query->on('products.id', '=', 'custom_details.detailable_id') ->where('custom_details.detailable_type', '=', 'App\Models\Product'); if (count($filters) > 0) { $query->where(function ($query) use ($filters) { foreach ($filters as $key => $filter) { $query->orWhere(function ($query) use ($filter) { $query->where('custom_details.name', $filter['name']) ->where('custom_details.type', $filter['type']); $query->where(function ($query) use ($filter) { switch ($filter['type']) { case 'text': if (isset($filter['values']) && $filter['values']) { $query->whereIn('custom_details.value', $filter['values']); } break; case 'date': if (isset($filter['min']) && $filter['min']) { $query->whereDate('custom_details.value', '>=' ,$filter['min']); } if (isset($filter['max']) && $filter['max']) { $query->whereDate('custom_details.value', '<=' ,$filter['min']); } break; case 'number': if (isset($filter['min']) && $filter['min']) { $query->where('custom_details.value', '>=' ,$filter['min']); } if (isset($filter['max']) && $filter['max']) { $query->where('custom_details.value', '<=' ,$filter['max']); } break; } }); }); } }); } }); foreach ($filters as $key => $filter) { if (isset($filter['order'])) { $query->orderBy('custom_details.value', $filter['order']); } }
解决方案
要实现所有过滤条件都满足的查询,核心思路是:每个产品必须匹配每一条过滤规则,不能用单表join直接匹配(因为一条custom_details记录只能对应一个属性),可以用以下两种方式实现:
方式一:使用EXISTS子查询(性能更优)
对每个过滤条件,单独判断产品是否存在符合条件的custom_details记录,所有条件都满足才保留。同时修复原代码中的字段和逻辑错误。
$query = Product::query() ->select("products.*") ->with(['customDetails', 'category']); // 处理过滤条件 foreach ($filters as $filter) { $query->whereExists(function ($subQuery) use ($filter) { $subQuery->select(DB::raw(1)) ->from('custom_details') ->where('custom_details.detailable_id', '=', DB::raw('products.id')) ->where('custom_details.detailable_type', '=', 'App\Models\Product') ->where('custom_details.name', $filter['name']) ->where('custom_details.data_type', $filter['type']); // 修正字段名:原代码用了type,实际是data_type // 根据类型添加过滤规则 switch ($filter['type']) { case 'text': if (!empty($filter['values'])) { $subQuery->whereIn('custom_details.value', $filter['values']); } break; case 'date': if (!empty($filter['min'])) { $subQuery->whereDate('custom_details.value', '>=', $filter['min']); } if (!empty($filter['max'])) { // 修正逻辑错误:原代码把max判断写成了min $subQuery->whereDate('custom_details.value', '<=', $filter['max']); } break; case 'number': if (!empty($filter['min'])) { $subQuery->where('custom_details.value', '>=', $filter['min']); } if (!empty($filter['max'])) { $subQuery->where('custom_details.value', '<=', $filter['max']); } break; } }); } // 处理排序:单独关联排序字段,避免重复数据 foreach ($filters as $filter) { if (isset($filter['order'])) { $query->leftJoin('custom_details as cd_sort', function ($join) use ($filter) { $join->on('products.id', '=', 'cd_sort.detailable_id') ->where('cd_sort.detailable_type', '=', 'App\Models\Product') ->where('cd_sort.name', $filter['name']) ->where('cd_sort.data_type', $filter['type']); }) // 根据数据类型转换后排序,避免字符串排序错误 ->orderBy(DB::raw("CAST(cd_sort.value AS {$filter['type']})"), $filter['order']) ->selectRaw('products.*'); // 确保只返回products表字段 } } $products = $query->distinct()->get();
方式二:分组统计+Having
通过join关联所有符合条件的custom_details记录,分组后统计每个产品匹配的过滤条件数量,只有数量等于总过滤条件数的产品才保留。
$query = Product::query() ->select("products.*") ->with(['customDetails', 'category']) ->join('custom_details', function ($join) { $join->on('products.id', '=', 'custom_details.detailable_id') ->where('custom_details.detailable_type', '=', 'App\Models\Product'); }); $filterCount = count($filters); if ($filterCount > 0) { $query->where(function ($q) use ($filters) { foreach ($filters as $filter) { $q->orWhere(function ($q) use ($filter) { $q->where('custom_details.name', $filter['name']) ->where('custom_details.data_type', $filter['type']); switch ($filter['type']) { case 'text': if (!empty($filter['values'])) { $q->whereIn('custom_details.value', $filter['values']); } break; case 'date': if (!empty($filter['min'])) { $q->whereDate('custom_details.value', '>=', $filter['min']); } if (!empty($filter['max'])) { $q->whereDate('custom_details.value', '<=', $filter['max']); } break; case 'number': if (!empty($filter['min'])) { $q->where('custom_details.value', '>=', $filter['min']); } if (!empty($filter['max'])) { $q->where('custom_details.value', '<=', $filter['max']); } break; } }); } }) ->groupBy('products.id') // 确保产品匹配了所有过滤条件 ->havingRaw('COUNT(DISTINCT custom_details.name) = ?', [$filterCount]); } // 处理排序(同方式一) foreach ($filters as $filter) { if (isset($filter['order'])) { $query->leftJoin('custom_details as cd_sort', function ($join) use ($filter) { $join->on('products.id', '=', 'cd_sort.detailable_id') ->where('cd_sort.detailable_type', '=', 'App\Models\Product') ->where('cd_sort.name', $filter['name']) ->where('cd_sort.data_type', $filter['type']); }) ->orderBy(DB::raw("CAST(cd_sort.value AS {$filter['type']})"), $filter['order']) ->selectRaw('products.*'); } } $products = $query->get();
关键修正点
- 字段名错误:原代码中
custom_details.type应为custom_details.data_type,与表结构字段一致。 - 日期逻辑错误:原代码中日期的max判断误用了
$filter['min'],已修正为$filter['max']。 - 过滤逻辑调整:从
OR匹配改为每个条件单独用EXISTS验证,确保产品满足所有过滤规则。 - 排序优化:单独关联排序字段并转换数据类型后排序,避免字符串排序导致的数字/日期排序错误,同时用
distinct或selectRaw避免重复数据。
内容的提问来源于stack exchange,提问作者ephraim lambarte
相关产品推荐
相关产品推荐

