laravel中真正的模型映射应分层处理:查询层用select()/addselect()投影字段,序列化层用$casts/$appends/resource控制输出,避免滥用map()导致分页失效、n+1和类型失控。

为什么 map() 不是你想要的“映射”
很多人搜“Laravel 模型映射”,其实是想把数据库查出来的 Model 实例,转成更轻量、结构可控的数组或对象(比如去掉敏感字段、重命名键、加计算字段),但直接用集合的 map() 容易踩坑:它只处理已加载的数据,不改查询本身;如果用了分页或延迟加载,map() 后的结果可能丢失分页元信息,甚至触发 N+1。
-
map()返回的是Collection,不是Paginator或Builder,分页失效 - 对未加载关联调用
$model->relation,会现场查库,性能崩盘 - 返回值类型不可控,JSON 响应时容易暴露
Model内部属性(如$casts、$appends)
用 select() + addSelect() 在查询层做字段投影
真正在数据库层面“映射”结构,优先走查询构建器。适合字段少、逻辑简单、不依赖模型访问器的场景。
- 用
select('id', 'name as title')重命名字段,MySQL/PG 支持别名直接生效 - 需要计算字段(如拼接全名)用
addSelect(\DB::raw("CONCAT(first_name, ' ', last_name) as full_name")) - 避免
select('*')后再 PHP 层过滤——既浪费带宽,又绕过数据库优化 - 注意:原始字段名若含空格或特殊字符,需用反引号包裹,如
select('`user id` as user_id')
Post::select('id', 'title as name')->addSelect(\DB::raw("UPPER(status) as status_code"))->get();
用 toArray() 配合 $casts 和 $appends 控制输出结构
这是最常用也最容易失控的方式。模型的序列化行为由三个地方共同决定:$casts(类型转换)、$appends(附加属性)、toArray() 或 jsonSerialize() 的覆盖逻辑。
-
$casts = ['price' => 'float']会让toArray()自动转类型,但不会重命名字段 -
$appends = ['full_name']要配合定义getFullNameAttribute()访问器,否则报错 - 如果只想在 API 返回时生效,别在模型里硬写
$hidden,改用makeHidden(['password'])动态隐藏 - 重写
toArray()是最后手段——它会彻底绕过 Laravel 序列化机制,后续调用toJson()可能出问题
用 Resource 类做真正的响应层映射
当结构变化复杂、涉及关联嵌套、需要不同接口返回不同字段时,Resource 是唯一健壮方案。它和查询解耦,专注“怎么输出”,不干预“怎么查”。
- 每个 Resource 类对应一个输出结构,比如
PostResource只暴露id、title、author_name - 关联数据用
AuthorResource::make($this->whenLoaded('author')),确保没预加载时不查库 - 不要在 Resource 里写业务逻辑(如权限判断),放中间件或 Policy 更清晰
- 批量返回用
PostResource::collection($posts),它自动处理分页——这才是真正兼容LengthAwarePaginator的方式
class PostResource extends JsonResource { public function toArray($request) { return [ 'id' => $this->id, 'title' => $this->title, 'author_name' => $this->author?->name ?? null ]; }}
真正难的不是选哪个方法,而是分清“这里该不该查关联”“这个字段是前端要的,还是只是临时加工用的”——一旦混淆,map() 就会变成性能黑洞,Resource 也会被写成业务逻辑容器。











