Eloquent Relationships in Laravel 5.3 (Chap 1)

Xin chào các bạn, để tiếp tục với loạt bài những điểm mới của Laravel 5.3, hôm nay mình sẽ giới thiệu về Eloquent: Relationships. Nếu đã làm việc với DB thì chắc mọi người đều biết là một table trong DB thường liên kết với các bảng khác theo các mối quan hệ: 1-1 (One to One), 1-n (One to Many), n-n (Many to Many),... Và Laravel hỗ trợ bạn cả những mối quan hệ khác giúp cho việc thao tác và quản lý dữ liệu trở nên rất dễ dàng như: Has Many Through, Polymorphic Relations, Many To Many Polymorphic Relations. Có thể những thuật ngữ này khá khó hiểu nhưng đừng lo lắng, hãy cùng mình đi tìm hiểu về nó nhé (hehe)

Eloquent Relationships là gì?

Trước tiên thì các bạn cần biết rõ Eloquent là gì, link tham khảo thêm tại đây. Nói ngắn gọn là mỗi một table trong DB sẽ tương ứng với 1 class Model. Mỗi 1 model cho phép bạn truy vấn trực tiếp vào dữ liệu trong table tương ứng (Vd tên table là users thì tên Model sẽ là số ít của tên table: User). Còn Eloquent relationships được định nghĩa như là những function trên class Model đó. Nghe cũng có vẻ đơn giản, nhưng Laravel làm thế nào để hiểu được function đó là relationships và dựa vào những tham số gì để thực hiện truy vấn trong DB, giờ mình sẽ đi sâu vào từng quan hệ để định nghĩa rõ ràng hơn (huytsao).

One To Many

Vì quan hệ 1-1 khá đơn giản và ít sử dụng nên mình bỏ qua và đi sang luôn quan hệ 1-n. Ví dụ 1 nhà ga sẽ thuộc 1 khu vực, mà 1 khu vực có thể có nhiều nhà ga. Mối quan hệ giữa chúng sẽ là 1 khu vực - n nhà ga. Ta đã có 2 table trong DB đó là stations (nhà ga) và areas (khu vực), tương ứng với đó là 2 model Station và Area. Để thực hiện lấy các bản ghi thông qua 2 table thì ta sẽ định nghĩa các function relationship như sau:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Area extends Model
{
    /**
     * Get the phone record associated with the area.
     */
    public function stations()
    {
        return $this->hasMany('App\Station');
    }
}

Trong Area Model mình vừa viết 1 function tên là stations. Function này sẽ trả về kết quả của function hasMany dựa trên lớp Eloquen Model. Tham số đầu tiên truyền vào phương thức hasMany là tên của model liên quan.

Ở đoạn code trên chúng ta chỉ truyền vào function hasMany 1 tham số là tên của Model và Eloquent sẽ mặc định khóa ngoại của Station model là area_id(sử dụng công thức tên khóa ngoại = tên của phương thức quan hệ + _id) và khóa ngoại này sẽ có 1 giá trị tương ứng với column id của table areas (hoặc thuộc tính $primaryKey được định nghĩa trong Area Model). Hay nói cách khác là Eloquent sẽ tìm giá trị của area id trong column area_id của bản ghi Station. Nếu bạn không muốn khóa ngoại là mặc định cũng như sử dụng 1 tên column khác id thì bạn có thể truyền 2 tham số vào function hasMany:

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

Và khi 1 quan hệ được định nghĩa như trên thì chúng ta có thể lấy được các bản ghi sử dụng Eloquent dynamic properties:

$station = Area::find(1)->stations;
foreach ($stations as $station) {
    //
}

Tất cả các quan hệ cũng được dùng như Query Builders, bạn có thể thêm các điều kiện truy vấn khác như:

$station = App\Area::find(1)->stations()
    ->where('name', 'Tan Son Nhat')
    ->first();

Chỉ cần định nghĩa như trên là ta có thể truy cập model Area từ model Station. Ta cũng có thể truy cập ngược lại từ model Station đến model Area bằng phương thức belongsTo như sau:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Station extends Model
{
    /**
     * Get the area that owns the station.
     */
    public function area()
    {
        return $this->belongsTo('App\Area');
    }
}

Eloquent sẽ hiểu function trên nếu định nghĩa đầy đủ sẽ là:

public function area()
{
    return $this->belongsTo('App\Area', 'id', 'area_id');
}

Nếu bạn không muốn các khóa có tên mặc định như trên hoặc muốn join với model con ở một column khác thì ta lần lượt truyền vào các tham số :

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

Và ta lấy được thông tin của Area một cách dễ dàng bằng cách truy cập vào Eloquent dynamic properties:

