Eloquent: relationships in laravel-Phần 1

Hôm nay mình xin giới thiệu với các bạn về cách sử dụng relationship model trong Laravel. Mỗi một PHP Framework có một cách tạo relationships model khác nhau, tuy nhiên tựu chung phía sau đó cũng chỉ là những câu lệnh SQL mà thôi. và Laravel cũng vậy.

Tài liệu tham khảo tại trang chủ của Laravel: https://laravel.com/docs/5.2/eloquent-relationships

Giới thiệu

Các bảng cơ sở dữ liệu thường liên quan tới nhau. Ví dụ, 1 bài đăng blog có thể có nhiều comments, hoặc 1 order có thể có nhiều người dùng đã đặt nó. Eloquent giúp cho việc quản lý và làm việc với các mối quan hệ này trở nên dễ dàng hơn, và hỗ trợ các loại khác nhau của relationships:

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

Định nghĩa relationships

Eloquent Relationships được định nghĩa là các function trong các lớp Eloquent model. Giống như các Eloquent model, các relationships cũng hỗ trợ các query builder mạnh mẽ, định nghĩa relationship cung cấp các method chaining và query mạnh mẽ. Ví dụ:

$user->posts()->where('active', 1)->get();

Nhưng trước khi đi sâu vào cách sử dụng, chúng ta cùng tìm hiểu cách làm thế nào để xác định từng loại relationship.

One to One

Mối quan hệ one-to-one là mối quan hệ hết sức cơ bản. Ví dụ, một User có thể được liên kết với 1 Phone. Để 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;

use Illuminate\Database\Eloquent\Model;

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

Đối số đầu tiên truyền cho phương thức hasOne là tên của model được liên kết. Một khi các mối quan hệ được xác định, chúng ta có thể truy xuất các bản ghi sư dụng Eloquent's dynamic properties. Dynamic properties cho phép bạn truy cập vào các relationship functions như thể nó là thuộc tính được định nghĩa trên các model:

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

Eloquent giả định các foreign key dựa theo tên model.Trong trường hợp này, Phone model sẽ tự động lấy foreign key là user_id. Nếu bạn muốn thay đổi điều này, bạn có thể thêm vào đối số thứ 2 của hasOne method:

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

Ngoài ra, Eloquent cho rằng foreign key cần phải có một giá trị phù hợp với cột id của parent. Nói cách khác, Eloquent sẽ tìm kiếm những giá trị của cột id của User trong cột user_id của các bản ghi Phone. Nếu bạn muốn relationship sử dụng các cột khác với cột id, bạn có thể truyền thêm 1 đối số thứ 3 trong phương thức hasOne như sau:

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

Định nghĩa các Inverse của Relationship

Vì vậy, chúng ta có thể truy cập vào Phone model từ User model. Bây giờ, chúng ta sẽ định nghĩa 1 relationship trên Phone model sẽ cho phép chúng ta truy cập vào User model. Chúng ta sử dụng phương thức nghịch đảo của hasOnebelongsTo:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

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

Trong ví dụ trên, Phone model sẽ cố gắng để match cột user_id với 1 giá trị idUser model. Tuy nhiền , nếu foreign key trên Phone model không phải là user_id, bạn có thể tùy chỉnh bằng cách thêm 1 đối số thứ 2 vào phương thức belongsTo như sau:

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

Và cũng giống như phương thức hasOne, bạn có thể thêm vào đối số thứ 3 để thay đổi mặc định của phương thức là đối chiếu với cột id của User model như sau:

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

One To Many

Một quan hệ one-to-many được sử dụng để xác định các mối quan hệ khi một model sở hữu nhiều số lượng của model khác. Ví dụ, 1 blog post có nhiều comment. Giống như nhiều Eloquent relationship khác, one-to-many được xác định bằng 1 function được đặt ở model của bạn:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

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

Nhớ rằng, Eloquent sẽ tự động xác định đúng cột foreign key trên Comment model. Theo quy ước, Eloquent sẽ có "snake case" tên của model và hậu tố _id. Vì vậy, trong ví dụ này, Eloquent sẽ hiểu foreign key trên Comment model chính là post_id.

Một khi relationship được xác định, chúng ta có thể truy cập lấy collection của comments bằng cách truy cập lấy comments property như sau:

