Laravel Tutorial

Bài 31: Eloquent ORM relationship trong Laravel – Phần 1

Eloquent Relationship cho phép chúng ta định nghĩ ra các mối quan hệ (relationship) giữa các model với nhau. Từ đó có thể query, làm việc với các model được định nghĩa quan hệ một cách đơn giản.

Các mỗi quan hệ này thực ra là một bản config trên code về cấu trúc link giũa các table trong database

Ví dụ: Một post sẽ có nhiều comment, hoặc một post chỉ thuộc về một tác giả

1. Định nghĩa relationship trong model

# One To One

Ví dụ: Mỗi một User thì chỉ có duy nhất một số điện thoại (phone)

Để định nghĩa mối quan hệ (1-1) này chúng ta dùng phương thức hasOne với cú pháp như sau:

hasOne($relationModel, $foreignKey, $localKey');

Trong đó:

  • $relationModel là model sẽ được link với model hiện tại
  • $foreignKey là khóa ngoại của table $relationModel ở trên dùng để liên kết giũa 2 bảng với nhau. Mặc định thì $foreignKey sẽ là tên table của model hiện tại cộng với ‘_id‘, ví dụ model User thì sẽ là user_id.
  • $localKey là cột chứa dữ liệu để liên kết với table của $relationModel. Mặc định thì $localKey sẽ là primary key của model hiện tại

Để xác định mối quan hệ này, chúng ta đặt một method phone trên User model. Phương thức phone nên trả về kết quả của 1 phương thức hasOne trên cơ sở của lớp Eloquent model:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
    * Get the phone associated with the user.
    */
    public function phone()
    {
        return $this->hasOne(Phone::class);
    }
}

Với ví dụ trên thì $foreignKey  là user_id và $localKey là id

Sau khi đã định nghĩa xong relationship cho Model lúc này bạn có thể query đến relation model bằng cách gọi đến phương thức định nghĩa relation như một thuộc tính trong model hiện tại

Ví dụ: Truy vấn dữ liệu trong relation model

// truy vấn data của table phone của table user với id = 1
$phone = User::find(1)->phone;

Bạn cũng có thể định nghĩa được mối quan hệ đảo ngược của quan hệ 1-1 này. Ví dụ một số điện thoại (phone) thì sẽ chỉ thuộc về một User

belongsTo($relatedModel, $foreignKey, $ownerKey);

Trong đó:

  • $relatedModel là model của bạn muốn liên kết.
  • $foreignKey là column của bảng hiện tại sẽ dùng để liên kết. Mặc định $foreignKey sẽ là tên của phương thức cộng với primary key của $relatedModel.
  • $ownerKey là column của bảng $relatedModel sẽ dùng để liên kết. Mặc định $ownerKey là khóa chính của $relatedModel model.

Ví dụ: Định nghĩa mối quan hệ đảo ngược cho model Phone và User

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Phone extends Model
{
    /**
    * Get the user that owns the phone.
    */
    public function user()
    {
        return $this->belongsTo(User::class);
    }
}

Lúc này query User trong Phone sẽ như sau:

// Lấy user có số điện thoại +84123456789

$user = Phone::where('number', '+84123456789')->user;

# One To Many

Mối quan hệ một nhiều được sử dụng trong trường hợp một model (A) sẽ được link đến một hoặc nhiều model khác (B). Ví dụ một bài post sẽ có rất nhiều comment

Ví dụ: Định nghĩa quan hệ một nhiều giữa model Post và model Comment

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
    * Get the comments for the blog post.
    */
    public function comments()
    {
        return $this->hasMany(Comment::class);
    }
}

Query đến function comment trong model Post

use App\Models\Post;

$comments = Post::find(1)->comments;
foreach ($comments as $comment) {
    //
}

Lấy các comment của post có title = foo

$comment = Post::find(1)->comments()                     
    ->where('title', 'foo')                     
    ->first();

Nếu bạn muốn định nghĩa mối quan hệ đảo ngược cho comment với post trong trường hợp này thì có thể sử dụng như đối với one to one ở trên. Vì lúc này một comment vẫn thuộc về một post

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    /**
    * Get the post that owns the comment.
    */
    public function post()
    {
        return $this->belongsTo(Post::class);
    }
}

