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

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();

关键修正点

  1. 字段名错误:原代码中custom_details.type应为custom_details.data_type,与表结构字段一致。
  2. 日期逻辑错误:原代码中日期的max判断误用了$filter['min'],已修正为$filter['max']。
  3. 过滤逻辑调整:从OR匹配改为每个条件单独用EXISTS验证,确保产品满足所有过滤规则。
  4. 排序优化:单独关联排序字段并转换数据类型后排序,避免字符串排序导致的数字/日期排序错误,同时用distinct或selectRaw避免重复数据。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 07:05:54