Tìm hiểu về các Relationships trong Laravel

Trong cuộc sống, mọi sự vật đều liên kết với nhau, ví dụ như một cuốn sách phải có tác giả, hay một ngôi trường phải có nhiều lớp học. Ở trong cơ sở dữ liệu cũng vậy, các bảng cũng có thể liên kết với nhau. Ở trong Laravel đã cung cấp sẵn cho chúng ta các mối quan hệ để có thể giảm thiếu thời gian công sức của các lập trình viên. Nó giúp chúng ta truy vấn các bảng trở nên dễ dàng hơn. Ở bài này chúng ta cùng nhau tìm hiểu về các loại quan hệ

  • One to One
  • One to Many
  • Many to Many
  • Has One Through
  • Has Many Through
  • Polymorphic Relations

Các mối quan hệ trong Laravel

1. One to One

Đây là một kiểu quan hệ đơn giản nhất, mà chúng ta có thể hiểu rằng cái này chỉ phụ thuộc vào cái kia và ngược lại. Ví dụ cho dễ hình dung, ta có bảng Users và bảng Avatar thì ở đây một người dùng thì chỉ có một cái avatar và chiếc avatar này chỉ đại diện cho user đó. Để biểu diễn mối quan hệ này ta sử dụng method hasOne

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Authenticatable
{
    ...
    
    public function avatar()
    {
        return $this->hasOne('App\Avatar');
    }
}

Ở trên ta thấy tham số truyền vào đầu tiên trong method hasOne là tên của model liên quan đến với bảng đó. Khi ta khai báo như vậy, nếu muốn lấy avatar của user có id là 1 chả hạn, ta đơn giản chỉ làm như thế này.

$avatar = User::find(1)->avatar;

Chú ý ở trên Eloquent sẽ tự động match cột khóa ngoại có tên user_id của bảng Avatar tương ứng với trường id của bảng User, nếu trong trường hợp khóa ngoại không đặt là user_id mà đặt một tên khác thì cần truyền thêm một tham số thứ 2.

return $this->hasOne('App\Avatar', 'foreign_key');

Tương tự với trường hợp nếu khóa chính của bảng mà bạn liên kết, bạn không đặt là id thì cần truyền tiếp một tham số thứ 3 để cho nó hiểu được là khóa ngoại của cột này tương ứng với khóa chính với cột kia

return $this->hasOne('App\Avatar', 'foreign_key', 'local_key');

Inverse

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Avatar extends Model
{
  protected $table = 'avatar';

  public function user()
  {
    $this->belongsTo('App\User');
  }
}

Ở ví dụ trước thì ta có thể từ một user để gọi tới avatar tương ứng của user đó, giờ chúng ta cũng sẽ có thể truy vấn tương tự từ avatar này để truy vấn ra user thuộc về nó.
Ở ví dụ trên, Eloquent đã tự động match cột user_id ở bảng Avatar với cột id của bảng User. Trong Eloquent mặc định xác nhận khóa ngoại là tên bảng bỏ 's' _id, ở trong trường hợp này là user_id. Nếu khóa ngoại ở bạng Avatar không đặt đúng theo qui chuẩn trước đó thì sẽ cần phải khai báo thêm 1 tham số thứ 2 nữa.

 return $this->belongsTo('App\User', 'foreign_key');

Nếu như bạn không muốn map foreign key của bảng Avatar với cột id ở bảng User mà lại là 1 cột khác chả hạn thì tiếp tục thêm 1 tham số thứ 3 như sau.

return $this->belongsTo('App\User', 'foreign_key', 'other_key');

2. One to Many

Mối quan hệ này để biểu thị một mối quan hệ cha-con. Ví dụ một user thì sẽ có nhiều bài posts. Thì mối quan hệ này sẽ được biểu diễn như sau.

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Authenticatable
{
    
    public function posts()
    {
        return $this->hasMany('App\Post');
    }
}

