Skip to content

Eloquent: Relationships

Giới thiệu (Introduction)

Các bảng database thường có liên kết với nhau. Ví dụ, một bài viết blog có thể có nhiều bình luận, hoặc một đơn hàng liên kết đến người dùng đã đặt. Eloquent giúp quản lý và làm việc với các mối quan hệ này dễ dàng, hỗ trợ nhiều loại relationship phổ biến:

Định nghĩa Relationships (Defining Relationships)

Eloquent relationship được định nghĩa dưới dạng method trên model class. Vì relationship cũng là query builder, bạn có thể chuỗi (chain) thêm điều kiện:

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

One to One / Has One

Quan hệ một-một là loại cơ bản nhất. Ví dụ, một User model liên kết với một Phone model:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;

class User extends Model
{
    /**
     * Lấy điện thoại liên kết với user.
     */
    public function phone(): HasOne
    {
        return $this->hasOne(Phone::class);
    }
}

Tham số đầu tiên là tên class model liên kết. Truy cập relationship qua dynamic property:

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

Eloquent tự động xác định foreign key dựa trên tên model cha. Trong trường hợp này, model Phone được giả định có cột user_id. Để tùy chỉnh:

php
return $this->hasOne(Phone::class, 'foreign_key');

return $this->hasOne(Phone::class, 'foreign_key', 'local_key');

Định nghĩa quan hệ nghịch đảo (Defining the Inverse)

Sử dụng belongsTo để định nghĩa quan hệ nghịch đảo:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Phone extends Model
{
    /**
     * Lấy user sở hữu điện thoại.
     */
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

One to Many / Has Many

Quan hệ một-nhiều: một model cha có nhiều model con. Ví dụ, bài viết có nhiều bình luận:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Post extends Model
{
    /**
     * Lấy danh sách bình luận của bài viết.
     */
    public function comments(): HasMany
    {
        return $this->hasMany(Comment::class);
    }
}

Eloquent tự động xác định foreign key — lấy tên model cha ở dạng "snake case" + _id. Ví dụ: model Comment sẽ có cột post_id.

Truy cập comments:

php
use App\Models\Post;

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

foreach ($comments as $comment) {
    // ...
}

Thêm điều kiện cho relationship query:

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

Tùy chỉnh foreign key:

php
return $this->hasMany(Comment::class, 'foreign_key');

return $this->hasMany(Comment::class, 'foreign_key', 'local_key');

One to Many (Inverse) / Belongs To

Để cho phép model con truy cập model cha, sử dụng belongsTo:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Comment extends Model
{
    /**
     * Lấy bài viết sở hữu bình luận.
     */
    public function post(): BelongsTo
    {
        return $this->belongsTo(Post::class);
    }
}

Truy cập model cha:

php
use App\Models\Comment;

$comment = Comment::find(1);

return $comment->post->title;

Eloquent xác định foreign key bằng tên method + _id. Ví dụ: method post → cột post_id.

Tùy chỉnh:

php
return $this->belongsTo(Post::class, 'foreign_key');

return $this->belongsTo(Post::class, 'foreign_key', 'owner_key');

Model mặc định (Default Models)

Sử dụng withDefault để trả về model rỗng khi relationship không tồn tại:

php
public function user(): BelongsTo
{
    return $this->belongsTo(User::class)->withDefault();
}

// Với thuộc tính mặc định
public function user(): BelongsTo
{
    return $this->belongsTo(User::class)->withDefault([
        'name' => 'Guest Author',
    ]);
}

Has One of Many

Đôi khi một model có nhiều model liên kết nhưng bạn chỉ muốn lấy "một" bản ghi theo điều kiện (mới nhất, cũ nhất, v.v.):

php
/**
 * Lấy đơn hàng gần nhất.
 */
public function latestOrder(): HasOne
{
    return $this->hasOne(Order::class)->latestOfMany();
}

/**
 * Lấy đơn hàng cũ nhất.
 */
public function oldestOrder(): HasOne
{
    return $this->hasOne(Order::class)->oldestOfMany();
}

/**
 * Lấy đơn hàng lớn nhất.
 */
public function largestOrder(): HasOne
{
    return $this->hasOne(Order::class)->ofMany('price', 'max');
}

Has One Through

Quan hệ "has-one-through" định nghĩa mối liên kết một-một qua một model trung gian. Ví dụ, một Mechanic model có thể truy cập Car model qua trung gian Owner:

php
class Mechanic extends Model
{
    public function carOwner(): HasOneThrough
    {
        return $this->hasOneThrough(Owner::class, Car::class);
    }
}

Has Many Through

Quan hệ "has-many-through" cung cấp cách truy cập model xa thông qua model trung gian. Ví dụ, Project có nhiều Deployment thông qua Environment:

php
class Project extends Model
{
    public function deployments(): HasManyThrough
    {
        return $this->hasManyThrough(Deployment::class, Environment::class);
    }
}

Scoped Relationships

Bạn có thể dùng attribute Scope trên relationship để tự động áp dụng scope:

php
use Illuminate\Database\Eloquent\Attributes\Scope;
use App\Models\Scopes\ActiveScope;

#[Scope(ActiveScope::class)]
public function activeComments(): HasMany
{
    return $this->hasMany(Comment::class);
}

Quan hệ Many to Many

Quan hệ nhiều-nhiều phức tạp hơn hasOnehasMany. Ví dụ: user có nhiều role, mỗi role cũng thuộc nhiều user.

Cấu trúc bảng (Table Structure)

Cần 3 bảng: users, roles, và bảng trung gian role_user (theo thứ tự bảng chữ cái):

users
    id - integer
    name - string

roles
    id - integer
    name - string

role_user
    user_id - integer
    role_id - integer

Cấu trúc Model (Model Structure)

Sử dụng belongsToMany:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class User extends Model
{
    /**
     * Các role thuộc về user.
     */
    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class);
    }
}

