
本文详解如何在 Laravel 中为 Product 模型优雅获取“基础单位 + 所有子单位”的扁平化单位列表,通过组合 belongsTo、hasMany 关系及模型访问器实现无需复杂 SQL 即可完成多层级单位聚合。
本文详解如何在 laravel 中为 product 模型优雅获取“基础单位 + 所有子单位”的扁平化单位列表,通过组合 `belongsto`、`hasmany` 关系及模型访问器实现无需复杂 sql 即可完成多层级单位聚合。
在实际业务中(如商品库存、计量单位管理),常需将一个基础单位(如 "Piece")与其所有子单位(如 "Dozen - 12"、"Case - 144")统一聚合展示。但 Laravel 的标准 belongsTo 关系仅支持单字段外键绑定,无法直接表达「某 Product 的 unit_id 既对应自身单位,又作为 base_unit_id 匹配其子单位」这一双向语义。本文提供一套清晰、可维护且符合 Eloquent 设计哲学的解决方案。
✅ 核心思路:分层建模 + 聚合访问器
不强行用单一关系覆盖全部逻辑,而是分步构建:
- Product → baseUnit:标准一对一关系,指向其基础计量单位;
- Unit → subUnits:一对多自关联,查出所有以该单位为 base_unit_id 的子单位;
- 最终通过模型访问器(Accessor)合并并扁平化结果。
1. 定义模型关系
app/Models/Product.php
<?php namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
public function baseUnit()
{
return $this->belongsTo(Unit::class, 'unit_id', 'id');
}
// 访问器:返回包含 baseUnit 及其所有 subUnits 的扁平集合
public function getUnitsAttribute()
{
if (! $this->relationLoaded('baseUnit')) {
$this->load('baseUnit.subUnits');
}
$base = $this->baseUnit->replicate(); // 避免污染原始关系实例
$base->unsetRelation('subUnits'); // 清除嵌套关系,保持扁平结构
return $this->baseUnit->subUnits->merge($base)->sortByDesc('multiplier');
}
}
app/Models/Unit.php
<?php namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Unit extends Model
{
public function subUnits()
{
return $this->hasMany(Unit::class, 'base_unit_id', 'id');
}
public function baseUnit()
{
return $this->belongsTo(Unit::class, 'base_unit_id', 'id');
}
}
? 注意:subUnits 是典型的自关联 hasMany,关键在于外键 base_unit_id 指向父单位的 id;而 baseUnit() 则用于反向查询(如需追溯上级单位)。
2. 使用方式(简洁高效)
// 一次性预加载 + 自动聚合
$products = Product::with('baseUnit.subUnits')->get();
foreach ($products as $product) {
echo $product->name . " uses units:\n";
foreach ($product->units as $unit) {
echo "- {$unit->name} (×{$unit->multiplier})\n";
}
}
输出结构完全匹配预期(扁平数组、含全部单位):
[
{
"id": 1,
"name": "Product 1",
"unit_id": 1,
"cost": 10,
"price": 14,
"units": [
{ "id": 2, "name": "Dozen - 12", "multiplier": 12, "base_unit_id": 1 },
{ "id": 1, "name": "Piece", "multiplier": 1, "base_unit_id": null }
]
}
]
3. 进阶优化建议
- 性能考量:若子单位数量庞大,可在 subUnits() 关系中添加 ->whereNotNull('base_unit_id') 显式过滤;
- 缓存友好:对频繁访问的单位树,可结合 Cache::remember() 缓存 units 属性结果;
- API 序列化控制:在 Product 的 toArray() 或资源类(Resource)中显式调用 $product->units,避免意外触发 N+1;
- 类型安全(PHP 8+):为 units 访问器添加返回类型声明 public function getUnitsAttribute(): Collection。
⚠️ 注意事项
- ❌ 不要尝试在 belongsTo 中使用 orWhere() —— Eloquent 关系构造器不支持运行时条件拼接,会导致查询逻辑错误或报错;
- ❌ 避免在 with() 中直接写 with('units') 并期望自动识别多条件关系,Eloquent 无法解析此类隐式语义;
- ✅ 始终优先使用 replicate() 复制模型实例,防止修改原始关系数据影响后续逻辑;
- ✅ 推荐将 units 定义为 getUnitsAttribute()(访问器),而非普通方法,使其天然支持 toArray()、API Resource 和 Blade 模板中的 $product->units 调用。
通过分层建模与访问器聚合,你不仅解决了“双字段关联”的表意难题,更获得了高内聚、易测试、可扩展的领域模型设计 —— 这正是 Laravel Eloquent 灵活性与工程实践的最佳结合。