$area = App\Station::find(1)->area->address;

Many To Many

Quan hệ này cũng không khó hơn quan hệ one to many nhiều lắm. Giả sử một khóa học có thể có nhiều môn học, và một môn học lại được sử dụng trong nhiều khóa học. Vậy quan hệ giữa khóa học là nhiều - nhiều. Để định nghĩa mối quan hệ này thì ngoài 2 bảng courses, subjects ta còn cần thêm 1 bảng trung gian là course_subject (primary key của bảng này sẽ gồm 2 cột là course_idsubject_id). Mình sẽ sử dụng function belongsToMany ở cả 2 model. Ví dụ trên Course model như sau:

 namespace App;

use Illuminate\Database\Eloquent\Model;

class Course extends Model
{
    /**
     * The subjects that belong to the course.
     */
    public function subjects()
    {
        return $this->belongsToMany('App\Subject');
    }
}

Ta có thể xác định tên của table để join quan hệ này (ở đây là course_subject). Để tùy chọn cột để join, ta có thể truyền thêm tham số vào function belongsToMany với tham số thứ 3 là tên của khóa ngoại ứng với model định nghĩa quan hệ (ở đây là Course) và tham số thứ 4 là tên column của khóa ngoại ứng với model tham chiếu đến (ở đây là Subject):

    return $this->belongsToMany('App\Subject', 'course_subject', 'course_id', 'subject_id');

Chú ý rằng Eloquent mặc định chỉ có các khóa ngoại tồn tại trong bảng trung gian. Nếu bạn muốn định nghĩa thêm vài column nào đó vào trong bảng thì ta sẽ định nghĩa như sau:

    return $this->belongsToMany('App\Subject')->withPivot('column1', 'column2');

Thêm vào đó, bạn cũng có thể định nghĩa thêm các timestamps như created_atudpated_at khi định nghĩa quan hệ:

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

Định nghiã xong rồi thì làm thế nào để tương tác với bảng trung gian? Rất đơn giản chỉ cần sử dụng thuộc tính pivot trên model:

$course = App\Course::find(1);

foreach ($course->subjects as $subject) {
    echo $subject->pivot->created_at;
}

Bạn cũng có thể lọc các kết quả trả về bởi belongsToMany bằng cách sử dụng wherePivotwherePivotIn:

return $this->belongsToMany('App\Subject')->wherePivot('active', 1);

return $this->belongsToMany('App\Subject')->wherePivotIn('active', [1, 2]);

Has Many Through

Nói ngắn gọn thì quan hệ này hỗ trợ cho việc truy cập những quan hệ từ xa thông qua một quan hệ trung gian. Ví dụ model Area có thể có nhiều model Train thông qua model Station:

areas
    id - integer
    address - string

stations
    id - integer
    area_id - integer
    name - string

trains
    id - integer
    station_id - integer
    name - string

Nhìn vào bảng trên chúng ta dễ dàng nhận thấy table stations chứa khóa ngoại area_id trỏ đến id của table areas, table trains lại chứa khóa ngoại station_id trỏ đến id của table stations. Để thực hiện truy vấn này, Eloquent sẽ tìm area_id trên bảng trung gian stations, sau khi thấy các station id phù hợp, chúng sẽ được dùng để truy vấn bảng trains. Vậy định nghĩa mối quan hệ đó trong model như thế nào? Hãy xem đoạn code bên dưới:

 <?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Area extends Model
{
    /**
     * Get all of the trains for the area.
     */
    public function trains()
    {
        return $this->hasManyThrough('App\Station', 'App\Train');
    }
}

Tham số đầu tiên của function hasManyThrough là tên của model cuối nơi mà mình muốn lấy dữ liệu, tham số thứ 2 là tên của model trung gian. Nếu muốn cài đè vào các khóa ngoại mà Eloquent mặc định thì bạn cần truyền vào tham số thứ 3 là tên của khóa ngoại trên model trung gian, tham số thứ 4 là tên của khóa ngoại trên model cuối và tham số thứ 5 là local key. Ví dụ:

class Area extends Model
{
    public function trains()
    {
        return $this->hasManyThrough(
            'App\Train', 'App\Station',
            'area_id', 'station_id', 'id'
        );
    }
}

Polymorphic Relations (Quan hệ đa hình)

Quan hệ đa hình cho phép một model có thể thuộc về nhiều hơn 1 model khác với một sự liên kết đơn. Giả sử người dùng có thể vote cả món ăn (food), cả bài viết (post). Sử dụng mối quan hệ đa hình bạn có thể chỉ cần sử dụng duy nhất 1 bảng vote cho cả 2 quan hệ trên.

 foods
    id - integer
    name - string
    description - text

