Relationships trong Laravel


Khi học về csdl (mysql hay sql server) thì hầu hết chúng ta cũng biết về mối quan hệ. Thì trong Laravel cũng vậy, đề làm việc giữa các model ta cũng phải khai báo những quan hệ trong đó. Cùng mình bắt tay tìm hiểu ngay bây giờ nào.

Giới thiệu

Khi học về cơ sở dữ liệu có những mối quan hệ nào thì Laravel cũng hỗ trợ hầu hết các mối quan hệ đó. Nó giúp chúng ta dễ dàng thao tác dữ liệu trên nhiều bảng vô cùng dễ dàng và nhanh chóng.

Defining Relationships

One to One (1-1)

Đây là kiểu quan hệ đơn giản nhất, ta hiểu rằng 1 cái này chỉ phụ thuộc vào một cái khác và ngược lại. Ví dụ, ta có bảng UsersAvatar, ở đây có nghĩa một user chỉ có đúng một avatar và avatar này chỉ đại diện cho user đó. Để biểu diễn mối quan hệ này trong models ta sử dụng method hasOne:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

use App\Models\Avatar;

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

        // or
        return $this->hasOne(Avatar::class);

        // or
        return $this->hasOne(Avatar::class, 'user_id', 'id');
    }
}

Ở 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 bảng đó. Có 2 cách truyền vào, 1 là truyền namespace của model đó vào, 2 là truyền trực tiếp class của model đó vào. Tham số thứ 2 và thứ 3 lần lượt là khóa ngoại và khóa chính.

Đi sâu vào một chút xíu, vì sao cách khai báo đầu tiên và thứ 2 được sử dụng mặc dù không truyền khóa ngoại và khóa chính vào. Nó chỉ hoạt động được khi khóa ngoại là user_id trong bảng Avatar và khóa chính là id trong bảng Users.
Mặc định nó sẽ lấy tên class ghép với khóa chỉnh của model làm khóa ngoại, và lấy primaryKey làm khóa chính .

public function hasOne($related, $foreignKey = null, $localKey = null)
{
    $instance = $this->newRelatedInstance($related);

    $foreignKey = $foreignKey ?: $this->getForeignKey();

    $localKey = $localKey ?: $this->getKeyName();

    return $this->newHasOne($instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey);
}

public function getForeignKey()
{
    return Str::snake(class_basename($this)).'_'.$this->getKeyName();
}

public function getKeyName()
{
    return $this->primaryKey;
}

Sau khi khai báo, để lấy được avatar của 1 user nào đó, đơn giản chỉ làm như sau:

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

Inverse
Tương tự khai báo phương thức user trong model Avatar:

<?php

namespace App;

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

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

  public function user()
  {
    return $this->belongsTo(User::class);
    // or
    return $this->belongsTo(User::class, 'user_id', 'id');
  }
}

One to Many (1-n)

Mối quan hệ này để biểu thị mối quan hệ cha-con. Tức là một cha có nhiều con nhưng một con chỉ có một cha (tính ca ruột thôi nha).
Ví dụ dưới đâu nói về mối quan hệ giữa users có nhiều bài posts:

<?php

namespace App;

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

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

Tương tự như quan hệ One-One, để lấy những bài biết của user ta thử như sau:

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

Trả về 1 collection của Post

và ngược lại trên model Post:

namespace App;

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

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

Many to Many (n-n)

Quan hệ này thì nó phức tạp hơn so với hai quan hệ trước đó. Khi học csdl thì ta đã hiểu quan hệ nhiều nhiều thì gồm 3 bảng. Thì đây cũng vậy. Ví dụ một product sẽ thuộc nhiều orders và một order lại cho nhiều products. Để biểu diễn được quan hệ này thì chúng ta phải nhờ sự trợ giúp của một bảng thứ 3, bảng trung gian giúp tạo quan hệ cho 2 bảng kia, mình đặt tên là order_product và đồng thời chưa 2 cột order_idproduct_id và chúng ta không cần tạo model cho bảng trung gian này. Theo dõi ví dụ bên dưới:

namespace App;

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

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

Muốn lấy ra được một product có bao nhiêu order chỉ cần:

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

Tương tự như One-One, belongsToMany cũng đã tạo những tham số mặc định, 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 là order_product. Tuy nhiên đời đâu như là mơ, đấu phải lúc nào cx được đặt tên bảng theo ý mình thích được, giả dụ là product_order thì cần phải truyền tham số thứ 2 vào:

return $this->belongsToMany(Order::class, 'product_order');

Đi sâu vào thì Laravel cũng đã định nghĩa những giá trị mặc định nếu chúng ta không truyền các tham số vào.

