5.2 模型定义与查询构造器


5.2 模型定义与查询构造器

本节摘要:Eloquent 用约定把一张表变成一个类:表名、主键、时间戳都有默认规则,约定不合适再显式声明;fillable 白名单守住批量赋值的门。查询构造器则以链式语法覆盖排序、筛选、聚合、子查询等复杂取数,与 Eloquent 共享同一底层。本节把模型编制规则与常用查询写法一次讲透,并以一次真实的"待办工单列表"调优为例展开完整过程。

接上节:地基之上,编制施工队

上一节建好了三张表,本节把它们映射成类。模型层的价值不在省 SQL,而在把"数据怎么读写"的规矩收进一个地方:字段怎么转换、哪些字段允许批量写入、删了算删还是算归档——这些规矩写一次,全项目共用。

模型编制:约定与声明

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; class WorkOrder extends Model { // 约定已覆盖的:表名 work_orders、主键 id、自动时间戳 // 需要声明的:批量赋值白名单 protected $fillable = ['team_id', 'title', 'sn', 'budget', 'status', 'due_at']; // 字段类型转换:库里存字符串,代码里当日期与枚举用 protected $casts = [ 'due_at' => 'datetime', 'status' => OrderStatus::class, // PHP 枚举 'budget' => 'integer', ]; public function team(): BelongsTo { return $this->belongsTo(Team::class); } }
<?php // app/Enums/OrderStatus.php:状态枚举,配合 casts 使用 namespace App\Enums; enum OrderStatus: string { case Draft = 'draft'; case Open = 'open'; case Building = 'building'; case Done = 'done'; public function label(): string { return match($this) { self::Draft => '草稿', self::Open => '待施工', self::Building => '施工中', self::Done => '已完工', }; } }

编制规则里最要紧的是 fillable:它决定 WorkOrder::create($data) 能写哪些字段。没有白名单,任何能触达接口的人都能顺手改掉你没打算暴露的字段——第四章讲批量赋值漏洞时提过的第二道防线就是它。

查询构造器:链着写的 SQL

Eloquent 的静态调用背后就是查询构造器,两者共享同一套链式语法。日常取数的写法几乎可以照着 SQL 翻译:

<?php use App\Models\WorkOrder; use Illuminate\Support\Facades\DB; // 基本筛选与排序:预算十万以上、施工中的单子,按期限排序 $orders = WorkOrder::query() ->where('budget', '>=', 100000) ->where('status', 'building') ->orderBy('due_at') ->limit(20) ->get(); // 或者条件组:状态为二选一,且期限在本月 $urgent = WorkOrder::query() ->where(function ($q) { $q->where('status', 'open')->orWhere('status', 'building'); }) ->whereBetween('due_at', [now()->startOfMonth(), now()->endOfMonth()]) ->get(); // 聚合与分组:各队组的在建单量 $load = WorkOrder::query() ->selectRaw('team_id, count(*) as total, sum(budget) as amount') ->whereIn('status', ['open', 'building']) ->groupBy('team_id') ->get(); // 与表无关的纯构造器场景:跨表统计直接用 DB 门面 $rows = DB::table('work_order_worker') ->join('workers', 'workers.id', '=', 'work_order_worker.worker_id') ->select('workers.name', DB::raw('count(*) as joined_orders')) ->groupBy('workers.name') ->orderByDesc('joined_orders') ->get();

选择标准一句话:以"模型对象"为操作单元(增删改、按主键取、走关系)用 Eloquent;纯报表式取数、不映射业务对象的复杂统计,查询构造器甚至直接 DB 表名更省事。两者可随时衔接——WorkOrder::query() 出来的就是构造器,需要时无缝加约束。

图 5-2:一条列表查询从写法到 SQL 的翻译路径

图 5-2:一条列表查询从写法到 SQL 的翻译路径

完整案例:待办列表从能用到能用得快

需求:团队后台的"待办工单"列表,展示本队未完工的单子,带队组名与人数,分页。第一版这么写,功能没问题:

<?php public function index(Request $request) { $orders = WorkOrder::query() ->where('team_id', $request->user()->team_id) ->whereIn('status', ['open', 'building']) ->orderByDesc('created_at') ->paginate(20); foreach ($orders as $order) { // 每条单子循环里查一次队组 一次人数:20 条就是 40 次额外查询 $order->team_name = $order->team->name; $order->crew_count = $order->workers()->count(); } return $orders; }

开启 DB 旁听一看,一页请求跑了 41 条 SQL(列表一条、关联四十条),这就是 N+1 的现场,5.4 节会专门讲预加载怎么根治它。这里先给速效版:

<?php public function index(Request $request) { return WorkOrder::query() ->where('team_id', $request->user()->team_id) ->whereIn('status', ['open', 'building']) ->withCount('workers') // 人数并成一条子查询 ->orderByDesc('created_at') ->paginate(20); } // SQL 条数从 41 降到 3:主查询、人数子查询合一条、分页计数一条

解读这次调优:先保证对(功能正确),再让快有依据(旁听 SQL 条数),最后用框架特性一次性修好——这个"旁听再动手"的顺序,比背任何优化口诀都可靠。复杂的动态条件可以进一步收进模型作用域(local scope),让控制器只说业务话:WorkOrder::pending()->forTeam($team)->paginate(),5.4 节会给出作用域的完整写法。

⚠️ 常见坑:whereIn 传入空数组时构造器会生成非法 IN 条件或全表行为,动态条件拼装前先判空;orderBy 直接引用用户输入的排序字段,等于把排序规则交给外人,白名单校验后再用。

本节要点回顾

  • 编制靠约定:表名、主键、时间戳自动对号,fillable 白名单与 casts 类型转换是两个必写项。
  • 一套语法两层皮:Eloquent 与查询构造器同底,业务对象用模型,报表统计可下沉到 DB 表名。
  • 条件组装:闭包管理条件组,参数绑定防注入,动态排序字段必须白名单。
  • 调优顺序:先旁听 SQL 再动手,withCount 这类聚合预载是列表接口的第一速效药。

一张表编好了,多张表怎么协同?下一节把关系模型讲透:一对一、一对多、多对多、多态关联,以及中间表的正确用法。


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