Laravel如何设计代码结构实现嵌套JSON的新增与upsert存储并保留关联ID
现有代码可优化点
- 所有嵌套处理逻辑耦合在Template模型中,不符合「每个Model负责自身数据存储」的要求
- 多次调用
refresh()产生不必要的查询,性能损耗高 - 子模型关联字段需要手动
associate,重复代码多且容易出错 upsert方法未指定唯一冲突键,无法保证原有ID不被修改,也可能执行失败
优化实现方案
核心思路:把嵌套存储逻辑拆分到每个子模型,每个模型仅负责自身字段存储 + 直接下级关联的处理,上层模型不用感知下层的嵌套结构。
1. Template 模型实现
<?php class Template extends Model { protected $table = 'templates'; protected $primaryKey = 'templates_id'; protected $guarded = []; // 你已配置的关联定义 public function pages() { return $this->hasMany(TemplatesPage::class, 'templates_id', 'templates_id'); } // 嵌套保存统一入口,兼容新增/更新 public function saveNested(array $data) { // 保存自身非嵌套字段 $this->fill(collect($data)->except('pages')->toArray())->save(); // 组装页面upsert数据,自动注入当前模板ID $pageUpsertData = collect($data['pages'])->map(function ($pageItem) { $pageItem['templates_id'] = $this->templates_id; return collect($pageItem)->except('components')->toArray(); })->toArray(); // 页面upsert,唯一冲突键保证原有pages_id不变 TemplatesPage::upsert($pageUpsertData, ['templates_id', 'pages_id']); // 拿到当前模板的所有关联页面,逐个处理下级嵌套 $this->pages()->whereIn('pages_id', collect($data['pages'])->pluck('pages_id')) ->get() ->each(function (TemplatesPage $page, $index) use ($data) { $page->saveNested($data['pages'][$index]); }); } }
2. TemplatesPage 模型实现
<?php class TemplatesPage extends Model { protected $table = 'templates_pages'; protected $primaryKey = 'pages_id'; protected $guarded = []; public function components() { return $this->hasMany(TemplatesComponent::class, 'pages_id', 'pages_id'); } public function saveNested(array $data) { // 保存页面自身字段 $this->fill(collect($data)->except('components')->toArray())->save(); // 组装组件upsert数据,自动注入关联ID $componentUpsertData = collect($data['components'])->map(function ($componentItem) { $componentItem['templates_id'] = $this->templates_id; $componentItem['pages_id'] = $this->pages_id; return collect($componentItem)->except('lines')->toArray(); })->toArray(); // 组件upsert,保留原有components_id TemplatesComponent::upsert($componentUpsertData, ['pages_id', 'components_id']); // 处理组件下的嵌套线条 $this->components()->whereIn('components_id', collect($data['components'])->pluck('components_id')) ->get() ->each(function (TemplatesComponent $component, $index) use ($data) { $component->saveNested($data['components'][$index]); }); } }
3. TemplatesComponent 模型实现
<?php class TemplatesComponent extends Model { protected $table = 'templates_components'; protected $primaryKey = 'components_id'; protected $guarded = []; public function lines() { return $this->hasMany(TemplatesComponentsLine::class, 'components_id', 'components_id'); } public function saveNested(array $data) { // 保存组件自身字段 $this->fill(collect($data)->except('lines')->toArray())->save(); // 组装线条upsert数据,自动注入关联ID $lineUpsertData = collect($data['lines'])->map(function ($lineItem) { $lineItem['templates_id'] = $this->templates_id; $lineItem['pages_id'] = $this->pages_id; $lineItem['components_id'] = $this->components_id; return $lineItem; })->toArray(); // 线条upsert,保留原有lines_id TemplatesComponentsLine::upsert($lineUpsertData, ['components_id', 'lines_id']); } }
4. 控制器调用示例
// 拿到请求的完整JSON数据后直接调用即可 $template = Template::findOrFail($request->input('templates_id')); $template->saveNested($request->all());
方案优势
- 每个模型仅负责自身和直接下级的存储逻辑,符合单一职责,后续加新的嵌套层级只需修改对应模型即可
- 自动注入关联外键,不需要手动调用
associate,避免漏传关联字段出错 - 所有
upsert都指定了唯一冲突键,保证原有ID不会被修改,完全符合需求 - 去掉了不必要的
refresh调用,大幅减少多余的数据库查询,性能更高 - 新增和更新逻辑合并,不需要分开写两套方法,减少重复代码
内容的提问来源于stack exchange,提问作者flaxon
相关产品推荐
相关产品推荐