/**
 * Define a many-to-many relationship.
 *
 * @param  string  $related
 * @param  string|null  $table
 * @param  string|null  $foreignPivotKey
 * @param  string|null  $relatedPivotKey
 * @param  string|null  $parentKey
 * @param  string|null  $relatedKey
 * @param  string|null  $relation
 * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
 */
public function belongsToMany($related, $table = null, $foreignPivotKey = null, $relatedPivotKey = null,
                                $parentKey = null, $relatedKey = null, $relation = null)
{
    ...
}

Diễn tả một xíu về các tham số truyền vào:

  • $related: là class mà mình muốn tạo quan hệ.
  • $table: là bảng trung gian.
  • $foreignPivotKey: là khóa ngoại của bảng đang định nghĩa quan hệ (product_id trên bảng order_product).
  • $relatedPivotKey: là khóa ngoại của bảng mà chúng ta tạo quan hệ (order_id trên bảng order_product).
  • $parentKey: là khóa chính của bảng đang định nghĩa quan hệ (mặc định là id).
  • $relatedKey: là khóa chính của bảng quan hệ (mặc định là id).
  • $relation: là tên của quan hệ (không quan trọng).

Ngược lại, đối với model Order ta định nghĩa tương tự như model Product:

namespace App;

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

class Order extends Model
{
    public function products()
    {
        return $this->belongsToMany(Product::class);
    }
}

Lấy giá trị 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 là 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(Product::class)->withPivot('address');

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

return $this->belongsToMany(Product::class)->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(Product::class)
                ->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.

Insert & Update

Ở các mối quan hệ phía trên, đơn giản chỉ cần lưu id của quan hệ mình muốn tạo vào trong model là xong, nhưng ở quan hệ n-n thì có đôi chút khác.
Vì bảng trung gian không có model để quản lý nên ta sẽ quản lý nó thông qua model mà chúng ta cần handle.
Ví dụ một User có nhiều quyền, và chúng ta thay đổi nó. Thì đầu tiên chúng ta cần có userrole sẵn thì mới có thể tạo quan hệ được.
Để thêm quyền cho các user chúng ta sử dụng attach

use App\Models\User;

$user = User::find(1);

$user->roles()->attach($roleId);

Bảng trung gian có những cột giá trị khác, thì khi thêm vào có thể kèm theo:

$user->roles()->attach($roleId, ['expires' => $expires]);

Ngược lại, để xóa một role ra khỏi user

// Detach a single role from the user...
$user->roles()->detach($roleId);

// Detach all roles from the user...
$user->roles()->detach();

Ngoài ra chúng ra có thể thêm nhiều role vào 1 lần:

$user = User::find(1);

$user->roles()->detach([1, 2, 3]);

$user->roles()->attach([1, 2, 3]);

$user->roles()->attach([
    1 => ['expires' => $expires],
    2 => ['expires' => $expires],
]);

Sync
Một số trường hợp chúng ta muốn cập nhật lại toàn bộ role của thằng user thì ta sử dụng phương thức sync

$user->roles()->sync([1, 2, 3]);

$user->roles()->sync([1 => ['expires' => true], 2, 3]);

Sau khi đồng bộ thì thằng user chỉ có đúng 3 role truyền vào.

Has One Of Many

Đôi khi giữa 2 bảng có quan hệ 1-n nhưng ta chỉ muốn lấy 1 recore mới nhất, thì ta khai báo như sau:

public function latestOrder()
{
    return $this->hasOne(Order::class)->latestOfMany();
}

Hoặc cũ nhất

public function latestOrder()
{
    return $this->hasOne(Order::class)->oldestOfMany();
}

hoặc lấy order nào có giá trị lớn nhất

public function latestOrder()
{
    return $this->hasOne(Order::class)->orderBy('total', 'desc')
}

Has One Through

Đây là mối quan hệ liên kết các bảng với nhau thông qua một bảng trung gian.
Giả sử ta có 3 bảng như sau:

users
    id - integer
    supplier_id - integer

suppliers
    id - integer

history
    id - integer
    user_id - integer

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

<?php

namespace App;

use App\Models\History;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;

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

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(
            History::class,
            User::class,
            '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
        );
    }
}

Ở model UserHistory chúng ta định nghĩa như bình thường

namespace App;

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

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

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

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

Has Many Through

Mối quan hệ này cung cấp cho chúng ta cách truy cập bảng liên kết dễ dàng thông qua bảng trung gian. Khác với n-n, bảng trung gian trong quan hệ many to many thì không cần khai báo model và cũng như không sử dụng nhiều.
Ví dụ 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

Chúng ta biểu diễn quan hệ như sau

<?php