Và query cũng tương tự như đối với one to one ở trên

use App\Models\Comment;

$comment = Comment::find(1);
return $comment->post->title;

# Has One Through

Đây thực ra cũng là mối quan hệ 1-1 tuy nhiên chúng phải link với nhau thông qua một model khác.

Ví dụ: Một bộ phận sẽ thuộc về một chiếc xe và một chiếc xe sẽ thuộc về một người. Trong trường hợp này bạn muốn kiểm tra xem một bộ phận thuộc về người nào đó thì bạn cần phải liên kết với một bảng trung gian trong trường hợp này thì chiếc xe chính là trung gian.

Để cho dễ hiểu bạn có thểm xem qua data struct và cách định nghĩa như sau:

Cấu trúc của các bảng

mechanics
    id - integer
    name - string

cars
    id - integer
    model - string
    mechanic_id - integer

owners
    id - integer
    name - string
    car_id - integer

Lúc này để định nghĩa owner của mechanics các bạn có thể sử dụng phương thức hasOneThrough

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Mechanic extends Model
{
    /**
    * Get the car's owner.
    */    
    public function carOwner()
    {
        return $this->hasOneThrough(Owner::class, Car::class);
    }
}

Và nếu như các column name của các bạn không theo rule trên thì bạn có thể thiết lập thủ công vào phương thức hasOneThrough

class Mechanic extends Model
{
    /**
    * Get the car's owner.
    */
    public function carOwner()
    {
        return $this->hasOneThrough(
            Owner::class,
            Car::class,
            'mechanic_id', // Foreign key on the cars table...
            'car_id', // Foreign key on the owners table...
            'id', // Local key on the mechanics table...
            'id' // Local key on the cars table...
        );
    }
}

# Has Many Through

Mối quan hệ này cũng tương tự như mối quan hệ one to many, chỉ khác là nó phải cần thêm một model thứ 3 để xác định được

Ví dụ: Một project sẽ có nhiều môi trường và một environment sẽ có nhiều lần deploy. Như vậy để có thể lấy ra được các deploy của project bạn phải cần thêm thông tin của environment. Ở đây environment đóng vai trò làm trung gian

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Project extends Model
{
    /**
    * Get all of the deployments for the project.
    */
    public function deployments()
    {
        return $this->hasManyThrough(Deployment::class, Environment::class);
    }
}

Nếu trong trường hợp tên các cột của bạn không theo rule trên thì bạn có thể xác định thủ công vào trong phương thức hasManyThrough với các biến truyền vào như sau:

class Project extends Model
{
    public function deployments()
    {
        return $this->hasManyThrough(
            Deployment::class,
            Environment::class,
            'project_id', // Foreign key on the environments table...
            'environment_id', // Foreign key on the deployments table...
            'id', // Local key on the projects table...
            'id' // Local key on the environments table...
        );
    }
}

# Many To Many

Đây là mối quan hệ thường gặp trong các chức năng phân quyền của ứng dụng

Ví dụ: 1 user sẽ có nhiều roles và 1 role cũng sẽ thuộc về nhiều user

Để xác định relationship này, cần thiết phải có 3 bảng: users, roles và role_user. Bảng role_user sẽ chứa 2 column user_id và role_id

users
    id - integer
    name - string

roles
    id - integer
    name - string

role_user
    user_id - integer
    role_id - integer

Quan hệ many-to-many được định nghĩa bằng cách gọi phương thức belongsToMany dựa trên Eloquent class

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
    * The roles that belong to the user.
    */
    public function roles()
    {
        return $this->belongsToMany(Role::class);
    }
}

Một khi các mối quan hệ được xác định, bạn có thể truy cập vào roles bằng cách truy cập dynamic property:

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

Giống như tất cả các relationship khác, bạn có thể gọi phương thức roles và tiếp tục cho thêm vào các query:

$roles = App\User::find(1)->roles()->orderBy('name')->get();

Như đã đề cập trước đó, để xác định tên bảng của bảng tham gia vào relationship, Eloquent sẽ join 2 model liên quan theo thứ tự của bảng chữ cái. Tuy nhiên, bạn cũng có thể ghi đè quy ước này. Bạn có thể làm như vậy bằng cách thêm vào 1 đối số thứ 2 trong phương thức belongsToMany như sau:

