Giao diện
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:
- One To One
- One To Many
- Many To Many
- Has One Through
- Has Many Through
- One To One (Polymorphic)
- One To Many (Polymorphic)
- Many To Many (Polymorphic)
Đị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 hasOne và hasMany. 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 - integerCấ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ả Post và Video 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ụ: Post và User đề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 - stringphp
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 instanceOne to Many (Polymorphic)
Tương tự one-to-many nhưng model con thuộc nhiều loại model cha. Ví dụ: Post và Video đề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ụ: Post và Video 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 has và whereHas:
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();Aggregate cho Related Models
Đếm Related Models (Counting Related Models)
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());Thêm và Cập nhật Related Models
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 associate và dissociate:
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);
}
}