namespace App;

use App\Models\User;
use App\Models\Post;
use Illuminate\Database\Eloquent\Model;

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

Tương tự như hasOneThrough, chúng ta cũng có thể thay đổi khóa chính và khóa ngoại ràng buộc

class Team extends Model
{
    public function posts()
    {
        return $this->hasManyThrough(
            Post::class,
            User::class,
            'team_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.
        );
    }
}

Polymorphic Relationships

Đâ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.

One to One

Mối quan hệ này tương tự quan hệ One to One mình đã giới thiệu phía trên, tuy nhiên mục đích của mối quan hệ này là 1 model cso 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 phải tạo một bảng để lưu ảnh của post và một bảng để lưu ảnh của product, giả sử có nhiều bảng cần đến image thì lại phải tạo nhiều bảng để lưu trữ hình ảnh. Vậy nên mối quan hệ Polymorphic được sinh ra:

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 postproduct, còn trường imageable_type sẽ lưu tên class hai model đó. Theo convention của laravel thì bảng lưu trung gian sẽ bắt buộc phải có 2 trường id và type nhưng để rõ ràng hơn thì sẽ lưu thêm tiền tố tên_bảng_bỏ_s + able_.
Trong migrate đơn giản chỉ cần khai báo như sau:

public function up()
    {
        Schema::create('image', function (Blueprint $table) {
            $table->bigIncrements('id');
            ...
            $table->morphs('imageable');
            $table->timestamps();
        });
    }

morphs sẽ tự sinh ra 2 cột như trên.
Tiếp theo cần khai báo vào model

<?php

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

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

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

Để lấy image thuộc posts thì trỏ đến image

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

$image = $post->image;

và ngược lại, nhưng mình không khuyến khích dùng như này, mất công mình phải check nó là model gì để không bị lỗi data

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

$imageable = $image->imageable;

Insert & Update

$image = new Image([
    ...
])
$post = App\Post::find(1);
$post->image()->save($image);

// or

$post = Post::find(1);
$comment = $post->image()->create([
    'name' => 'helloworld',
]);

One to Many

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(Comment::class, 'commentable');
    }
}

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

Lấy comment ra

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

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

Và ngược lại, cũng như phía trên mình cũng không khuyến khích truy vấn ngược

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

$commentable = $comment->commentable;

Insert & Update
Tương tự như one-one, việc tạo các records vô cùng đơn giản

//project message add
$project = Project::find(1);   

$message = new Message;
$message->body = "Hi nqdat.com";

$project->messages()->save($message);

//video message add
$video = Video::find(1);   

$message = new Message;
$message->body = "Hi nqdat.com";

$video->messages()->save($message);

Many to Many

Quan hệ này phức tạp hơp 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(Tag::class, 'taggable');
    }
}

//tag.php
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

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

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

Xong rồi, muốn lấy các tag của 1 post cũng làm tương tự như các mối quan hệ khác

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

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

Và ngược lại

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

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

Insert & Update

$post = Post::find(1);    
 
$tag = new Tag;
$tag->name = "nqdat.com";
 
$post->tags()->save($tag);

$video = Video::find(1);    
 
$tag = new Tag;
$tag->name = "ItSolutionStuff.com";
 
$video->tags()->save($tag);

// or 


$post->tags()->saveMany([$tag1, $tag2]);
$video->tags()->saveMany([$tag1, $tag2]);

Nếu như đã có tag sẵn, thì ta có thể sử dụng attach

$video = Video::find(1);    
 
$tag1 = Tag::find(3);
$tag2 = Tag::find(4);

$video->tags()->attach([$tag1->id, $tag2->id]);

Hoặc đồng bộ lại

$post->tags()->sync([$tag1->id, $tag2->id]);

Nghĩa là nếu không có tag1 tag2 sẽ thêm vào, cái nào khác thì sẽ bỏ ra.

Cuối cùng là detach

$post->tags()->detach([$tag1->id, $tag2->id]);

Kết luận

Qua bày này mình giới thiệu tới các bạn các chứng năng cơ bản và phổ biến sử dụng nhiều trong các ứng dụng. Có thể chưa đáp ứng nhu cầu của các bạn, bạn có thể tham khảo thêm trên trang chủ của Laravel.

Ngoài ra bạn nên tìm hiểu thêm về property hay method để có thể nắm model trong lòng bàn tay nha

Nguồn tham khảo:

Rất mong được sự ủng hộ của mọi người để mình có động lực ra những bài viết tiếp theo.
{\__/}
( ~.~ )
/ > ♥️ I LOVE YOU 3000

JUST DO IT!


Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source Nguyễn Quang Đạt !
Comments
  TOC