Toàn tập các quan hệ (Relationships) trong Laravel
Danh sách các quan hệ:
- One to One
- One to Many
- Many to Many
- Has One Through
- Has Many Through
- Polymorphic Relations
1. One to one
Quan hệ này theo mình thấy thì dễ hiểu và dễ sử dụng nhất, hiểu đơn giản thằng A chỉ phụ thuộc vào thằng B và ngược lại thằng B chỉ phụ thuộc vào thằng A.
VD cho dễ hình dung, thằng A ở đây mình coi là 1 model User (chứa thông tin cơ bản) và thằng B mình coi là 1 model UserDetail (chứa thông tin chi tiết).
Ta có thể biểu diễn mối quan hệ One to one trong từng model như sau:
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
...
public function detail()
{
return $this->hasOne(UserDetail::class, 'user_id');
}
}
Và để lấy thông tin chi tiết của user
$user = User::with('detail')->find(1);
$user_detail = $user->detail;
Giải thích các tham số của hàm hasOne:
return $this->hasOne($related, $foreignKey = null, $localKey = null);
$related: Tên model quan hệ tới. Cụ thể model User có quan hệ hasOne tới model UserDetail
$foreignKey: Khóa ngoại liên kết với model UserDetail. Cụ thể là user_id
$localKey: Khóa chính của model User. Cụ thể là id
Từ dữ liệu trên ta có quan hệ
return return $this->hasOne(UserDetail::class, 'user_id', 'id');
Ta có thể rút gọn
return return $this->hasOne(UserDetail::class);
Sở dĩ vì Laravel đã tự động tạo cột khóa ngoại user_id của model UserDetail tới cột khóa chính id của model User
Bạn hãy thử chọc vào hàm hasOne trong core laravel bạn sẽ thấy $foreignKey và $localKey được tạo tự động nếu không truyền giá trị vào.
$foreignKey = $foreignKey ?: $this->getForeignKey();
$localKey = $localKey ?: $this->getKeyName();
Nhưng theo mình và mình hay dùng thì hãy viết như này cho clear, dễ hiểu và đỡ nhầm lẫn:
return $this->hasOne(UserDetail::class, 'user_id');
Kể từ giờ trở về sau mình sẽ không nói lại phần viết tắt này nữa nếu không cần thiết nhé!
Ngược lại, quan hệ từ UserDetail tới User:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class UserDetail extends Model;
{
...
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
}
Các tham số truyền vào hàm belongsTo tương tự hàm hasOne
return $this->belongsTo($related, $foreignKey = null, $localKey = null);
Và cách mình hay dùng vẫn là
return $this->belongsTo(User::class, 'user_id');
Lấy thông tin User từ UserDetail
$userDetail = UserDetail::with('user')->find(1);
$email = $userDetail->user->email;
2. One to Many
Quan hệ này sẽ biểu thị mối quan hệ cha và con, ví dụ 1 user sẽ có nhiều bài posts. Lúc này quan hệ sẽ được biểu diễn như sau:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
...
public function posts()
{
return $this->hasMany(Post::class, 'user_id');
}
}
user_id là khóa ngoại trong bảng posts.
Xem tất cả bài viết của user có id = 1.
$user = User::with('posts')->find(1);
$posts = $user->posts;
Thiết lập quan hệ giữa model Post tới model User
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Models\User;
class Posts extends Model
{
...
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
}
Lấy thông tin user thông qua post:
$post = Post::with('user')->find(1);
$user = $post->user;
3. Many to many
Quan này sẽ phức tạp hơn 1 chút và cần có 1 bảng trung gian.
Ví dụ 1 bài post sẽ có nhiều category, và 1 category sẽ có nhiều bài post. Quan hệ này giúp ta có thể từ 1 bài post có thể lấy được tất cả category mà nó đang được gắn và từ 1 category có thể lấy được tất cả các bài post mà nó đang có.
Ta sẽ có 3 bảng sau: posts, post_categories, categories; Bảng trung gian là post_categories.
Thiết lập quan hệ giữa model Post tới model Category
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
...
public function categories()
{
return $this->belongsToMany(Category::class, 'post_categories', 'post_id', 'category_id');
}
}
Thiết lập quan hệ giữa model Category tới model Post
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
...
public function posts()
{
return $this->belongsToMany(Post::class, 'post_categories', 'category_id', 'post_id');
}
}
Lấy tất cả category mà bài post có id = 1 đang được gán:
$post = Post::with('categories')->find(1);
$categories = $post->categories;
Lấy tất cả bài post mà category có id = 1 đang có:
$category = Category::with('posts')->find(1);
$posts = $category->posts;
Lấy giá trị bảng trung gian:
Mặc định laravel trả về dữ liệu trung gian với key là pivot và chỉ trả về dữ liệu 2 khóa ngoại vì vậy nếu trong bảng trung gian có các dữ liệu khác mà bạn muốn lấy kèm theo (ví dụ lấy thêm dữ liệu trường sort trong bảng trung gian):
Ở trong model Post ta sẽ sửa lại quan hệ với category một chút
public function categories()
{
return $this->belongsToMany(Category::class, 'post_categories', 'post_id', 'category_id')->withPivot('sort');
}
$post = Post::with('categories')->find(1);
$categories = $post->categories;
foreach($post->categories as $category)
{
echo $category->pivot->sort;
}
Thêm kiệu cho bảng trung gian
public function categories()
{
return $this->belongsToMany(Category::class, 'post_categories', 'post_id', 'category_id')->withPivot('sort')->wherePivot('category_id', '>', 1);
}
Câu lệnh trên sẽ lấy ra category có id > 1
4. Has One Through
Hình dung ta có quan hệ 3 bảng như sau:
users
id - integer
name - string
user_details
id - integer
user_id - integer
countries
id - integer
user_detail_id - integer
title - string
Hiểu đơn giản 1 người dùng sẽ có 1 bàn thông tin chi tiết về mình và thuộc về 1 quê quán. Nhìn vào bảng users không có chứa khóa ngoại tới bảng countries nhưng ta vẫn có thể lấy được tên quê quán thông qua bảng trung gian user_details.
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
public function country()
{
return $this->hasOneThrough(Country::class, UserDetail::class, 'user_id', 'user_detail_id');
}
}
Các tham số truyền vào hàm hasOneThrough:
return $this->hasOneThrough($related, $through, $firstKey = null, $secondKey = null, $localKey = null, $secondLocalKey = null)
$related: tên model quan hệ tới để lấy dữ liệu (theo ví dụ trên là model Country để lấy quê quán)
$through: tên model trung gian để liên kết (theo ví dụ trên là model UserDetail)
$firstKey: khóa ngoại của bảng trung gian (là user_id nằm trong model trung gian UserDetail)
$secondKey: khóa ngoại của bảng muốn lấy dữ liệu (là user_detail_id nằm trong model Country)
$localKey: Khóa liên kết, từ UserDetail (user_id) đến User (id)
$secondLocalKey: Khóa liên kết, từ Country (user_detail_id) đến UserDetail (id)
2 model UserDetail và Country định nghĩa quan hệ như bên dưới (để sử dụng quan hệ hasOneThrough không nhất thiết phải viết rõ quan hệ trong 2 model này nhé, mình chỉ ghi thêm cho rõ hơn về quan hệ thôi):
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class UserDetail extends Model
{
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
}
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Country extends Model
{
public function user_detail()
{
return $this->belongsTo(UserDetail::class, 'user_detail_id');
}
}
Từ model User ta có thể lấy được quê quán của người đó:
$user = User::with('country')->find(1);
$country = $user->country->title;
5. Has many through
Quan hệ này giúp ta có thể truy cập sang quan hệ mong muốn 1 cách dễ dàng thông qua 1 bảng chung gian. VD ta có nhiều groups, mỗi groups lại có nhiều user và mỗi user lại có nhiều bài posts.
Vậy làm sao để từ group ta có thể lấy được tất cả các bài viết thuộc group đó. Ta sẽ sử dụng bảng trung gian là user để lấy được dữ liệu. Xét 3 bảng sau:
groups
id - integer
name - string
users
id - integer
group_id - integer
posts
id - integer
user_id - integer
Nhìn vào 3 bảng trên ta thấy bảng posts không hề chứa khóa ngoại liên kết tới bảng groups nhưng ta vẫn có thể lấy được các bài viết thuộc group đó thông quá quan hệ hasManyThrough
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Group extends Model
{
public function posts()
{
return $this->hasManyThrough(Post::class, User::class, 'group_id', 'user_id');
}
}
Nhìn khá là giống với quan hệ hasOneThrough các tham số truyền vào là tương tự nhé
return $this->hasManyThrough($related, $through, $firstKey = null, $secondKey = null, $localKey = null, $secondLocalKey = null)
Từ model Group ta có thể lấy được tất cả các bài post thuộc về nó
$group = Group::with('posts')->find(1);
$posts = $group->posts;
6. Polymorphic
Mối quan hệ đa hình trong Laravel cho phép 1 model có thể belongsTo nhiều model khác mà chỉ cần dùng 1 associate.
6.1. One To One
posts
id - integer
title - string
users
id - integer
name - string
images
id - integer
url - string
imageable_id - integer
imageable_type - string
Mối quan hệ này cho phép 1 model có thể belongsTo 1 hoặc nhiều model khác.
VD: model Post và model User có thể cùng lưu trữ link ảnh vào model Image, gọi là chia sẻ mối quan hệ đa hình với model Image. Chỉ từ 1 danh sách link ảnh duy nhất có thể dùng chung cho cả các bài post và user. Nếu không sử dụng quan hệ này ta sẽ phải tạo riêng ra 2 bảng post_images và user_images. Sau đó gán khóa ngoại vào mỗi bảng.
Cách để xây dựng mối quan hệ polymorphic này: Với imageable_id
sẽ lưu id
của bảng posts
và bảng users, còn trường imageable_type
sẽ lưu tên class model Post
và User. Theo rule của laravel bảng trung gian sẽ bắt buộc phải có 2 field id
và type
nhưng để rõ ràng hơn thì sẽ lưu thêm tiền tố tên_bảng_bỏ_s + able_ + id_hoặc_type
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Image extends Model
{
public function imageable()
{
return $this->morphTo();
}
}
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
public function image()
{
return $this->morphOne(Image::class, 'imageable');
}
}
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
public function image()
{
return $this->morphOne(Image::class, 'imageable');
}
}
Để lấy ra ảnh của 1 bài post:
$post = Post::find(1);
$image = $post->image;
Để lấy ra ảnh của 1 user:
$user = User::find(1);
$image = $user->image;
Và ngược lại có thể truy từ image ra xem post hay user phụ thuộc vào nó:
$image = Image::find(1);
$imageable = $image->imageable;
6.1. One to Many Polymorphic
posts
id - integer
title - string
products
id - integer
title - string
comments
id - integer
content - text
commentable_id - integer
commentable_type - string
Mối quan hệ này cũng gần giống với quan hệ One to One
. VD 1 User
có thể comment ở cả Post
lẫn Product
thì chỉ cần 1 bảng comments
để lưu trữ chung.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
public function commentable()
{
return $this->morphTo();
}
}
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
}
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
}
Để lấy ra comment của 1 bài post:
$post = Post::find(1);
foreach ($post->comments as $comment) {
echo $comment->content;
}
Để lấy ra comment của 1 product:
$product = Product::find(1);
foreach ($product->comments as $comment) {
echo $comment->content;
}
Và ngược lại có thể truy từ comment ra xem post hay product phụ thuộc vào nó:
$comment = Comment::find(1);
$commentable = $comment->commentable;
6.1 Many to Many Polymorphic
Quan hệ này sẽ phức tạp hơn một chút. Ví dụ một post
hay là product có thể có nhiều tags
. Sử dụng mối quan hệ many to many polymorphic
cho phép truy vấn lấy ra các tags
thuộc về một post
hay video
posts
id - integer
title - string
products
id - integer
title - string
tags
id - integer
name - string
taggables
tag_id - integer
taggable_id - integer
taggable_type - string
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Tag extends Model
{
public function posts()
{
return $this->morphedByMany(Post::class, 'taggable');
}
public function products()
{
return $this->morphedByMany(Product::class, 'taggable');
}
}
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
}
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
}
Để lấy ra các của 1 bài post:
$post = Post::find(1);
foreach ($post->tags as $tag) {
echo $tag->name;
}
Để lấy ra các tag của 1 product:
$product = Product::find(1);
foreach ($product->tags as $tag) {
echo $tag->name;
}
Và ngược lại có thể truy từ tag ra xem post hay product phụ thuộc vào nó:
$tag = Tag::find(1);
foreach ($tag->posts as $post) {
echo $post->title;
}
Tổng kết:
- Tùy thuộc vào nhu cầu sử dụng ta sẽ chọn loại quan hệ phù hợp cho bài toán của chúng ta, hiểu đơn giản và tránh máy móc.
- Khi sử dụng quan hệ nên sử dụng Eager Loading trước khi trỏ đến (sử dụng) quan hệ, nếu không bạn sẽ gặp phải trường hợp n + 1 query rất khủng khiếp.