5.4 预加载、访问器与事务批量浇筑


5.4 预加载、访问器与事务批量浇筑

本节摘要:数据层收尾的三门功夫:预加载根治关联查询的 N+1 顽疾,访问器与修改器统一字段的读写口径,事务与批量操作保证多步写库要么整体成功、要么整体不动。本节用上一节留下的"41 条 SQL"现场开工修复,再以一笔"预算扣减加建单"的写库流程演示事务封装,读完你能给数据层做一次完整的加固。

收尾之前,先把两个隐患摆上台面

前几节的数据层已经能用了,但埋着两颗雷。一是性能雷:动态属性每访问一次就查一次库,列表页循环访问就是 N+1,5.2 节旁听过 41 条 SQL 的现场;二是一致性雷:多步写库做到一半失败(扣了预算、单子没建成),数据从此对不上账。本节三件事——预加载、访问器、事务——分别是这两颗雷的拆除工具加上一把日常的小扳手。

预加载:把 N+1 压成常数条

with 把关联查询提前、合并、批量执行:

<?php // 修复 5.2 节的列表:关联一次取回,整页三条 SQL $orders = WorkOrder::query() ->with('team.workers') // 嵌套预加载:队组连带队员 ->withCount('workers') ->orderByDesc('created_at') ->paginate(20); // 惰性预加载:模型已经在手,补一枪批量查询 $orders->load('team'); // 带条件的预加载:只装在场工人 $orders->load(['workers' => fn ($q) => $q->wherePivot('role', '电工')]);

根治手段是防止漏网:模型里声明 $with 属性,这个模型的查询默认就带指定关联;想彻底封死动态属性偷查,可以在模型上调用 preventLazyLoading(开发环境开启),漏加载直接抛异常,问题在开发期暴露而不是上线后。

<?php // 顺手把常用查询收进作用域:控制器只说业务话 public function scopePending($query) { return $query->whereIn('status', ['open', 'building']); } public function scopeForTeam($query, $teamId) { return $query->where('team_id', $teamId); } // 使用侧:WorkOrder::pending()->forTeam($teamId)->with('team')->paginate(20);

访问器与修改器:字段的读写口径

金额存分、展示要元;工单号展示要带前缀——这类"存取口径"集中到访问器(Accessor)与修改器(Mutator),模板和接口不用各写一遍换算:

<?php // Laravel 11 风格:Attribute 类统一声明读写口径 use Illuminate\Database\Eloquent\Casts\Attribute; protected function budget(): Attribute { return Attribute::make( // 访问器:取出来时换算成元,保留两位 get: fn ($value) => round($value / 100, 2), // 修改器:写进去时统一存分 set: fn ($value) => (int) round($value * 100), ); } // 使用侧全程透明: $order->budget = 199.5; // 库里存 19950 echo $order->budget; // 读出 199.5

口径统一后有一个连带要求:第五章之前的代码里 budget 都按"分"理解,接入口径后全项目自动变"元",任何地方再手工除一百就是二次换算——改造存量代码时全文搜索一下换算点,别新旧口径打架。

图 5-4:一次事务浇筑的成败对照

图 5-4:一次事务浇筑的成败对照

事务与批量操作:整体浇筑

<?php use Illuminate\Support\Facades\DB; // 闭包写法:抛异常自动回滚,正常返回自动提交 $result = DB::transaction(function () use ($team, $data) { // 第一步 扣预算 $team->decrement('budget', $data['budget']); // 第二步 建单 $order = $team->orders()->create($data); // 第三步 记流水 $order->ledger()->create(['amount' => -$data['budget']]); return $order; }); // 手工控制写法:需要按步骤决定提交与否时用 DB::beginTransaction(); try { // ... 多步写库 DB::commit(); } catch (\Throwable $e) { DB::rollBack(); throw $e; }

批量写是事务的好搭档。每行一条 insert 的循环改成一次多行插入,事务里的锁持有时间骤降;updateOrCreate、upsert 这类"存在则改否则插入"的原语,也替你省掉了先查后插的竞态窗口:

<?php // 批量插入:一条 SQL 进五百行 DB::table('work_order_worker')->insert($rows); // 批量 upsert:按 sn 去重更新 WorkOrder::upsert($ordersPayload, ['sn'], ['title', 'budget', 'status']);

⚠️ 常见坑:事务里发邮件、调第三方接口是最常见的误用——网络慢会拖长锁窗口,接口失败会回滚本来已经成功的本地数据。原则:事务只包数据库操作,慢外呼放到提交之后、交给队列(第八章)。

本节要点回顾

  • 预加载三层:with 提前批量、load 补装、$with 默认随行,开发环境开 preventLazyLoading 逼出漏网点。
  • 读写口径:Attribute 的 get 与 set 把换算收进模型,全项目一处声明处处透明。
  • 事务:闭包写法异常自动回滚,钱的流动与多表联动必须整体浇筑,慢外呼移出事务。
  • 批量原语:批量 insert、upsert、updateOrCreate,既提速又消竞态。

数据层的地基、主体、承重、浇筑都齐了。下一章进入装修环节:Blade 模板把数据变成用户看得见的页面。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U