---
tags: laravel
---
# Eloquent Relationship
說明 Laravel 自帶的 Eloquent,如何進行一對一.一對多.多對多等 CRUD操作
[Laravel官方說明](https://laravel.com/docs/7.x/eloquent-relationships)
### 示範表格結構

## 命名慣例
* 外鍵欄位為對應表格模型名加上 "_id"
* 關係函式的名稱不要與模型的屬性相衝突,會導致無法解析
==以下寫法均採用語法糖,限定外鍵欄位與主鍵欄位須採用預設格式,例如 user_id與id。==
## 一對一
每個Model實例只屬於/擁有另一個Model實例,例如每個商品(Item)只屬於一個分類(Cgy)
表格設計上,會增加所對應Model的流水號,作為外鍵。舉例來說,要說明某商品屬於哪個類別,會在商品表格上加入cgy_id欄位
撰寫流程:
Step 1.修改Migration與表格
Step 2.撰寫關係函式
Step 3.測試關係函式
==主鍵和外鍵都應採用bigInteger,否則執行migrate時會出現錯誤==
#### 修改Migration範例
```
//在up()裏頭
Schema::create('items', function (Blueprint $table) {
...
//舊寫法
$table->unsignedBigInteger('cgy_id')->index();
$table->foreign('cgy_id')->references('id')->on('cgies')->onDelete('cascade');
//新寫法
$table->foreignId('cgy_id')->constrained();
});
//在down()裡面
Schema::table('items', function(Blueprint $table){
$table->dropForeign(['cgy_id']);
});
```
#### 撰寫Model關係函式範例
```
\\App\Models\Item.php
//此商品"屬於"哪個類別
public function cgy()
{
//如未按照Laravel的命名慣例,則需提供外鍵與主鍵欄位名稱
//外鍵命名慣例: 對應模型 + _id,例如 cgy_id
//主鍵命名慣例: id
return $this->belongsTo(\App\Models\Cgy::class);
}
```
> belongsTo()函式的寫法可改成belongsTo('\App\Models\Cgy')
> 如果外鍵欄位名稱不是cgy_id,就需要在belongsTo()的第二參數加以說明
> 如果主鍵欄位名稱不是id,就需要在belongsTo()的第三參數加以說明
> 以下依此類推
#### 使用關係函式
```
//修改關聯主鍵
$item->cgy()->asssociate($another_cgy);
$item->save();
//移除關聯
$item->cgy()->dissociate();
$item->save();
//取用關聯表格的資料
$item->cgy->id;
```
## 一對多
每個Model實例屬於/擁有多個實例,例如每個使用者(User)擁有多筆訂單(Order)
表單設計上,會在另一個對應表單增加自己的流水號,作為外鍵。舉例來說,要說明使用者擁有哪些訂單,會在訂單表格上加入user_id欄位
撰寫流程:
Step 1.修改Migration與表格
Step 2.撰寫關係函式
Step 3.測試關係函式
#### 修改Migration範例
同一對一,如果已經做過就無須再做
#### 撰寫Model關係函式範例
```
\\App\Models\Cgy.php
//此分類擁有那些商品
public function items()
{
//如未按照Laravel的命名慣例,則需提供外鍵與主鍵欄位名稱
//外鍵命名慣例: 對應模型 + _id,例如 cgy_id
//主鍵命名慣例: id
return $this->hasMany(\App\Models\Item::class);
}
```
> hasMany()函式的寫法可改成hasMany('\App\Models\Item')
> 如果外鍵欄位名稱不是cgy_id,就需要在hasMany()的第二參數加以說明
#### 使用關係函式
如果加入已存在的關係,不會產生新的變化,也不會報錯
```
//針對關聯表格進行查詢
$items = $cgy->items()->where('enabled',true)->get();
//修改關聯主鍵
$cgy->items()->save($item);
$cgy->items()->saveMany([$task,$task2]);
//取用關聯表格的資料
$cgy->items;
```
## 多對多
類似一對多,每個Model實例屬於/擁有多個Model實例,但反過來也是相同的一對多關係。例如每個商品(Item)屬於多筆訂單(Order)。反過來,每個訂單也擁有多個商品。
表單設計上,會建立一個新的表單作為pivot,來記錄多對多關係。舉例來說,商品與訂單的多對多關係就會建立名為item_order的表格,依照字母順序來排列
撰寫流程:
Step 1.修改Migration與表格,建立pivot表格
Step 2.撰寫關係函式
Step 3.確定有假資料可用(Option),否則寫Seeder&Factory
Step 3.測試關係函式
### pivot表格範例
```
public function up()
{
Schema::create('item_order', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('item_id')->index()->unsigned();
$table->foreign('item_id')->references('id')->on('items')->onDelete('cascade');
$table->bigInteger('order_id')->index()->unsigned();
$table->foreign('order_id')->references('id')->on('orders')->onDelete('cascade');
$table->integer('qty')->default(1);
$table->timestamps();
});
}
public function down()
{
Schema::table('item_order', function (Blueprint $table) {
$table->dropForeign(['item_id','order_id']);
});
Schema::dropIfExists('item_order');
}
```
### 關係函式範例
withTimestamps() 新增關係時一併維護pivot表格的created_at與updated_at欄位
withPivot('col_name1','col_name2') 同時帶出pivot表格的其他欄位
```
//app/Models/Item.php
//此商品屬於那些訂單
public function orders(){
return $this->belongsToMany(\App\Models\Order::class)->withTimestamps();
}
```
>如果Laravel沒有正確地使用我們的pivot表格名稱,可在belongsToMany()的第二參數說明正確的pivot表格名稱
```
//app/Models/Order.php
//此訂單擁有那些商品
public function items(){
return $this->belongsToMany(\App\Models\Item::class)->withTimestamps()->withPivot('qty');
}
```
### 使用關係函式範例
語法:
新增關係 **save**(對應Model參考,[pivot表格額外欄位資料])
新增關係 **attach**(對應Model流水號陣列,[pivot表格額外欄位資料])
移除關係 **detach**([對應Model流水號]) 如不提供流水號,表示移除所有關係
同步關係 **sync**(對應Model流水號陣列)
```
//取出該訂單的所有商品
$order->items;
//取出訂單某商品的數量,需搭配withPivot('qty')使用
$order->items()->first()->pivot->qty;
//用商品參考將該商品加入訂單
$order_1->items()->save($item_1);
//用商品參考將該商品加入訂單,pivot表的qty為2
$order_1->items()->save($item_1,['qty'=>2]);
//用商品主鍵將該商品加入訂單
$order_2->items()->attach([1,2]);
//用商品主鍵將該商品移出訂單
$order_1->items()->detach(1);
//重置該訂單的商品內容為1,2,3.4
$order_1->items()->sync([1,2,3,4]);
```
>pivot屬性名稱可以根據你的喜好做修改,作法為在belongsToMany()後面加一個as('新別名'),之後就可以用新別名取代pivot
```
//app/Models/Order.php
//此訂單擁有那些商品,將pivot改名為detail
public function items(){
return $this->belongsToMany(\App\Models\Item::class)->as('detail');
}
$order->items()->detail->qty
```
### 常見問題
Q1.關係函式在使用時到底需不需要加小括號呢?
A:要看你的目的,如果你的查詢條件都已經完成,想直接開始查詢就不用加。反之如果需要進一步的查詢,比如下where(),就需要加
### 關聯查詢
//找出分類中至少有1個商品的
$cgies = Cgy::has('items')->get();
//找出分類中至少有5個以上商品的
$cgies = Cgy::has('items','>=',5)->get();
$cgies = Cgy::whereHas('items',function($query){
//找出分類中至少擁有商品價格為8000以上的
$query->where('price','>',8000);
})->get();
### 進階技巧
#### 連帶更新父紀錄的更新時間
即當子紀錄(如Item)的欄位被修改之後,同步修改父紀錄(如Cgy)的updated_at欄位
```
\\App\Models\Item.php
protected $touches = ['cgy'];
```
#### 積極性載入(Eager Loading)
當查詢資料時,連帶把關係資料一起帶出來,以簡化查詢作業。預設為消極性載入(Lazy Loading),也就是當使用該屬性時才進行查詢
```
//取出所有的分類及其擁有的歌曲資料
$cgies = Cgy::with('songs')->get();
//取出所有的分類並算出其擁有的歌曲數量
$songs_count = Cgy::withCount('songs')->get();
```
##### Nested Eager Loading
一併往下查詢到再下一層的Model,比如同時查詢該書的作者的聯絡單
$book = App\Models\Book::with('author.contacts')->get();
##### Eager loading限定欄位
一併先查詢關聯的Author資料,但只先取id和name欄位
$users = App\Models\Book::with('author:id,name')->get();
##### Constraints Eager Loading
一併查詢posts表格的時候,同時對posts表格查詢加入title欄位內有first字樣的條件
$users = App\Models\User::with(['posts' => function($query){
$query->where('title','like','%first%');
}])->get();