Cũng tương tự quan hệ One-One Eloquent sẽ tự động tìm khóa ngoại ở model Post , ở trong trường hợp này eloquent sẽ tìm tới khóa ngoại là "snake case" tên model _id, ở ví dụ trên sẽ là user_id, Eloquent sẽ giả định user_id là khóa ngoại ở model Post. Để truy vấn tới nhiều posts thuộc một user quá đơn giản bằng cách:

$user = App\User::find(1)->posts;

và ngược lại

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
  public function user()
  {
    return $this->belongsTo('App\User');
  }
}

3. Many to Many

Quan hệ này thì nó sẽ phức tạp hơn với hai quan hệ trước nó. Ví dụ một product sẽ thuộc nhiều orders mà một order lại có nhiều products . Để biểu diễn được quan hệ này chúng ta cần sử dụng đến một bảng thế 3, mình sẽ đặt tên là order_product. và đồng thời sẽ chứa 2 cột là order_idproduct_id. Ở mối quan hệ này ta sẽ sử dụng method belongsToMany. Ví dụ biểu diễn method Product với bảng order

namespace App;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    public function orders()
    {
        return $this->belongsToMany('App\Order');
    }
}

bây giờ muốn lấy ra được một product có bao nhiêu order thì chỉ cần

$orders = App\Product::find(1)->orders;

Có một chú ý là, Eloquent sẽ tự động tìm đến bảng trung gian đặt tên theo thứ tự alphabet, trong trường hợp này bảng sẽ tên là order_product. Tuy nhiên nếu bạn muốn đặt 1 cái tên khác mà không theo quy ước ví dụ là product_order thì chỉ cần truyền thêm tham số thứ 2 vào method belongsToMany.

    return $this->belongsToMany('App\Order', 'product_order');

Tương tự bạn cũng có thể custom lại tên của 2 tên của cột liên kết tương ứng với tham số thứ 3 và thứ 4 của method belongsToMany. Với tham số thứ 3 sẽ là khóa ngoại của bảng đang định nghĩa quan hệ và tham số thứ 4 là bảng chúng ta muốn join. Ví dụ trong trường hợp này.

namespace App;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    public function orders()
    {
        return $this->belongsToMany('App\Order', 'product_order', 'product_id', 'order_id');
    }
}

Ngược lại, đối với bảng Order ta định nghĩa như sau:

namespace App;

use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
    public function product()
    {
        return $this->belongsToMany('App\Product');
    }
}

và chúng ta cũng tương tự như định nghĩa model Product, chúng ta cũng có thể truyền các tham số vào nếu muốn

namespace App;

use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
    public function product()
    {
        return $this->belongsToMany('App\Product', 'product_order', 'order_id', 'product_id');
    }
}

Lấy giá trị của bảng trung gian Để làm việc với mối quan hệ Many to Many này thì chúng ta cần sử dụng đến một bảng trung gian. Eloquent cũng hỗ trợ giúp chúng ta lấy được các giá trị của bảng này. Để truy cập đến các cột của bảng trung gian chúng ta sẽ sử dụng thuộc tính pivot. Ví dụ :

$product = App\Product::find(1);

foreach($product->orders as $order)
{
    echo $order->pivot->created_at;
}

Theo mặc định thì Eloquent chỉ lấy các trường trung gian và created_at, update_at nếu chúng ta muốn lấy ra giá trị của một cột khác thì cần khai báo thêm như sau, giả sử chúng ta cần lấy thêm trường address

return $this->belongsToMany('App\Product')->withPivot('address');

Hoặc là khi bạn muốn hai trường created_atupdate_at của bảng trung gian tự động cập nhật giá trị thì khai báo thêm

return $this->belongsToMany('App\Product')->withTimestamps();

Đôi khi người dùng lại muốn thay đổi tên của thuôc tính pivot thì phải làm như thế nào, chỉ cần sử dụng method as được khai báo trong model là xong. Ví dụ

return $this->belongsToMany('App\Product')
                ->as('newname')
                ->withTimestamps();

giờ muốn truy cập các thuộc tính của bảng trung gian thay thế pivot thành newname là được.

