前言
- 如果你是剛開始學習Laravel,可能會感覺比較興奮,同時在實操的過程中,又會很苦悶。特別是laravel提供的類有哪些可以用的方法,它們具體怎麼用,每個引數該如何給對應的值,都是頭疼的事情。
- 也許你會說我可以查詢官方API可以瞭解,但是又會發現文件寫得實在是太簡潔了,又必須進入到API對應的原始碼中去拜讀才能初步領會(時間開銷較大)。
- 其實,很多時候我們可以通過它給定的一些內部機制和方法,直接通過例項操作就可以摸索出運作原理。比如今天我們準備展開講解的Eloquent 模型的多對多(非主鍵)關聯中我們會在tinker中對model進行操作,只要不是執行的最終獲取資料操作,我們都可以採用toSql方法檢視我們的關聯查詢是否產出正確的sql語句。
- 正式開始之前,我們先來關注這麼幾個問題:
- 如果表的主鍵不是預設自增id怎麼辦?
- 中間表中有一個欄位同時又對應其他N個表怎麼辦?
- 新增記錄時如果pivot中間表有其他欄位需要填入資料怎麼辦?
- 假設我們有Teacher表與cities的關係為N:M,Teacher在修改老師記錄時,我們對老師去過的城市進行CRUD操作時,要判斷該老師去過的城市是否已經有學生了,有的話,就不允許刪除這個對應的城市。
建立基本資料模型
基本說明:
- 一位老師會去多個城市;
- 一個城市裡會有多位老師;
teachers 表
欄位名 | 型別 | 備註 |
---|---|---|
id | unsigned integer | 教師編號 |
name | varchar(255) | 教師姓名 |
subject | varchar(255) | 教授科目 |
對應遷移表:
Schema::create('teachers', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('subject');
$table->timestamps();
});
城市 表
欄位名 | 型別 | 備註 |
---|---|---|
id | unsigned integer | city編號 |
name | varchar(255) | 城市名 |
對應遷移表:
Schema::create('cities', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->timestamps();
});
模型關聯:
teacher_city_pivot 表
欄位名 | 型別 | 備註 |
---|---|---|
id | unsigned integer | id |
teacher_id | unsigned integer | 對應教師表id |
city_id | unsigned integer | 對應城市表id |
對應遷移表:
Schema::create('teacher_city_pivot', function(Blueprint $table){
$table->increments('id');
$table->unsignedInteger('teacher_id');
$table->unsignedInteger('city_id');
$table->unique(['teacher_id', 'city_id']); // 同一位老師對應同一城市只能出現一次;
});
預設:中間表中的teacher_id 對應著teachers表中的id,city_id對應cities表的id。
多對多 pivot 對應關係
模型關聯:
多對多關聯時,採用中間表pivot(見下方的teacher_city_pivot表連線;
在教師表(teachers)對應的model中我們需要通過pivot表關聯城市表(cities):
class Teacher extends Model
{
protected $fillable = ['name', 'subject'];
public function cities()
{
// 第一個引數是教師對應的城市模型,第二個引數是中間表名;
return $this->belongsToMany('App\Models\City','teacher_city_pivot');
}
}
在城市表(cities)對應的model中我們同樣需要通過pivot表關聯老師表(teachers):
class City extends Model
{
protected $fillable = ['name'];
public function teacher()
{
// 第一個引數是城市對應的教師模型,第二個引數是中間表名;
return $this->belongsToMany('App\Models\Teacher','teacher_city_pivot');
}
}
進入tinker中,先分別新建一個教師資訊和一個城市資訊;
➜ Laravel git:(master) ✗ php artisan tinker;
Psy Shell v0.8.11 (PHP 7.1.7 — cli) by Justin Hileman
>>> $t = App\Models\Teacher::find(1);
=> null
>>> $t = App\Models\Teacher::create(['name'=>'Miss Li', 'subject'=>'Enlish']);
=> App\Models\Teacher {#743
name: "Miss Li",
subject: "Enlish",
updated_at: "2017-09-21 16:26:20",
created_at: "2017-09-21 16:26:20",
id: 1,
}
>>> $s = App\Models\City::create(['name'=>'Loo']);
=> App\Models\City {#737
name: "HangZhou",
updated_at: "2017-09-21 16:26:49",
created_at: "2017-09-21 16:26:49",
id: 1,
}
然後我們再來測試他們之間的關聯是否正常建立:
>>> $t->cities()->attach(1);
=> null
>>> $t = App\Models\Teacher::find(1);
=> App\Models\Teacher {#749
id: 1,
name: "Miss Li",
subject: "Enlish",
created_at: "2017-09-21 16:26:20",
updated_at: "2017-09-21 16:26:20",
}
>>> $t->cities
=> Illuminate\Database\Eloquent\Collection {#739
all: [
App\Models\City {#745
id: 1,
name: "Loo",
created_at: "2017-09-21 16:26:49",
updated_at: "2017-09-21 16:26:49",
pivot: Illuminate\Database\Eloquent\Relations\Pivot {#750
teacher_id: 1,
city_id: 1,
},
},
],
}
通過上面的操作,我們可以肯定他們之間的關聯是非常ok的,同時可以很清楚的知道每次鏈式操作最終返回物件的類的出處,從而更加快捷的去參閱API文件。在建立關聯的時候我們用到了attach()方法,並且傳入了一個整型引數,它對應著這位老師準備要教的一名新學生的id。
如這位老師飛多個城市,我們可以傳入一個陣列作為引數attach([1,2,3]);
多對多 pivot 非預設主鍵對應關係
現在我們來回答第一個問題:如果表的主鍵不是預設自增id怎麼辦?
通過這個問題,我們首先想想,如果學校辭退了20名老師(編號40~60),然後管理員把他們都刪掉了。校長老大還想使用40到60的編號,在預設自增id就會對應不上了。此時我們就需要新增一個欄位來標識教師編號了,同時這個編號也是我們的主鍵了,所以我們需要進行以下幾個步驟來解決一些問題。
- 在Teacher模型對應的遷移檔案中加上:
$table->unsignedInteger('teachers_id');
- 在Teacher模型中加上:
protected $primaryKey = 'teachers_id';
- 執行
php artisan migrate:refresh
遷移命令。 - 進入tinker中,看看新的對應關係是否正常建立。
同理,我們可以通過上述方法解決:學生的編號不是自增id
pivot 中一列對應多表(多對多和一對多示例)
現在我們再來看看更加複雜的應用。
- 城市與教師:N : M
- 城市與學生:N : M
- 城市與家長:N : M
- 教師與學生:1 : N
- 學生與家長:1 : N
新的pivot表我們變成下面這樣,通過觀察下面的表結構和對應的記錄,不難發現上面的對應關係中的前三種已經反映出來,同時表的設計還考慮到資料的冗餘。這種冗餘可以體現為2種表設計,
- 其一,將城市,教師,學生,家長表的主鍵id都放到同一張表(id,city_id,teacher_id,student_id,parent_id),在實際的資料儲存中會發現很多記錄中的很多欄位是沒有資料的;比如我們想將城市與教師關聯,就有一條唯一記錄,學生和載入id在這條記錄中是不會有資料的。(不推薦)
- 其二,城市和教師、學生和家長表之間各建立一箇中間表。這也是我們平時用的比較多的一種設計方式。(本文開篇時所提到的常規方法,具體採用哪一種就見仁見智了^_^)
因為對應關係發生了變化,前面的 pivot 表我們也更新名稱為 city_type_bind_pivot
欄位名 | 型別 | 備註 |
---|---|---|
id | unsigned integer | id |
city_source_id | unsigned integer | 城市編號 |
bind_id | unsigned integer | 對應bind_type中的非預設主鍵id |
bind_type | enum | 對應多張表,可取值為:teacher, student, parent |
bind_id 和 bind_type 共同確定唯一一條記錄:
id | city_source_id | bind_id | bind_type |
---|---|---|---|
1 | 1 | 1 | teacher |
2 | 1 | 2 | teacher |
3 | 1 | 1 | student |
3 | 1 | 2 | student |
4 | 1 | 1 | parent |
因為歷史原因,需要修改教師表和學生表,我們的教師表中的預設id不能作為主鍵,需要新加一個欄位teacher_id充當此作用。
欄位名 | 型別 | 備註 |
---|---|---|
id | unsigned integer | id |
teacher_id | unsigned integer | 教師編號 |
name | varchar(255) | 教師姓名 |
subject | varchar(255) | 教授科目 |
同樣,學生表也是如此:
欄位名 | 型別 | 備註 |
---|---|---|
id | unsigned integer | id |
student_id | unsigned integer | 學生編號 |
teacher_id | unsigned integer | 教師編號 |
name | varchar(255) | 學生姓名 |
另外,我們又想將學生的家長們也納入系統中來,學生有什麼問題的時候可以隨時聯絡到他們。
欄位名 | 型別 | 備註 |
---|---|---|
id | unsigned integer | id |
student_id | unsigned integer | 學生編號 |
parent_id | unsigned integer | 家長編號 |
name | varchar(255) | 家長姓名 |
城市表:
欄位名 | 型別 | 備註 |
---|---|---|
id | unsigned integer | id |
city_id | unsigned integer | 城市id |
name | varchar(255) | 城市名 |
接下來,我們再在model中更新或新增關聯關係:
City model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class City extends Model
{
protected $fillable = ['name', 'city_id', ];
protected $primaryKey = 'city_id';
public function teachers(){
return $this->belongToMany('App\Models\Teacher', 'city_type_bind_pivot', 'city_source_id', 'bind_id')
->withPivot('city_source_id', 'bind_id', 'bind_type')
->wherePivot('bind_type', '=', 'teacher');
}
public function students(){
return $this->belongToMany('App\Models\Student', 'city_type_bind_pivot', 'city_source_id', 'bind_id')
->withPivot('city_source_id', 'bind_id', 'bind_type')
->wherePivot('bind_type', '=', 'student');
}
public function parents(){
return $this->belongToMany('App\Models\Teacher', 'city_type_bind_pivot', 'city_source_id', 'bind_id')
->withPivot('city_source_id', 'bind_id', 'bind_type')
->wherePivot('bind_type', '=', 'parent');
}
}
進入tinker:
$city = App\Models\City::find(1)
>>> $city->teachers()->toSql()
=> "select * from `teachers` inner join `city_type_bind_pivot`
on `teachers`.`teacher_id` = `city_type_bind_pivot`.`bind_id`
where `city_type_bind_pivot`.`city_source_id` = ?
and `city_type_bind_pivot`.`bind_type` = ?"
>>>
然後我們可以對比分析:上面的 teachers() 方法,
一個城市對應多位老師:belongsToMany;
第一個引數:與城市模型對應的 教師模型;
第二個引數:中間表名;
第三個引數:中間表中與城市關聯的外來鍵;(即當前模型對應的city_id),體現在這一句:where city_type_bind_pivot.city_source_id = ?
這裡的?
就是City模型的當前例項對應的city_id。
第四個引數:中間表中與當前模型對應教師表關聯的外來鍵;體現在這一句:on teachers.teacher_id = city_type_bind_pivot.bind_id
另外,->wherePivot('bind_type', '=', 'brand');
就體現在sql語句的and city_type_bind_pivot.bind_type = ?
這句上面了,?
就是wherePivot
方法中的引數值brand
。
最後說明下->withPivot('city_source_id', 'bind_id', 'bind_type')
這句的作用是將中間表的某些欄位加入到對應的模型中,方便在資料展示時使用。
家下來我們再看看在教師模型中如何對應關聯上城市:
public function cities(){
return $this->belongToMany(
'App\Models\City',
'city_type_bind_pivot', // pivot table name
'bind_id', // where
'city_source_id', // on
'city_id') // 如果沒有在City中使用$primaryKey來指定主鍵的話,這個就必須加上了。
->withPivot('source_type', 'bind_id', 'bind_type')
->wherePivot('bind_type', '=', 'teacher');
}
再次進入tinker,來觀察下對應的sql:
>>> $teacher = App\Models\Teacher::find(1)
>>> $teacher->cities()->toSql()x
=> "select * from `city` inner join `city_type_bind_pivot`
on `city`.`city_id` = `source_type_binds`.`city_source_id`
where `city_type_bind_pivot`.`bind_id` = ?
and `city_type_bind_pivot`.`bind_type` = ?"
因為城市對應學生和家長的關聯方式和教師類似,就不再累述。
修改並刪除不需要的元素
前端提供一個select選擇框(注意該select框要調整為多選,同時name='city_source_id[]'),將所有城市列舉出來,資料提交後。
新建teacher記錄的同時新增其對應的城市,下面的操作是將記錄插入到pivot中間表中:
foreach ($all_input['city_source_id'] as $key => $mode_id){
$flag->cities()->attach($key,
['city_source_id' => $mode_id,
'bind_id' => $all_input['teacher_id'], //前端文字框輸入的(唯一教師編號)
'bind_type' => 'teacher']
);
}
修改並刪除:解決: 因為我們有 teacher 表與 city 的關係為N:M,Teacher在修改老師記錄時,我們對老師去過的城市進行CRUD操作時,要判斷該老師去過的城市是否已經有學生了,有的話,就不允許刪除這個對應的城市。
/**
* 追加:判斷是否已經新增;
* 如果當前的教師新增的城市在city表中不存在,就新增進去。
*/
foreach ($all_input['city_source_id'] as $key => $mode_id){
if(!$cur_teacher->cities->contains('city_id', $mode_id)){
$cur_teacher->cities()->attach($key, ['city_source_id' => $mode_id, 'bind_id' => $all_input['brand_id'], 'bind_type' => 'brand']);
}
}
// 刪除 teacher 對應的 cities 中 已經在$all_input['city_source_id']不存在的內容;
// 但是還需要判斷:該教師對應的某城市中沒有對應的學生存在;
$mode_ids = $cur_teacher->cities()->pluck('city_id')->toArray();
$del_mids = array_diff($mode_ids, $all_input['city_source_id']);
foreach ($del_mids as $city_id) {
$students = Student::getStudentsByCityAndTeacher($cur_teacher->teacher_id, $city_id);
if(empty($students)) {
$cur_teacher->cities()->detach([$city_id]);
}
}
在Student和Parent對應的Controller中,也可以如法炮製,不再累述。
瞭解Eloquent 其他常用方法
https://learnku.com/docs/laravel/5.3/eloquent-relationships#更新關聯
總結
Laravel 幫助我們封裝了很多資料庫層面的東西,但是在學習Eloquent,經常在建立模型關係時,如果感覺不知所措,不知道為啥得到的資料為什麼總是不能達到自己的要求時,可以直接進入tinker中除錯模型及其方法對應的sql語句結構。此時此刻,我們不需要view,不需要controller,只需要有遷移表和對應的模型即可。