Truy cập roles:

php
use App\Models\User;

$user = User::find(1);

foreach ($user->roles as $role) {
    // ...
}

Tùy chỉnh tên bảng trung gian và foreign keys:

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

Định nghĩa quan hệ nghịch đảo

php
class Role extends Model
{
    public function users(): BelongsToMany
    {
        return $this->belongsToMany(User::class);
    }
}

Truy cập cột bảng trung gian (Retrieving Intermediate Table Columns)

Sử dụng pivot attribute:

php
$user = User::find(1);

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

Chỉ định cột trung gian cần lấy:

php
return $this->belongsToMany(Role::class)->withPivot('active', 'created_by');

Thêm timestamps tự động:

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

Lọc qua cột bảng trung gian (Filtering Queries via Intermediate Table Columns)

php
return $this->belongsToMany(Role::class)
    ->wherePivot('approved', 1);

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

return $this->belongsToMany(Role::class)
    ->wherePivotNotIn('priority', [1, 2]);

return $this->belongsToMany(Podcast::class)
    ->wherePivotBetween('created_at', ['2020-01-01', '2020-12-31']);

return $this->belongsToMany(Podcast::class)
    ->wherePivotNull('expired_at');

Định nghĩa Model bảng trung gian tùy chỉnh

php
use App\Models\RoleUser;

return $this->belongsToMany(Role::class)->using(RoleUser::class);

Quan hệ Polymorphic

Quan hệ polymorphic cho phép model con thuộc về nhiều loại model cha khác nhau thông qua một liên kết duy nhất. Ví dụ, Comment model có thể thuộc cả PostVideo model.

One to One (Polymorphic)

Tương tự one-to-one nhưng model con có thể thuộc nhiều loại model cha. Ví dụ: PostUser đều có thể có một Image.

Cấu trúc bảng:

posts
    id - integer
    name - string

users
    id - integer
    name - string

images
    id - integer
    url - string
    imageable_id - integer
    imageable_type - string
php
class Image extends Model
{
    public function imageable(): MorphTo
    {
        return $this->morphTo();
    }
}

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

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

Truy cập:

php
$post = Post::find(1);
$image = $post->image;

// Từ Image, lấy model cha
$imageable = $image->imageable; // Trả về Post hoặc User instance

One to Many (Polymorphic)

Tương tự one-to-many nhưng model con thuộc nhiều loại model cha. Ví dụ: PostVideo đều có nhiều Comment:

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

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

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

One of Many (Polymorphic)

php
public function latestComment(): MorphOne
{
    return $this->morphOne(Comment::class, 'commentable')->latestOfMany();
}

Many to Many (Polymorphic)

Ví dụ: PostVideo chia sẻ quan hệ nhiều-nhiều polymorphic với Tag:

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

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

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

Custom Polymorphic Types

Thay vì lưu tên class đầy đủ, bạn có thể dùng "morph map":

php
use Illuminate\Database\Eloquent\Relations\Relation;

Relation::enforceMorphMap([
    'post' => 'App\Models\Post',
    'video' => 'App\Models\Video',
]);

Truy vấn Relationships (Querying Relations)

Relationship Methods vs. Dynamic Properties

  • Method (->posts()) — trả về query builder, cho phép thêm điều kiện
  • Dynamic Property (->posts) — trả về kết quả đã load (lazy loading)
php
// Method - có thể thêm điều kiện
$user->posts()->where('active', 1)->get();

// Dynamic property - trả về tất cả posts
$user->posts;

