> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Eloquent 关联入门

> 介绍如何使用 Eloquent 定义表之间的关联（一对一、一对多、多对多）以及高效获取关联数据。

## 什么是关联

数据库中的表之间经常存在关联。例如一篇博客文章有多条评论，一笔订单归属于下单的用户。

Eloquent 提供了简洁定义和操作这些关联的方式。关联作为 Eloquent 模型的方法进行定义。

<Info>
  本页以 `User`、`Post`、`Comment`、`Tag` 等模型为例。假设这些模型与表已经创建好了。
</Info>

Eloquent 支持的主要关联类型：

| 关联              | 说明           |
| --------------- | ------------ |
| `hasOne`        | 一对一（父拥有一个子）  |
| `belongsTo`     | 一对一的反向（子属于父） |
| `hasMany`       | 一对多（父拥有多个子）  |
| `belongsToMany` | 多对多（通过中间表）   |

```mermaid theme={null}
erDiagram
    users ||--o| profiles : "hasOne / belongsTo (profiles.user_id)"
    posts ||--o{ comments : "hasMany / belongsTo (comments.post_id)"
    posts }o--o{ tags : "belongsToMany (post_tag 中间表)"
```

## hasOne（一对一）

`hasOne` 表示某模型拥有另一个模型的一条记录。例如 `User` 拥有一个 `Profile`。

### 定义关联

在 `User` 模型定义 `profile` 方法：

```php theme={null}
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;

class User extends Model
{
    public function profile(): HasOne
    {
        return $this->hasOne(Profile::class);
    }
}
```

Eloquent 根据父模型名推断外键，此处会假定 `profiles` 表有 `user_id` 列。

### 获取关联数据

关联可以作为属性访问：

```php theme={null}
$user = User::find(1);
$profile = $user->profile;
```

<Tip>
  以属性方式调用关联方法时，Eloquent 会自动发起查询并返回数据。这称为「动态关联属性」。
</Tip>

## belongsTo（一对一的反向）

`belongsTo` 是 `hasOne` 的反向，定义在拥有外键的一侧。例如 `Profile` 属于 `User`。

### 定义关联

```php theme={null}
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Profile extends Model
{
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}
```

Eloquent 会用「方法名 + \_id」（此处为 `user_id`）作为外键。

### 获取关联数据

```php theme={null}
$profile = Profile::find(1);
$user = $profile->user;

echo $user->name;
```

## hasMany（一对多）

`hasMany` 是最常见的一对多关联。例如一篇 `Post` 拥有多条 `Comment`。

### 定义关联

```php theme={null}
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Post extends Model
{
    public function comments(): HasMany
    {
        return $this->hasMany(Comment::class);
    }
}
```

Eloquent 会假定 `comments` 表有 `post_id`。

### 获取关联数据

`hasMany` 返回集合：

```php theme={null}
$post = Post::find(1);

foreach ($post->comments as $comment) {
    echo $comment->body;
}
```

也可以追加查询条件：

```php theme={null}
$recentComments = Post::find(1)
    ->comments()
    ->latest()
    ->take(5)
    ->get();
```

### 反向关联（belongsTo）

从评论访问文章：

```php theme={null}
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Comment extends Model
{
    public function post(): BelongsTo
    {
        return $this->belongsTo(Post::class);
    }
}
```

```php theme={null}
$comment = Comment::find(1);
echo $comment->post->title;
```

## belongsToMany（多对多）

多对多用于两个模型互相拥有多个关联。例如 `Post` 有多个 `Tag`，`Tag` 也属于多个 `Post`。

### 表结构

多对多需要中间表。例如 `Post` 与 `Tag` 使用 `post_tag` 中间表：

```text theme={null}
posts
    id - integer
    title - string

tags
    id - integer
    name - string

post_tag
    post_id - integer
    tag_id - integer
```

<Info>
  Eloquent 会按字母顺序拼接两个模型名推断中间表名（`post` + `tag` → `post_tag`）。
</Info>

### 定义关联

```php theme={null}
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Post extends Model
{
    public function tags(): BelongsToMany
    {
        return $this->belongsToMany(Tag::class);
    }
}
```

在 `Tag` 上同样定义即可反向访问：

```php theme={null}
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Tag extends Model
{
    public function posts(): BelongsToMany
    {
        return $this->belongsToMany(Post::class);
    }
}
```

### 获取关联数据

```php theme={null}
$post = Post::find(1);

foreach ($post->tags as $tag) {
    echo $tag->name;
}
```

### 增删关联数据

用 `attach` 添加、`detach` 删除中间表记录：

```php theme={null}
$post = Post::find(1);

// 添加标签
$post->tags()->attach($tagId);

// 删除标签
$post->tags()->detach($tagId);

// 完全替换当前关联
$post->tags()->sync([$tagId1, $tagId2]);
```

<Tip>
  `sync` 会自动删除未指定的中间表记录，只保留传入的 ID。
</Tip>

## 预加载（Eager Loading）

### 什么是 N+1 问题

以属性方式访问关联，Eloquent 每次都会发查询。这称为「延迟加载」。在循环中访问会产生 N+1 问题。

```php theme={null}
// 取全部文章 1 次查询
$posts = Post::all();

foreach ($posts as $post) {
    // 每次循环再查一次 user（N 次）
    echo $post->user->name;
}
```

100 篇文章会产生共 101 次查询，性能问题很大。

### with() 预加载

`with()` 一次性把关联取回来：

```php theme={null}
// 只查 2 次
$posts = Post::with('user')->get();

foreach ($posts as $post) {
    // 不再有额外查询
    echo $post->user->name;
}
```

执行的 SQL 只有两条：

```sql theme={null}
select * from posts

select * from users where id in (1, 2, 3, ...)
```

### 同时预加载多个关联

```php theme={null}
$posts = Post::with(['user', 'comments', 'tags'])->get();
```

### 嵌套预加载

用点号访问嵌套关联：

```php theme={null}
// 预加载 comments 及其 user
$posts = Post::with('comments.user')->get();
```

<Warning>
  预加载对避免 N+1 至关重要。循环中访问关联时请养成使用 `with()` 的习惯。
</Warning>

## 下一步

<Card title="Eloquent 入门" icon="database" href="/zh/eloquent">
  回顾 Eloquent 的基本 CRUD。
</Card>


## Related topics

- [Eloquent API 资源](/zh/eloquent-resources.md)
- [Eloquent 入门](/zh/eloquent.md)
- [Laravel Nightwatch 入门](/zh/blog/nightwatch-introduction.md)
- [MongoDB](/zh/mongodb.md)
- [Eloquent 工厂](/zh/eloquent-factories.md)
