> ## 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）

要從評論參照文章，可在 `Comment` 模型中定義 `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 的紀錄，僅保留你傳入的 ID 作為關聯。
</Tip>

## Eager Loading

### 什麼是 N+1 問題

以屬性方式存取關聯時，Eloquent 會每次都執行查詢。這稱為「延遲載入（Lazy Loading）」。在迴圈中存取關聯會發生 N+1 問題。

```php theme={null}
// 取得所有文章的 1 次查詢
$posts = Post::all();

foreach ($posts as $post) {
    // 每篇文章都會另外查詢一次使用者（N 次）
    echo $post->user->name;
}
```

若文章有 100 篇，就會執行 101 次查詢，對效能影響非常大。

### 用 with() 進行 Eager Loading

透過 `with()` 方法可以一起取得關聯資料，將查詢降到 2 次。

```php theme={null}
// 只需 2 次查詢就能取得文章與使用者
$posts = Post::with('user')->get();

foreach ($posts as $post) {
    // 不會再發送額外的查詢
    echo $post->user->name;
}
```

實際執行的 SQL 只有以下 2 個：

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

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

### 一次 Eager Load 多個關聯

可用陣列指定多個關聯。

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

### 巢狀的 Eager Loading

以點記號可 Eager Load 巢狀關聯。

```php theme={null}
// 一次 Eager Load 文章的評論，以及每則評論的使用者
$posts = Post::with('comments.user')->get();
```

<Warning>
  Eager Loading 對避免 N+1 問題非常重要。當在迴圈中存取關聯時，請養成使用 `with()` 的習慣。
</Warning>

## 下一步

<Card title="Eloquent 入門" icon="database" href="/zh-TW/eloquent">
  回頭複習 Eloquent 的基本 CRUD 操作。
</Card>


## Related topics

- [Eloquent API 資源](/zh-TW/eloquent-resources.md)
- [Eloquent 入門](/zh-TW/eloquent.md)
- [MongoDB](/zh-TW/mongodb.md)
- [Eloquent Factories](/zh-TW/eloquent-factories.md)
- [Eloquent 序列化](/zh-TW/eloquent-serialization.md)