$comments = App\Post::find(1)->comments;

Dĩ nhiên, vì tất cả các relationship cũng phục vụ query builder, bạn cũng có thể thêm các ràng buộc để comment được lấy ra và tiếp tục thêm các chuỗi query:

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

Giống như hasOne, bạn cũng có thể ghi đè các foreign key là local key bằng cách thêm các đối số cho phương thức hasMany.

return $this->hasMany('App\Comment', 'foreign_key');

return $this->hasMany('App\Comment', 'foreign_key', 'local_key');

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

Bây giờ chúng ta có thể truy cập tất cả các comments, hãy định nghĩa một quan hệ để cho phép comments có thể truy cập từ 1 post. Để xác định các inverse của quan hệ hasMany, chúng ta dùng phương thức belongsTo:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

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

Một khi relationship được xác nghĩa, chúng ta có thể lấy Post model cho một Comment bằng cách truy cập post "dynamic property":

$comment = App\Comment::find(1);

echo $comment->post->title;

Trong ví dụ trên, Eloquent sẽ cố gắng để match một post_id từ Comment model với 1 id của Post model. Eloquent xác định mặc định tên foreign key bằng cách kiểm tra tên của phương thức relationship và nối với hậu tố _id. Tuy nhiên bạn vẫn có thể tùy chỉnh nó bằng cách thêm đối số thứ 2 trong phương thức belongsTo như sau:

/**
 * Get the post that owns the comment.
 */
public function post()
{
    return $this->belongsTo('App\Post', 'foreign_key');
}

Nếu model cha không sử dụng id làm khóa chính của nó, bạn có thể tùy chọn nó bằng đối số thứ 3 của phương thức belongsTo

/**
 * Get the post that owns the comment.
 */
public function post()
{
    return $this->belongsTo('App\Post', 'foreign_key', 'other_key');
}

Many To Many

many-to-many, mối quan hệ hơi phức tạp hơn so với hasOnehasMany. Một ví dụ cho mối quan hệ này như là 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à user_role. Bảng user_role sẽ chứa 2 column user_id và role_id.

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. Ví dụ, hãy định nghĩa phương thức roles trên User model.

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

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

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);

Dĩ nhiên, 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('App\Role', '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('App\Role', 'role_user', 'user_id', 'role_id');

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

Để xác định các nghịch đảo của mối quan hệ many-to-many, bạn chỉ cần đặt một phương thức belongsToMany trên model của bạn. Chúng ta hay định nghĩa phương thức users trên Role model:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

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

Retrieving Intermediate Table Columns

Như bạn đã biết, làm việc với mối quan hệ many-to-many đòi hỏi cần thêm 1 bảng trung gian. Eloquent cung cấp một số cách hữu ích để tương tác với bảng này. Ví dụ, chúng ta hãy giả định đối tượng đang sử dụng của chúng ta đã có nhiều đối tượng Role. Sau khi truy cập mối quan hệ này, chúng ta có thể truy cập vào bảng trung gian bằng cách sử dụng pivot attribute trên model.

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

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

Chú ý rằng, mỗi Role model chúng ta nhận được tự động gán một pivot attribute. Thuộc tính này chứa 1 model đại diện cho bảng trung gian, và có thể sử dụng như bất kỳ Eloquent model nào khác. Theo mặc định, chỉ có các keys sẽ có mặt trên các pivot object. 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('App\Role')->withPivot('column1', 'column2');

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

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

Filtering Relationships Via Intermediate Table Columns

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('App\Role')->wherePivot('approved', 1);

return $this->belongsToMany('App\Role')->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. Hãy nhìn vào các bảng cần thiết để xác định mối quan hệ này:

countries
    id - integer
    name - string

users
    id - integer
    country_id - integer
    name - string

posts
    id - integer
    user_id - integer
    title - string

Mặc dù post không chứa cột country_id, mối quan hệ hasManyThrough cung cấp quyền truy cập vào post của country thông qua $country->posts. Để 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('App\Post', 'App\User');
    }
}

Đối số đầu tiên truyền cho phương thức hasManyThrough là tên của model cuối cùng chúng ta muốn truy cập, trong khi đối số thứ 2 là tên của model trung gian. 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(
            'App\Post', 'App\User',
            'country_id', 'user_id', 'id'
        );
    }
}