在Laravel项目开发中,多表关联查询是高频需求,其中查询指定分类下未被某订单关联的分包商属于典型的5表关联去重场景,需要理清表之间的关联关系才能写出正确的查询逻辑。

业务场景与表结构说明
我们先明确本次需求的业务背景,假设存在以下5张核心表,表之间的关联关系如下:
- 分类表
categories:存储分包商的分类信息,主键为id - 分包商表
suppliers:存储分包商基础信息,主键为id,通过category_id关联分类表 - 订单表
orders:存储订单基础信息,主键为id - 订单分包商关联表
order_supplier:存储订单和分包商的关联关系,字段为order_id和supplier_id - 分包商资质表
supplier_qualifications:存储分包商的资质信息,通过supplier_id关联分包商表,一个分包商可能有多个资质记录
需求目标是:查询分类ID为指定值,且没有被某个指定订单ID关联的所有分包商,同时需要避免因为分包商资质表的多条记录导致分包商数据重复。
查询逻辑拆解
要实现这个需求,我们可以把查询拆解为三个核心步骤:
- 先筛选出指定分类下的所有分包商,关联分包商资质表,此时可能因为资质表的多条记录出现分包商重复
- 查询出指定订单已经关联的所有分包商ID集合
- 从第一步的结果中排除第二步得到的分包商ID,同时做去重处理
基础查询实现
首先我们使用Laravel的查询构造器实现基础查询逻辑,代码如下:
<?php
// 指定分类ID
$categoryId = 2;
// 指定订单ID
$orderId = 10;
// 第一步:查询指定分类下的分包商,关联资质表
$supplierQuery = DB::table('suppliers')
->join('categories', 'suppliers.category_id', '=', 'categories.id')
->leftJoin('supplier_qualifications', 'suppliers.id', '=', 'supplier_qualifications.supplier_id')
->where('categories.id', $categoryId);
// 第二步:查询指定订单已关联的分包商ID
$relatedSupplierIds = DB::table('order_supplier')
->where('order_id', $orderId)
->pluck('supplier_id')
->toArray();
// 第三步:排除已关联的分包商,去重后获取结果
$result = $supplierQuery
->whereNotIn('suppliers.id', $relatedSupplierIds)
->select('suppliers.id', 'suppliers.name', 'suppliers.contact')
->distinct()
->get();
print_r($result);
Eloquent ORM实现方式
如果项目中已经定义了对应的模型,使用Eloquent ORM可以让代码更简洁,首先定义模型关联关系:
<?php
// Supplier模型
namespace AppModels;
use IlluminateDatabaseEloquentModel;
class Supplier extends Model
{
public function category()
{
return $this->belongsTo(Category::class, 'category_id');
}
public function qualifications()
{
return $this->hasMany(SupplierQualification::class, 'supplier_id');
}
public function orders()
{
return $this->belongsToMany(Order::class, 'order_supplier', 'supplier_id', 'order_id');
}
}
// Order模型
namespace AppModels;
use IlluminateDatabaseEloquentModel;
class Order extends Model
{
public function suppliers()
{
return $this->belongsToMany(Supplier::class, 'order_supplier', 'order_id', 'supplier_id');
}
}
基于模型实现查询的代码如下:
<?php
use AppModelsSupplier;
use AppModelsOrder;
$categoryId = 2;
$orderId = 10;
// 获取指定订单已关联的分包商ID
$order = Order::find($orderId);
$relatedSupplierIds = $order->suppliers()->pluck('suppliers.id')->toArray();
// 查询未被关联的分包商
$result = Supplier::whereHas('category', function ($query) use ($categoryId) {
$query->where('id', $categoryId);
})
->whereNotIn('id', $relatedSupplierIds)
->with('qualifications') // 按需加载资质关联
->distinct()
->get();
print_r($result);
去重方案说明
本次需求中需要去重的核心原因是分包商表和资质表是一对多关系,左连接资质表后,一个分包商会对应多条记录。我们使用distinct()方法对分包商的主键做去重,同时在select中只选择分包商表的字段,避免资质表的字段导致去重失效。如果查询中需要用到资质表的聚合字段,比如资质数量,可以改用groupBy('suppliers.id')的方式实现去重,代码如下:
<?php
$result = DB::table('suppliers')
->join('categories', 'suppliers.category_id', '=', 'categories.id')
->leftJoin('supplier_qualifications', 'suppliers.id', '=', 'supplier_qualifications.supplier_id')
->where('categories.id', 2)
->whereNotIn('suppliers.id', function ($query) {
$query->select('supplier_id')
->from('order_supplier')
->where('order_id', 10);
})
->select('suppliers.id', 'suppliers.name', DB::raw('count(supplier_qualifications.id) as qualification_count'))
->groupBy('suppliers.id', 'suppliers.name')
->get();
性能优化建议
针对这类多表关联查询,我们可以从以下几个方面做性能优化:
- 给关联字段添加索引,比如
suppliers.category_id、order_supplier.order_id、order_supplier.supplier_id、supplier_qualifications.supplier_id - 如果指定订单关联的分包商数量较多,
whereNotIn可能会有性能问题,可以改用leftJoin加whereNull的方式实现排除逻辑 - 只查询需要的字段,避免使用
select *,减少数据传输和内存占用
注意:如果分包商和订单的关联关系存在软删除逻辑,需要在查询时加上软删除的过滤条件,避免查询到已经被删除的关联记录。