Toàn tập các quan hệ (Relationships) trong Laravel

Danh sách các quan hệ:

  1. One to One
  2. One to Many
  3. Many to Many
  4. Has One Through
  5. Has Many Through
  6. 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$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 idtype 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.