return $this->belongsToMany('Role::class', 'role_user');

Ngoài tùy biến trên, bạn có thể tùy biến các tên cột của các keys bằng cách truyền thêm đối số cho phương thức belongsToMany. Đối số thứ 3 là tên foreign key mà bạn đang xác định relationship, trong khi đối số thứ 4 là tên foreign key trong model mà bạn đang join đến

return $this->belongsToMany('Role::class', 'role_user', 'user_id', 'role_id');

Định nghĩa Inverse của quan hệ trên

<?php

namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Role extends Model
{
    /**
    * The users that belong to the role.
    */
    public function users()
    {
        return $this->belongsToMany(User::class);
    }
}

Truy xuất vào table trung gian

$user = User::find(1);

foreach ($user->roles as $role) {
    echo $role->pivot->created_at;
}

Nếu bảng pivot của bạn chứa các thuộc tính mở rộng, bạn phải xác định chúng khi xác định các mối quan hệ:

return $this->belongsToMany('Role::class')->withPivot('column1', 'column2');

Nếu bạn muốn bảng pivot của bạn tự động có created_atupdated_at, sử dụng các phương thức withTimestamps() vào trong định nghĩa của mối quan hệ:

return $this->belongsToMany('Role::class')->withTimestamps();

Bạn cũng có thể lọc các kế quả trả về bởi belongsToMany bằng cách sử dụng phương thức wherePivot and wherePivotIn khi định nghĩa các mối quan hệ:

return $this->belongsToMany('Role::class')->wherePivot('approved', 1);
return $this->belongsToMany('Role::class')->wherePivotIn('approved', [1, 2]);

# Has Many Through

Quan hệ “has-many-through” cung cấp một thuận tiện short-cut để truy cập vào các mối quan hệ xa thông qua một mối quan hệ trung gian

Ví dụ, một Country model có thể có nhiều Post model thông qua một User model trung gian. Trong ví dụ này, bạn có thể dễ dàng lấy tất cả các blog post cho 1 country

Cấu trúc table:

countries
    id - integer
    name - string

users
    id - integer
    country_id - integer
    name - string

posts
    id - integer
    user_id - integer
    title - string

Để thực hiện các truy vấn này, Eloquent kiểm tra các country_id trên bảng user trung gian. Sau khi tìm ra id của user phù hợp, chúng được sử dụng để truy vấn bảng posts. Bây giờ chúng ta đã xem xét các cấu trúc bảng cho các mối quan hệ, hãy định nghĩa nó trên Country model

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Country extends Model
{
    /**
    * Get all of the posts for the country.
    */
    public function posts()
    {
        return $this->hasManyThrough('Post::class', 'User::class');
    }
}

Nếu bạn muốn tùy chỉnh các foreign key của relationship, bạn có thể truyền vào các đối số thứ 3 và thứ 4 của phương thức hasManyThrough. Đối số thứ 3 là foreign key của model trung gian, đối số thứ 4 là foreign key của model cuối cùng và đối số thứ 5 là local key

class Country extends Model
{
    public function posts()
    {
        return $this->hasManyThrough(
            'Post::class', 'User::class',
            'country_id', 'user_id', 'id'
        );
    }
}

2. Default Model

Trong các phương thức belongsTohasOnehasOneThrough và morphOne sử dụng để định nghĩa kiểu quan hệ giữa các model với nhau nó sẽ trả về null nếu như dữ liệu của quan hệ không tồn tại trong database

Trong trường hợp này laravel có cung cấp cho mọi người định nghĩa thêm giá trị default với phương thức withDefault

/**
* Get the author of the post.
*/
public function user()
{
    return $this->belongsTo(User::class)->withDefault();
}

Để xác định thêm attribute mặc định được truyền vào model bạn có thể truyền thêm mảng attribute vào phương thức hoặc một closure trả về mảng giá trị attribute

/**
* Get the author of the post.
*/
public function user()
{
    return $this->belongsTo(User::class)->withDefault([
        'name' => 'Guest Author',
    ]);
}

Hoặc

/**
* Get the author of the post.
*/
public function user()
{
    return $this->belongsTo(User::class)->withDefault(function ($user, $post) {
        $user->name = 'Guest Author';
    });
}

 

Leave a Comment