Eloquent ORM: các pattern và tối ưu hoá cho Laravel
Làm chủ Eloquent ORM với các pattern nâng cao và kỹ thuật tối ưu hoá. Eager loading, query scope, accessor, mutator và hiệu năng cho ứng dụng Laravel.

Eloquent ORM biến các tương tác với cơ sở dữ liệu thành những thao tác trôi chảy và biểu cảm. Ngoài cú pháp tinh tế, việc làm chủ các pattern nâng cao và kỹ thuật tối ưu hoá quyết định hiệu năng của ứng dụng Laravel khi vận hành.
Vấn đề N+1 là nguyên nhân chính gây chậm trong các ứng dụng Eloquent. Mỗi quan hệ không được tối ưu sẽ tạo thêm một truy vấn SQL trên mỗi bản ghi.
Giải quyết vấn đề N+1 bằng eager loading
Vấn đề N+1 xuất hiện khi mỗi lần lặp qua một collection lại kích hoạt thêm một truy vấn để tải các quan hệ. Với 100 bài viết và tác giả của chúng, đó là 101 truy vấn thay vì một truy vấn duy nhất đã được tối ưu.
Eager loading lấy tất cả các quan hệ trong tối đa một hoặc hai truy vấn, giúp giảm đáng kể thời gian phản hồi.
// Demonstration of N+1 problem and its solution
namespace App\Http\Controllers;
use App\Models\Article;
use Illuminate\Http\Request;
class ArticleController extends Controller
{
// ❌ N+1 problem: 1 articles query + N author queries
public function indexWithProblem()
{
$articles = Article::all(); // 1 query
foreach ($articles as $article) {
echo $article->author->name; // N additional queries
}
}
// ✅ Eager loading: 2 queries maximum
public function indexOptimized()
{
$articles = Article::with('author')->get(); // 2 queries total
foreach ($articles as $article) {
echo $article->author->name; // No additional queries
}
}
// ✅ Nested eager loading for multiple relationships
public function indexWithNestedRelations()
{
// Loads articles → authors → profiles + articles → comments → users
$articles = Article::with([
'author.profile',
'comments.user'
])->get();
return view('articles.index', compact('articles'));
}
}Eager loading với with() dự đoán nhu cầu và tải dữ liệu trước. Khoảng cách hiệu năng trở nên rất ấn tượng trên các collection lớn.
Eager loading có điều kiện và bị giới hạn
Những quan hệ lớn đôi khi cần được tải một phần. Các ràng buộc lên eager loading giới hạn dữ liệu được lấy về đồng thời tránh được N+1.
// Eager loading with constraints to optimize queries
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
class UserController extends Controller
{
public function showWithRecentOrders(int $id)
{
// Load only the 5 most recent paid orders
$user = User::with(['orders' => function (Builder $query) {
$query->where('status', 'paid')
->orderByDesc('created_at')
->limit(5);
}])->findOrFail($id);
return view('users.show', compact('user'));
}
public function indexActiveWithStats()
{
// Conditional eager loading with withCount
$users = User::query()
->where('active', true)
->with(['profile', 'subscription'])
->withCount(['orders', 'reviews']) // Adds orders_count and reviews_count
->withSum('orders', 'total') // Adds orders_sum_total
->get();
return view('users.index', compact('users'));
}
public function showWithConditionalRelation(int $id)
{
// Load relationship only if user is premium
$user = User::findOrFail($id);
$user->loadMissing(
$user->isPremium() ? ['premiumFeatures', 'analytics'] : []
);
return view('users.show', compact('user'));
}
}Các phương thức withCount() và withSum() thêm các phép tổng hợp mà không cần tải toàn bộ collection, lý tưởng cho thống kê trên dashboard.
Query scope cho các truy vấn tái sử dụng
Query scope đóng gói logic lọc bên trong model. Scope cục bộ mang lại sự linh hoạt, trong khi scope toàn cục được áp dụng tự động cho mọi truy vấn.
Scope cục bộ sử dụng tiền tố scope trong model nhưng được gọi mà không cần tiền tố đó: scopeActive() trở thành User::active().
// Local and global scopes to encapsulate business logic
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;
#[ScopedBy([PublishedScope::class])] // Global scope via PHP 8 attribute
class Article extends Model
{
// Simple local scope: Article::published()
public function scopePublished(Builder $query): Builder
{
return $query->whereNotNull('published_at')
->where('published_at', '<=', now());
}
// Local scope with parameter: Article::byCategory('tech')
public function scopeByCategory(Builder $query, string $category): Builder
{
return $query->where('category', $category);
}
// Local scope with optional parameter
public function scopePopular(Builder $query, int $minViews = 1000): Builder
{
return $query->where('views_count', '>=', $minViews)
->orderByDesc('views_count');
}
// Dynamic scope for flexible search
public function scopeSearch(Builder $query, ?string $term): Builder
{
if (empty($term)) {
return $query;
}
return $query->where(function (Builder $q) use ($term) {
$q->where('title', 'like', "%{$term}%")
->orWhere('content', 'like', "%{$term}%")
->orWhereHas('tags', fn($t) => $t->where('name', 'like', "%{$term}%"));
});
}
}// Reusable global scope across models
namespace App\Models\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class PublishedScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
// Automatically applied to all Article queries
$builder->whereNotNull('published_at')
->where('published_at', '<=', now());
}
}// Using scopes in a controller
$articles = Article::query()
->byCategory('technology')
->popular(500)
->search($request->input('q'))
->with('author')
->paginate(20);
// Disable a global scope temporarily
$allArticles = Article::withoutGlobalScope(PublishedScope::class)->get();Việc nối tiếp các scope tạo ra những truy vấn dễ đọc và dễ bảo trì, đồng thời tập trung logic nghiệp vụ trong model.
Sẵn sàng chinh phục phỏng vấn Laravel?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
Accessor và mutator với Attribute
Laravel 9+ giới thiệu một cú pháp thống nhất cho accessor và mutator thông qua lớp Attribute. Cách tiếp cận hiện đại này thay thế các phương thức get*Attribute và set*Attribute.
// Modern accessors and mutators with the Attribute class
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class User extends Model
{
protected $fillable = ['first_name', 'last_name', 'email', 'password'];
// Accessor: generates a virtual attribute from other fields
protected function fullName(): Attribute
{
return Attribute::make(
get: fn () => "{$this->first_name} {$this->last_name}",
);
}
// Mutator: transforms value before saving
protected function password(): Attribute
{
return Attribute::make(
set: fn (string $value) => Hash::make($value),
);
}
// Combined accessor + mutator
protected function email(): Attribute
{
return Attribute::make(
get: fn (string $value) => Str::lower($value),
set: fn (string $value) => Str::lower(trim($value)),
);
}
// Cached accessor to avoid recalculations
protected function initials(): Attribute
{
return Attribute::make(
get: fn () => Str::upper(
Str::substr($this->first_name, 0, 1) .
Str::substr($this->last_name, 0, 1)
),
)->shouldCache(); // Caches result during request
}
// Computed attribute based on a relationship
protected function ordersTotal(): Attribute
{
return Attribute::make(
get: fn () => $this->orders->sum('total'),
);
}
}// Transparent usage of accessors and mutators
$user = new User();
$user->first_name = 'John';
$user->last_name = 'Doe';
$user->email = ' JOHN@EXAMPLE.COM '; // Automatically normalized
$user->password = 'secret123'; // Automatically hashed
$user->save();
echo $user->full_name; // "John Doe"
echo $user->initials; // "JD"
echo $user->email; // "john@example.com"Phương thức shouldCache() tối ưu hoá các accessor tốn kém bằng cách tránh tính lại nhiều lần trên cùng một model.
Cast tuỳ chỉnh cho các kiểu phức tạp
Cast tự động chuyển đổi giá trị giữa PHP và cơ sở dữ liệu. Cast tuỳ chỉnh đóng gói logic serialize cho các kiểu nghiệp vụ.
// Custom cast for handling monetary amounts
namespace App\Casts;
use App\ValueObjects\Money;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
use InvalidArgumentException;
class MoneyCast implements CastsAttributes
{
public function __construct(
protected string $currency = 'USD'
) {}
// DB → PHP conversion: cents to Money object
public function get(Model $model, string $key, mixed $value, array $attributes): ?Money
{
if ($value === null) {
return null;
}
return new Money(
amount: (int) $value,
currency: $this->currency
);
}
// PHP → DB conversion: Money object to cents
public function set(Model $model, string $key, mixed $value, array $attributes): ?int
{
if ($value === null) {
return null;
}
if ($value instanceof Money) {
return $value->getAmountInCents();
}
if (is_numeric($value)) {
return (int) ($value * 100);
}
throw new InvalidArgumentException('Value must be Money instance or numeric');
}
}// Immutable Value Object representing amounts
namespace App\ValueObjects;
use JsonSerializable;
final readonly class Money implements JsonSerializable
{
public function __construct(
private int $amount, // Stored in cents
private string $currency
) {}
public function getAmountInCents(): int
{
return $this->amount;
}
public function getAmountInUnits(): float
{
return $this->amount / 100;
}
public function format(): string
{
return number_format($this->getAmountInUnits(), 2) . ' ' . $this->currency;
}
public function add(Money $other): self
{
return new self($this->amount + $other->amount, $this->currency);
}
public function jsonSerialize(): array
{
return [
'amount' => $this->getAmountInUnits(),
'currency' => $this->currency,
];
}
}// Using the custom cast
namespace App\Models;
use App\Casts\MoneyCast;
use Illuminate\Database\Eloquent\Model;
class Order extends Model
{
protected function casts(): array
{
return [
'total' => MoneyCast::class, // USD by default
'shipping_cost' => MoneyCast::class . ':USD',
'tax_amount' => MoneyCast::class . ':USD',
'paid_at' => 'datetime',
'metadata' => 'array',
];
}
}// Natural usage with the cast
$order = Order::find(1);
echo $order->total->format(); // "149.99 USD"
echo $order->total->getAmountInCents(); // 14999
$order->total = 199.99; // Automatically converted
$order->save(); // Stored as 19999 in DBValue Object kết hợp với cast tuỳ chỉnh đảm bảo tính toàn vẹn của dữ liệu nghiệp vụ trong khi vẫn duy trì một API thanh lịch.
Tối ưu các truy vấn quy mô lớn
Các thao tác trên hàng triệu bản ghi đòi hỏi những kỹ thuật cụ thể để tránh cạn kiệt bộ nhớ. Chunking và cursor xử lý dữ liệu theo lô.
Model::all() tải toàn bộ bản ghi vào bộ nhớ. Với một bảng 100.000 dòng, điều này có thể tiêu tốn vài gigabyte RAM và làm sập ứng dụng.
// Batch processing techniques for large tables
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class ProcessUsersCommand extends Command
{
protected $signature = 'users:process';
public function handle(): int
{
// ✅ Chunk: processes in batches of 1000, reloads from DB
User::query()
->where('needs_processing', true)
->chunk(1000, function ($users) {
foreach ($users as $user) {
$user->processAccount();
}
});
// ✅ Chunk with updates: avoids infinite loop during modifications
User::query()
->where('status', 'pending')
->chunkById(1000, function ($users) {
foreach ($users as $user) {
$user->update(['status' => 'processed']);
}
});
// ✅ Lazy collection: single record in memory at a time
foreach (User::lazy(1000) as $user) {
$user->sendNewsletter();
}
// ✅ Cursor: for read-only operations, minimal memory
foreach (User::cursor() as $user) {
$this->info("Processing: {$user->email}");
}
// ✅ Mass update without Eloquent: maximum performance
User::query()
->where('last_login_at', '<', now()->subYear())
->update(['status' => 'inactive']);
// ✅ Optimized mass deletion
User::query()
->where('deleted_at', '<', now()->subMonths(6))
->forceDelete();
return self::SUCCESS;
}
}Việc lựa chọn giữa chunk(), lazy() và cursor() phụ thuộc vào trường hợp sử dụng: chunk() cho việc thay đổi, lazy() cho các thao tác trung gian, và cursor() cho việc chỉ đọc với bộ nhớ tối thiểu.
Quan hệ polymorphic nâng cao
Quan hệ polymorphic cho phép một model thuộc về nhiều kiểu model khác nhau thông qua một quan hệ duy nhất. Sự linh hoạt này lý tưởng cho bình luận, thẻ hoặc tệp đính kèm.
// Model with inverse polymorphic relationship
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Comment extends Model
{
protected $fillable = ['body', 'user_id'];
// A comment can belong to Article, Video, or any other model
public function commentable(): MorphTo
{
return $this->morphTo();
}
public function user()
{
return $this->belongsTo(User::class);
}
}// Parent model with polymorphic relationship
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Article extends Model
{
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}// Another parent model using the same relationship
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Video extends Model
{
public function comments(): MorphMany
{
return $this->morphMany(Comment::class, 'commentable');
}
}// Migration for polymorphic comments table
// database/migrations/2026_01_15_create_comments_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->text('body');
$table->foreignId('user_id')->constrained();
$table->morphs('commentable'); // Creates commentable_type and commentable_id
$table->timestamps();
// Composite index for polymorphic queries
$table->index(['commentable_type', 'commentable_id']);
});
}
};// Using polymorphic relationships
$article = Article::find(1);
$article->comments()->create([
'body' => 'Excellent article!',
'user_id' => auth()->id(),
]);
$video = Video::find(1);
$video->comments()->create([
'body' => 'Very instructive video',
'user_id' => auth()->id(),
]);
// Retrieve parent from comment
$comment = Comment::with('commentable')->find(1);
echo get_class($comment->commentable); // App\Models\Article or App\Models\VideoQuan hệ polymorphic tránh lặp lại bảng và tập trung logic cho các tính năng dùng chung.
Sẵn sàng chinh phục phỏng vấn Laravel?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
Trait và observer cho logic tái sử dụng
Trait đóng gói các hành vi có thể tái sử dụng giữa các model. Observer tập trung các hook trên các sự kiện vòng đời.
// Trait for automatic slug generation
namespace App\Models\Concerns;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
trait HasSlug
{
public static function bootHasSlug(): void
{
static::creating(function (Model $model) {
if (empty($model->slug)) {
$model->slug = $model->generateUniqueSlug();
}
});
}
protected function generateUniqueSlug(): string
{
$slug = Str::slug($this->getSlugSource());
$originalSlug = $slug;
$counter = 1;
// Check uniqueness and add suffix if needed
while (static::where('slug', $slug)->exists()) {
$slug = "{$originalSlug}-{$counter}";
$counter++;
}
return $slug;
}
// Can be overridden in the model
protected function getSlugSource(): string
{
return $this->title ?? $this->name;
}
}// Trait for using UUIDs as primary key
namespace App\Models\Concerns;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
trait HasUuid
{
public static function bootHasUuid(): void
{
static::creating(function (Model $model) {
if (empty($model->{$model->getKeyName()})) {
$model->{$model->getKeyName()} = (string) Str::uuid();
}
});
}
public function getIncrementing(): bool
{
return false;
}
public function getKeyType(): string
{
return 'string';
}
}// Observer to centralize hooks on Article model
namespace App\Observers;
use App\Models\Article;
use App\Jobs\NotifySubscribersJob;
use App\Services\SearchIndexService;
use Illuminate\Support\Facades\Cache;
class ArticleObserver
{
public function __construct(
private SearchIndexService $searchIndex
) {}
public function created(Article $article): void
{
// Invalidate recent articles cache
Cache::tags(['articles', 'recent'])->flush();
// Index for search
$this->searchIndex->index($article);
}
public function updated(Article $article): void
{
// Update search index
$this->searchIndex->update($article);
// Notify subscribers if article was just published
if ($article->wasChanged('published_at') && $article->published_at !== null) {
NotifySubscribersJob::dispatch($article);
}
Cache::tags(['articles'])->flush();
}
public function deleted(Article $article): void
{
$this->searchIndex->remove($article);
Cache::tags(['articles'])->flush();
}
// Prevent deletion if article has comments
public function deleting(Article $article): bool
{
if ($article->comments()->exists()) {
return false; // Cancel deletion
}
return true;
}
}// Model using traits and observer
namespace App\Models;
use App\Models\Concerns\HasSlug;
use App\Models\Concerns\HasUuid;
use App\Observers\ArticleObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Model;
#[ObservedBy(ArticleObserver::class)]
class Article extends Model
{
use HasSlug, HasUuid;
protected $fillable = ['title', 'content', 'published_at'];
}Các phương thức boot* của trait được thực thi tự động trong quá trình khởi tạo model, mang lại sự tích hợp trong suốt.
Kết luận
Làm chủ Eloquent ORM dựa trên việc hiểu các cơ chế nền tảng và áp dụng các pattern phù hợp. Các kỹ thuật được trình bày biến những truy vấn ngây thơ thành mã nguồn hiệu năng cao và dễ bảo trì.
Danh sách kiểm tra tối ưu Eloquent:
✅ Sử dụng with() một cách hệ thống cho các quan hệ được hiển thị
✅ Áp dụng withCount() thay vì tải các collection chỉ để đếm
✅ Đóng gói logic lọc trong các query scope
✅ Ưu tiên accessor thay vì các tính toán lặp lại trong view
✅ Triển khai cast tuỳ chỉnh cho các Value Object nghiệp vụ
✅ Sử dụng chunk() hoặc lazy() cho các thao tác trên bảng lớn
✅ Tập trung các side effect trong observer
✅ Trích xuất các hành vi chung vào trait
Các công cụ php artisan telescope hoặc laravel-debugbar cho phép trực quan hoá các truy vấn SQL được sinh ra và xác định những tối ưu còn thiếu.
Bắt đầu luyện tập!
Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.
Thẻ
Chia sẻ
Bài viết liên quan

Cau Hoi Phong Van Laravel va PHP: Top 25 Nam 2026
25 cau hoi phong van Laravel va PHP thuong gap nhat. Service Container, Eloquent ORM, middleware, queues va trien khai production voi dap an chi tiet kem code mau.

Laravel 11: Xây dựng Ứng dụng Hoàn chỉnh từ Đầu
Hướng dẫn toàn diện xây dựng ứng dụng Laravel 11 với xác thực, REST API, Eloquent ORM và triển khai. Bài hướng dẫn thực hành dành cho lập trình viên từ cơ bản đến trung cấp.

Laravel Middleware Chi Tiet: Xac Thuc, Rate Limiting va Custom Middleware
Huong dan toan dien ve Laravel Middleware voi cac vi du thuc te ve xac thuc nguoi dung, rate limiting, tao middleware tuy chinh va cac pattern san xuat nang cao trong ung dung PHP.