$product = App\Product::find(1);

foreach($product->orders as $order)
{
    echo $order->newname->created_at;
}

Một câu hỏi nữa được đặt ra là nếu muốn lấy các sản phẩm với điều kiện của bảng trung gian là hợp lệ thì sẽ như thế nào, rất đơn giản, Laravel cũng hỗ trợ chúng ta trong vấn đề này.

public function products()
{
    return $this->belongsToMany(Product::class)->wherePivot('price', '>', 20000);
}

Ở đây sẽ lấy ra các Order có giá lớn hơn 20000.

4. Has One Through

Đây là một mối quan hệ liên kết các bảng với nhau thông qua một bảng trung gian Ví dụ có 3 bảng:

users
    id - integer
    supplier_id - integer

suppliers
    id - integer

history
    id - integer
    user_id - integer

Mặc dù bảng history không chứ supplier_id nhưng chúng ta vẫn có thể truy cập đến user's history bới mối quan hệ hasOneThrough như sau

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Supplier extends Model
{
    public function userHistory()
    {
        return $this->hasOneThrough('App\History', 'App\User');
    }
}

Với tham số thứ nhất được truyền vào là tên của model mà chúng ta muốn truy cập, tham số thứ 2 là model trung gian. Chúng ta cũng có thể custom các key liên quan đến mối quan hệ này lần lượt là các tham số sau vào hàm định nghĩa quan hệ.

class Supplier extends Model
{
    public function userHistory()
    {
        return $this->hasOneThrough(
            'App\History',
            'App\User',
            'supplier_id', // Khóa ngoại của bảng trung gian user
            'user_id', // Khóa ngoại của bảng chúng ta muốn truy cập đến
            'id', // Khóa mà chúng ta muốn liên kết ở bảng supplier
            'id' // Khóa mà chúng ta muốn liên kết ở bảng user
        );
    }
}

hai bảng userhistory chúng ta định nghia như bình thường

// User.php
namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    public function supplier()
    {
         return $this->belongsTo(Supplier::class);
    }
}
// Supplier.php
namespace App;

use Illuminate\Database\Eloquent\Model;

class Supplier extends Model
{
    public function user()
    {
         return $this->hasOne(User::class);
    }
}

5. Has Many Through

Mối quan hệ has many through này cung cấp cho chúng ta cách truy cập bảng liên kết dễ dàng hơn thông qua bảng trung gian. Ví dụ một Team có nhiều bài Post thông qua bảng trung gian là User.

teams
    id - integer
    name - string

users
    id - integer
    team_id - integer
    name - string

posts
    id - integer
    user_id - integer
    title - string

Mặc dù bảng posts không chứa khóa ngoại team_id, nhưng với quan hệ hasManyThrough sẽ cung cấp cho chúng ta lấy tất cả posts của một teams bằng cách $team->posts. Để thực hiện việc này thì Eloquent sẽ kiểm tra team_id thông qua bảng users. Chúng ta sẽ biểu diễn quan hệ như sau:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Team extends Model
{
    public function posts()
    {
        return $this->hasManyThrough('App\Post', 'App\User');
    }
}

Tham số đầu tiên của quan hệ này là tên model mà chúng ta muốn truy cập, tham số thứ hai là model trung gian. Chúng ta cũng có thể custom lại tên của các khóa ngoại bằng cách thêm các tham số, với tham số thứ 3 là khóa ngoại của bảng trung gian, tham số thứ 4 là khóa ngoại của bảng mà chúng ta muốn gọi tới, tham số thứ 5 là trường mà chúng ta muốn liên kết ở bảng đang sử dụng, tham số thứ 6 là trường mà chúng ta muốn liên kết ở bảng trung gian.

class Tean extends Model
{
    public function posts()
    {
        return $this->hasManyThrough(
            'App\Post',
            'App\User',
            'country_id', // khóa ngoại của bảng trung gian
            'user_id', // khóa ngoại của bảng mà chúng ta muốn gọi tới
            'id', //trường mà chúng ta muốn liên kết ở bảng đang sử dụng
            'id' // trường mà chúng ta muốn liên kết ở bảng trung gian.
        );
    }
}