Kiểm tra sự tồn tại Relationship (Querying Relationship Existence)

Sử dụng haswhereHas:

php
// Lấy tất cả post có ít nhất một comment
$posts = Post::has('comments')->get();

// Lấy post có >= 3 comments
$posts = Post::has('comments', '>=', 3)->get();

// Lấy post có comment chứa từ "code"
$posts = Post::whereHas('comments', function (Builder $query) {
    $query->where('content', 'like', 'code%');
})->get();

Kiểm tra không tồn tại Relationship (Querying Relationship Absence)

php
$posts = Post::doesntHave('comments')->get();

$posts = Post::whereDoesntHave('comments', function (Builder $query) {
    $query->where('content', 'like', 'code%');
})->get();
php
$posts = Post::withCount('comments')->get();

foreach ($posts as $post) {
    echo $post->comments_count;
}

Thêm điều kiện:

php
$posts = Post::withCount(['votes', 'comments' => function (Builder $query) {
    $query->where('content', 'like', 'code%');
}])->get();

echo $posts[0]->votes_count;
echo $posts[0]->comments_count;

Các hàm Aggregate khác

php
$posts = Post::withSum('comments', 'votes')->get();
$posts = Post::withAvg('comments', 'votes')->get();
$posts = Post::withMin('comments', 'votes')->get();
$posts = Post::withMax('comments', 'votes')->get();

Eager Loading

Khi truy cập Eloquent relationship qua property, related model được "lazy load" — chỉ load khi truy cập. Điều này gây vấn đề "N + 1" query:

php
// 1 query cho books + N query cho mỗi author
$books = Book::all();

foreach ($books as $book) {
    echo $book->author->name; // Mỗi vòng lặp = 1 query
}

Sử dụng eager loading với with để chỉ cần 2 query:

php
$books = Book::with('author')->get();

foreach ($books as $book) {
    echo $book->author->name; // Không cần query thêm
}
sql
select * from books

select * from authors where id in (1, 2, 3, 4, 5, ...)

Eager Load nhiều Relationships

php
$books = Book::with(['author', 'publisher'])->get();

Nested Eager Loading

php
$books = Book::with('author.contacts')->get();

Ràng buộc Eager Loads (Constraining Eager Loads)

php
$users = User::with(['posts' => function (Builder $query) {
    $query->where('title', 'like', '%code%');
}])->get();

Lazy Eager Loading

Eager load sau khi đã truy vấn model cha:

php
$books = Book::all();

if ($someCondition) {
    $books->load('author', 'publisher');
}

// Với điều kiện
$author->load(['books' => function (Builder $query) {
    $query->orderBy('published_date', 'asc');
}]);

Ngăn Lazy Loading (Preventing Lazy Loading)

php
use Illuminate\Database\Eloquent\Model;

Model::preventLazyLoading(! $this->app->isProduction());

Method save

php
use App\Models\Comment;

$comment = new Comment(['message' => 'A new comment.']);

$post = Post::find(1);

$post->comments()->save($comment);

Lưu nhiều model cùng lúc:

php
$post->comments()->saveMany([
    new Comment(['message' => 'A new comment.']),
    new Comment(['message' => 'Another new comment.']),
]);

Method create

php
$post = Post::find(1);

$comment = $post->comments()->create([
    'message' => 'A new comment.',
]);

Belongs To Relationships

Sử dụng associatedissociate:

php
$account = Account::find(10);

$user->account()->associate($account);
$user->save();

// Gỡ liên kết
$user->account()->dissociate();
$user->save();

Many to Many Relationships

Attach / Detach

php
$user = User::find(1);

// Gắn role vào user
$user->roles()->attach($roleId);

// Gắn với dữ liệu pivot bổ sung
$user->roles()->attach($roleId, ['expires' => $expires]);

// Gỡ role
$user->roles()->detach($roleId);

// Gỡ tất cả roles
$user->roles()->detach();

Sync

php
// Chỉ giữ lại các role trong mảng
$user->roles()->sync([1, 2, 3]);

// Sync với dữ liệu pivot
$user->roles()->sync([1 => ['expires' => true], 2, 3]);

Toggle

php
// Gắn nếu chưa có, gỡ nếu đã có
$user->roles()->toggle([1, 2, 3]);

Cập nhật Timestamp cha (Touching Parent Timestamps)

Khi model con được cập nhật, tự động "touch" updated_at của model cha:

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Comment extends Model
{
    /**
     * Relationships cần touch.
     *
     * @var array
     */
    protected $touches = ['post'];

    /**
     * Lấy post sở hữu comment.
     */
    public function post(): BelongsTo
    {
        return $this->belongsTo(Post::class);
    }
}