关联关系:一对多、多对多、多态
High Contrast
Dark Mode
Light Mode
Sepia
Forest
1 min read208 words

关联关系:一对多、多对多、多态

Eloquent 关联是最容易产生 N+1 问题的地方,也是最能体现代码设计能力的地方。本节覆盖 TaskFlow 项目中的所有关联场景,每种关联都配有数据库设计和查询示例。


一对多(hasMany / belongsTo)

users (1) ──→ (N) tasks
projects (1) ──→ (N) tasks
// app/Models/User.php
class User extends Authenticatable
{
// 一个用户有多个任务
public function tasks(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Task::class);
}
// 带条件的关联
public function pendingTasks(): HasMany
{
return $this->hasMany(Task::class)->where('status', 'pending');
}
}
// app/Models/Task.php
class Task extends Model
{
// 任务属于一个用户
public function user(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(User::class);
}
// 任务属于一个项目(外键不是默认命名时)
public function project(): BelongsTo
{
return $this->belongsTo(Project::class, 'project_id');
}
}
// 查询
$user = User::find(1);
$tasks = $user->tasks;                    // 所有任务(懒加载)
$count = $user->tasks()->count();         // 使用 () 返回查询构建器
$pending = $user->tasks()->pending()->get(); // 链接作用域
// 创建关联记录
$task = $user->tasks()->create(['title' => '新任务', 'status' => 'pending']);
// 自动设置 user_id = $user->id
// 或者:
$user->tasks()->attach($task);   // 不适用于 hasMany

多对多(belongsToMany)

tasks ──→ task_tag (pivot) ←── tags
// Migration:pivot 表
Schema::create('task_tag', function (Blueprint $table) {
$table->foreignId('task_id')->constrained()->cascadeOnDelete();
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
$table->primary(['task_id', 'tag_id']);
// pivot 额外字段(如果需要)
$table->timestamp('tagged_at')->nullable();
});
// app/Models/Task.php
public function tags(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
{
return $this->belongsToMany(Tag::class)
->withTimestamps()        // 自动维护 created_at/updated_at
->withPivot('tagged_at'); // 如果 pivot 有额外字段
}
// app/Models/Tag.php
public function tasks(): BelongsToMany
{
return $this->belongsToMany(Task::class)->withTimestamps();
}
// 操作多对多关联
$task = Task::find(1);
// 附加标签(不重复,幂等)
$task->tags()->syncWithoutDetaching([1, 2, 3]);
// 同步(先删除不在列表中的,再添加新的)
$task->tags()->sync([1, 2, 3]);
// 附加单个(允许重复)
$task->tags()->attach($tagId);
// 附加带 pivot 数据
$task->tags()->attach($tagId, ['tagged_at' => now()]);
// 分离标签
$task->tags()->detach($tagId);
$task->tags()->detach();      // 分离所有
// 查询 pivot 数据
foreach ($task->tags as $tag) {
echo $tag->pivot->tagged_at;  // pivot 字段通过 ->pivot 访问
}

多态关联(morphTo / morphMany)

多态关联让一个模型关联到多种不同的模型——TaskFlow 中的评论可以属于任务、也可以属于项目:

comments (polymorphic)
commentable_type = "App\Models\Task"    commentable_id = 42
commentable_type = "App\Models\Project" commentable_id = 7
// Migration
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->text('content');
$table->foreignId('user_id')->constrained();
$table->morphs('commentable'); // 生成 commentable_id + commentable_type
$table->timestamps();
});
// app/Models/Comment.php
class Comment extends Model
{
public function commentable(): \Illuminate\Database\Eloquent\Relations\MorphTo
{
return $this->morphTo();
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
// app/Models/Task.php
public function comments(): \Illuminate\Database\Eloquent\Relations\MorphMany
{
return $this->morphMany(Comment::class, 'commentable')
->latest();
}
// app/Models/Project.php
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable')->latest();
}
// 创建多态关联记录
$task = Task::find(1);
$task->comments()->create([
'content' => '这个任务需要更多细节',
'user_id' => auth()->id(),
]);
// 查询
$comments = $task->comments; // 只获取任务的评论
// 通过 Comment 反向查询
$comment = Comment::find(1);
$owner = $comment->commentable;  // 返回 Task 或 Project 实例(多态!)

通过中间表的远程关联(hasManyThrough)

users → projects → tasks
User 通过 projects 表关联到 tasks
// app/Models/User.php
public function projectTasks(): \Illuminate\Database\Eloquent\Relations\HasManyThrough
{
return $this->hasManyThrough(
Task::class,    // 最终关联的模型
Project::class, // 中间模型
'user_id',      // 中间表的外键(关联到 users)
'project_id',   // 最终表的外键(关联到 projects)
);
}
// 使用
$user->projectTasks()->pending()->get();

关联计数(withCount)

// 同时加载关联数量,避免 N+1
$tasks = Task::withCount(['comments', 'tags'])
->get();
foreach ($tasks as $task) {
echo $task->comments_count;  // 不触发额外查询
echo $task->tags_count;
}
// 带条件的计数
$tasks = Task::withCount([
'comments',
'comments as resolved_comments_count' => fn($q) => $q->where('resolved', true),
])->get();

下一节预加载与 N+1 问题——关联定义好了,但不预加载就会产生 N+1 查询。with()load()loadMissing() 的区别,以及如何用 Laravel Debugbar 发现 N+1。