正确定义一对多关系需在「一」方写hasMany、「多」方写belongsTo,外键与主键非默认时须显式传参;防N+1须用with()预加载,跨表筛选优先JOIN而非whereHas(),JOIN后需手动聚合嵌套结构。

直接用 $user->posts 查关联数据,100 个用户就会触发 100 次查询,数据库立刻吃不消;正确做法是主动预加载、按需筛选、跨表条件优先走 JOIN —— 关键不在“会不会写”,而在“什么时候该用哪种方式”。
怎么正确定义一对多关系
关系必须写在「一」方模型里,外键在哪边,「多」方就在哪边:
- User 模型写
hasMany(Post::class),表示一个用户有多篇文章 - Post 模型写
belongsTo(User::class),表示一篇文章属于一个用户 - 如果外键不是
user_id(比如叫author_id),两边都要显式传参:hasMany(Post::class, 'author_id')和belongsTo(User::class, 'author_id') - 主键不是
id(比如是uid或uuid),hasMany第三个参数也得补上
避免 N+1 查询:预加载是基础操作
循环中访问 $user->posts 是懒加载,每次调用都发一次 SQL。批量查必须用 with():
- 查全部用户及其文章:
User::with('posts')->get() - 只取已发布的文章:
User::with(['posts' => fn($q) => $q->where('published', 1)]) - 限制字段减少传输量:
User::with('posts:id,title,user_id') - 只统计数量不取内容:
User::withCount('posts')
跨表条件筛选:JOIN 比 whereHas() 更稳
当条件同时涉及主表和关联表(例如“活跃用户发的政治类文章”),whereHas() 生成 EXISTS 子查询,大数据量下难走索引;join() 是单条 SQL,可命中复合索引:
- 用
join()写法:Post::join('users', 'posts.user_id', '=', 'users.id')<br> ->where('users.account_type', 'active')<br> ->where('posts.type', 'politics')<br> ->select('posts.*')<br> ->get() - 注意必须显式写
select('posts.*'),否则users.id会覆盖posts.id,导致模型构造失败 - 要带用户字段(如作者名):
select('posts.*', 'users.name as author_name'),别用users.*
返回嵌套结构:JOIN 后需手动聚合
用 leftJoin() 关联多张表(如 customers + expenses + allowances)会得到扁平化结果,一个客户对应多行记录。Eloquent 不会自动转成嵌套 JSON:
- 先查出扁平数据:
Customer::select(...)->leftJoin(...)->leftJoin(...)->get() - 再用 PHP 聚合重组:
遍历结果,按customer.id分组,把每条 expense 和 allowance 推进对应客户的expenses或allowances数组里 - 若要求严格结构化输出(如 API 返回
"expenses": [{}, {}]),这步不可跳过


