posts
    id - integer
    title - string
    url - string

votes
    id - integer
    body - text
    voteable_id - integer
    voteable_type - string

Để ý thấy column voteable_id sẽ lưu giữ giá trị id của food hoặc post còn column voteable_type lưu giữ tên lớp của model sở hữu (dùng để xác định kiểu của model sở hữu trả về khi truy cập vào quan hệ voteable). Ta định nghĩa quan hệ trong các model như sau:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Vote extends Model
{
    /**
     * Get all of the owning voteable models.
     */
    public function voteable()
    {
        return $this->morphTo();
    }
}

class Food extends Model
{
    /**
     * Get all of the food's votes.
     */
    public function votes()
    {
        return $this->morphMany('App\Vote', 'voteable');
    }
}

class Post extends Model
{
    /**
     * Get all of the post's votes.
     */
    public function votes()
    {
        return $this->morphMany('App\Vote', 'voteable');
    }
}

Để thực hiện truy vấn tất cả các votes của 1 món ăn (food), chúng ta chỉ cần sử dụng Eloquent dynamic properties:

$food = App\Food::find(1);

foreach ($food->votes as $vote) {
    //
}

Bạn cũng có thể lấy dữ liệu từ model Vote bằng cách truy cập tên của phương thức gọi tới morphTo là phương thức voteable:

$vote = App\Vote::find(1);

$voteable = $vote->voteable;

Quan hệ voteable trên model Vote sẽ trả về 1 instance hoặc là Post hoặc Food, dựa trên kiểu của model sở hữu vote. Như đã nói ở trên, voteable_type ở trên sẽ là App\Post hoặc App\Food. Nhưng nếu bạn muốn tách DB từ cấu trúc bên trong của ứng dụng thì bạn có thể định nghĩa một quan hệ morph map (đăng kí trong hàm boot của AppServiceProvider để thông báo cho Eloquent sử dụng tên bảng liên quan với mỗi model thay vì sử dụng tên class đầy đủ:

use Illuminate\Database\Eloquent\Relations\Relation;

Relation::morphMap([
    'posts' => App\Post::class,
    'foods' => App\Food::class,
]);

Lúc này column voteable_type sẽ lưu 2 giá trị 1 là posts, 2 là foods.

Many To Many Polymorphic Relations (Quan hệ đa hình nhiều - nhiều)

Hãy xem cấu trúc bảng cho mối quan hệ này cho dễ hình dung nhé:

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

Ví dụ ở trên là 1 blog model Post và Video có thể chia sẻ 1 liên kết đa hình tới model Tag, sử dụng quan hệ nhiều - nhiều cho phép bạn lấy một danh sách các tag không trùng lặp mà được chia sẻ qua các post và video. Ta sẽ định nghĩa các mối quan hệ trên model PostVideo sẽ đều có một phương thức tags gọi tới phương thức morphToMany trên lớp Eloquent:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    /**
     * Get all of the tags for the post.
     */
    public function tags()
    {
        return $this->morphToMany('App\Tag', 'taggable');
    }
}

Sau đó trên model Tag cũng sẽ định nghĩa 1 phương thức postsvideos:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Tag extends Model
{
    /**
     * Get all of the posts that are assigned this tag.
     */
    public function posts()
    {
        return $this->morphedByMany('App\Post', 'taggable');
    }

    /**
     * Get all of the videos that are assigned this tag.
     */
    public function videos()
    {
        return $this->morphedByMany('App\Video', 'taggable');
    }
}

Khi đã định nghĩa xong, chúng ta sẽ tìm cách để lấy các dữ liệu quan hệ. Đầu tiên là lấy toàn bộ các tag cho một post, bạn đơn giản chỉ cần sử dụng Eloquent dynamic properties tags:

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

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

Bạn cũng có thể lấy chủ thể của một quan hệ đa hình từ model đa hình bằng cách truy cập tên của phương thức mà thực hiện gọi tới morphedByMany. Trong trường hợp của chúng ta đó là phương thức posts hoặc videos trên model Tag. Vì vậy bạn sẽ sử dụng những phương thức này như là các Eloquent dynamic properties:

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

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

Vậy là xong các mối quan hệ trong Eloquent. Bài này mình gần như chỉ dịch tài liệu tiếng anh trên Docs còn bài sau mình sẽ giới thiệu kĩ về Querying Relations(Các quan hệ truy vấn) đến các bạn. Cảm ơn các bạn vì đã đọc đến đây! (lay2)

Tham khảo

Eloquent-relationships: https://laravel.com/docs/5.3/eloquent-relationships