6. Polymorphic

Đây là 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 Polymorphic

Mối quan hệ này tương tự quan hệ One to One, tuy nhiên mục đích của mối quan hệ này là 1 model có thể belongsTo 1 hay nhiều model khác. Ví dụ một bài post có một image và một product cũng có một image, nếu như bình thường thì các bạn phải tạo thêm 2 bảng là post_image để lưu ảnh của postproduct_iamge để lưu ảnh của product, nếu có ti tỉ các bảng cần đến image thì lại phải tạo thêm bấy nhiêu bảng để lưu ảnh, vậy thì sẽ quá phức tạp và rối, vậy nên mới sinh ra mối quan hệ polymorphic này. Ví dụ :

posts
    id - integer
    name - string

products
    id - integer
    name - string

images
    id - integer
    url - string
    imageable_id - integer
    imageable_type - string

Đây là 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 products, còn trường imageable_type sẽ lưu tên class model PostProduct. Theo convention của laravel thì bảng lưu trung gian sẽ bắt buộc phải có 2 trường idtype nhưng để rõ ràng hơn thì sẽ lưu thêm tiền tố tên_bảng_bỏ_s +able_. Cấu trúc model

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Image extends Model
{
    public function imageable()
    {
        return $this->morphTo();
    }
}

class Post extends Model
{
    public function image()
    {
        return $this->morphOne('App\Image', 'imageable');
    }
}

class Product extends Model
{
    public function image()
    {
        return $this->morphOne('App\Image', 'imageable');
    }
}

Để lấy ra image thuộc bài posts thì sẽ trỏ tới image là được.

$post = App\Post::find(1);

$image = $post->image;

và ngược lại từ image có thể suy ngược lại post hay product phụ thuộc vào nó.

$image = App\Image::find(1);

$imageable = $image->imageable;

6.1 One to Many Polymorphic

Mối quan hệ này cũng gần giống với quan hệ One to Many. Ví dụ một User có thể comment ở cả Post lẫn Video thì chỉ cần 1 bảng comments trong trường hợp này

posts
    id - integer
    title - string
    body - text

videos
    id - integer
    title - string
    url - string

comments
    id - integer
    body - text
    commentable_id - integer
    commentable_type - string

Cấu trúc model

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    public function commentable()
    {
        return $this->morphTo();
    }
}

class Post extends Model
{
    public function comments()
    {
        return $this->morphMany('App\Comment', 'commentable');
    }
}

class Video extends Model
{
    public function comments()
    {
        return $this->morphMany('App\Comment', 'commentable');
    }
}

Lấy ra các giá trị tất cả các comment của post ta làm như sau

$post = App\Post::find(1);

foreach ($post->comments as $comment) {
    echo $comment->content;
}

hay là từ comment để lấy ngược lại cái post hay videos thuộc về nó

    $comment = App\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à video có thể có nhiều tags. Sử dụng mối quan hệ many to many polymorphic cho phép bạn truy vấn lấy ra các tags thuộc về một post hay video

posts
    id - integer
    name - string

videos
    id - integer
    name - string

tags
    id - integer
    name - string

taggables
    tag_id - integer
    taggable_id - integer
    taggable_type - string

Cấu trúc model

//post.php

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    public function tags()
    {
        return $this->morphToMany('App\Tag', 'taggable');
    }
}
//tag.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Tag extends Model
{
    public function posts()
    {
        return $this->morphedByMany('App\Post', 'taggable');
    }

    public function videos()
    {
        return $this->morphedByMany('App\Video', 'taggable');
    }
}

Ok bây giờ muốn lấy ra các tag thuộc về một post ta cũng làm tương tự nhưng mối quan hệ khác.

$post = App\Post::find(1);

foreach ($post->tags as $tag) {
    //
}

hoặc là ngược lại

$tag = App\Tag::find(1);

foreach ($tag->videos as $video) {
    //
}

All Rights Reserved