Eloquent ORM: รูปแบบและการเพิ่มประสิทธิภาพสำหรับ Laravel
เชี่ยวชาญ Eloquent ORM ด้วยรูปแบบขั้นสูงและเทคนิคการเพิ่มประสิทธิภาพ ทั้ง eager loading, query scope, accessor, mutator และประสิทธิภาพสำหรับแอป Laravel

Eloquent ORM แปลงการโต้ตอบกับฐานข้อมูลให้กลายเป็นการดำเนินการที่ลื่นไหลและเข้าใจง่าย นอกเหนือจากไวยากรณ์ที่สวยงามแล้ว การเชี่ยวชาญรูปแบบขั้นสูงและเทคนิคการเพิ่มประสิทธิภาพยังเป็นตัวกำหนดประสิทธิภาพของแอปพลิเคชัน Laravel ในระบบจริง
ปัญหา N+1 เป็นสาเหตุหลักของความช้าในแอปพลิเคชัน Eloquent ทุกความสัมพันธ์ที่ไม่ได้รับการเพิ่มประสิทธิภาพจะสร้างคำสั่ง SQL เพิ่มขึ้นหนึ่งคำสั่งต่อหนึ่งระเบียน
แก้ไขปัญหา N+1 ด้วย eager loading
ปัญหา N+1 เกิดขึ้นเมื่อทุกการวนลูปบนคอลเลกชันกระตุ้นให้เกิดคำสั่งเพิ่มเติมเพื่อโหลดความสัมพันธ์ ด้วยบทความ 100 บทความและผู้เขียน นั่นหมายถึง 101 คำสั่งแทนที่จะเป็นคำสั่งเดียวที่ผ่านการเพิ่มประสิทธิภาพแล้ว
Eager loading ดึงความสัมพันธ์ทั้งหมดในคำสั่งสูงสุดหนึ่งหรือสองคำสั่ง ลดเวลาตอบสนองอย่างมาก
// 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 ด้วย with() คาดการณ์ความต้องการและโหลดข้อมูลล่วงหน้า ความแตกต่างของประสิทธิภาพจะน่าทึ่งมากบนคอลเลกชันขนาดใหญ่
Eager loading แบบมีเงื่อนไขและถูกจำกัด
ความสัมพันธ์ขนาดใหญ่บางครั้งต้องการการโหลดเพียงบางส่วน การจำกัด eager loading จะลดข้อมูลที่ดึงมาในขณะที่ยังคงหลีกเลี่ยง 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'));
}
}เมธอด withCount() และ withSum() เพิ่มการรวมค่าโดยไม่ต้องโหลดคอลเลกชันทั้งหมด เหมาะอย่างยิ่งสำหรับสถิติบนแดชบอร์ด
Query scope สำหรับคำสั่งที่นำกลับมาใช้ใหม่ได้
Query scope ห่อหุ้มตรรกะการกรองไว้ภายในโมเดล Scope ระดับท้องถิ่นมอบความยืดหยุ่น ในขณะที่ scope ระดับโลกจะถูกนำไปใช้กับทุกคำสั่งโดยอัตโนมัติ
Scope ระดับท้องถิ่นใช้คำนำหน้า scope ในโมเดลแต่จะเรียกใช้โดยไม่มีคำนำหน้านั้น: scopeActive() กลายเป็น 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();การเชื่อมโยง scope ทำให้คำสั่งอ่านง่ายและบำรุงรักษาง่าย โดยรวบรวมตรรกะธุรกิจไว้ในโมเดล
พร้อมที่จะพิชิตการสัมภาษณ์ Laravel แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
Accessor และ mutator ด้วย Attribute
Laravel 9+ นำเสนอไวยากรณ์แบบรวมศูนย์สำหรับ accessor และ mutator ผ่านคลาส Attribute แนวทางสมัยใหม่นี้ทดแทนเมธอด get*Attribute และ 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"เมธอด shouldCache() เพิ่มประสิทธิภาพให้กับ accessor ที่มีค่าใช้จ่ายสูงโดยหลีกเลี่ยงการคำนวณซ้ำหลายครั้งบนโมเดลเดียวกัน
Cast แบบกำหนดเองสำหรับชนิดข้อมูลที่ซับซ้อน
Cast แปลงค่าระหว่าง PHP และฐานข้อมูลโดยอัตโนมัติ Cast แบบกำหนดเองห่อหุ้มตรรกะการ serialize สำหรับชนิดข้อมูลทางธุรกิจ
// 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 ที่ผสมกับ cast แบบกำหนดเองรับประกันความถูกต้องของข้อมูลทางธุรกิจในขณะที่ยังคงรักษา API ที่สวยงามไว้
เพิ่มประสิทธิภาพคำสั่งขนาดใหญ่
การดำเนินการกับข้อมูลหลายล้านระเบียนต้องใช้เทคนิคเฉพาะเพื่อหลีกเลี่ยงการใช้หน่วยความจำจนหมด Chunking และ cursor ประมวลผลข้อมูลเป็นชุด
Model::all() โหลดระเบียนทั้งหมดเข้าสู่หน่วยความจำ บนตารางที่มี 100,000 แถว สิ่งนี้สามารถใช้ RAM หลายกิกะไบต์และทำให้แอปพลิเคชันล่ม
// 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;
}
}การเลือกระหว่าง chunk(), lazy() และ cursor() ขึ้นอยู่กับสถานการณ์การใช้งาน: chunk() สำหรับการแก้ไข, lazy() สำหรับการดำเนินการระหว่างกลาง และ cursor() สำหรับการอ่านอย่างเดียวด้วยหน่วยความจำขั้นต่ำ
ความสัมพันธ์ polymorphic ขั้นสูง
ความสัมพันธ์แบบ polymorphic อนุญาตให้โมเดลหนึ่งสามารถสังกัดโมเดลหลายประเภทผ่านความสัมพันธ์เดียว ความยืดหยุ่นนี้เหมาะสำหรับความคิดเห็น แท็ก หรือไฟล์แนบ
// 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\Videoความสัมพันธ์ polymorphic หลีกเลี่ยงการทำซ้ำตารางและรวบรวมตรรกะสำหรับฟังก์ชันที่ใช้ร่วมกัน
พร้อมที่จะพิชิตการสัมภาษณ์ Laravel แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
Trait และ observer สำหรับตรรกะที่นำกลับมาใช้ใหม่ได้
Trait ห่อหุ้มพฤติกรรมที่นำกลับมาใช้ใหม่ได้ระหว่างโมเดล Observer รวมศูนย์ hook บนเหตุการณ์วงจรชีวิต
// 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'];
}เมธอด boot* ของ trait จะถูกดำเนินการโดยอัตโนมัติระหว่างการเริ่มต้นโมเดล ทำให้การผสานรวมเป็นไปอย่างโปร่งใส
บทสรุป
การเชี่ยวชาญ Eloquent ORM ขึ้นอยู่กับการเข้าใจกลไกพื้นฐานและการประยุกต์ใช้รูปแบบที่เหมาะสม เทคนิคที่นำเสนอเปลี่ยนคำสั่งที่ไร้เดียงสาให้กลายเป็นโค้ดที่มีประสิทธิภาพและบำรุงรักษาได้
เช็กลิสต์การเพิ่มประสิทธิภาพ Eloquent:
✅ ใช้ with() อย่างเป็นระบบสำหรับความสัมพันธ์ที่แสดง
✅ ใช้ withCount() แทนการโหลดคอลเลกชันเพื่อนับจำนวน
✅ ห่อหุ้มตรรกะการกรองใน query scope
✅ เลือกใช้ accessor มากกว่าการคำนวณซ้ำใน view
✅ ใช้งาน cast แบบกำหนดเองสำหรับ Value Object ทางธุรกิจ
✅ ใช้ chunk() หรือ lazy() สำหรับการดำเนินการบนตารางขนาดใหญ่
✅ รวมศูนย์ผลข้างเคียงไว้ใน observer
✅ แยกพฤติกรรมร่วมออกเป็น trait
เครื่องมือ php artisan telescope หรือ laravel-debugbar ช่วยให้สามารถมองเห็นคำสั่ง SQL ที่ถูกสร้างขึ้นและระบุการเพิ่มประสิทธิภาพที่ยังขาดอยู่
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
แท็ก
แชร์
บทความที่เกี่ยวข้อง

25 คำถามสัมภาษณ์งาน Laravel และ PHP ยอดนิยมในปี 2026
รวม 25 คำถามสัมภาษณ์งาน Laravel และ PHP ที่พบบ่อยที่สุด ครอบคลุม Service Container, Eloquent ORM, middleware, queues และการ deploy ระบบ production พร้อมคำตอบและตัวอย่างโค้ดครบถ้วน

Laravel 11: สร้างแอปพลิเคชันสมบูรณ์ตั้งแต่เริ่มต้น
คู่มือครบวงจรสำหรับสร้างแอปพลิเคชัน Laravel 11 พร้อมระบบยืนยันตัวตน, REST API, Eloquent ORM และการ Deploy บทเรียนเชิงปฏิบัติสำหรับนักพัฒนาระดับเริ่มต้นถึงกลาง

Laravel Middleware เจาะลึก: Authentication, Rate Limiting และ Custom Middleware ฉบับสมบูรณ์
คู่มือเจาะลึก Laravel Middleware ครอบคลุมการทำงานของ Authentication Middleware, Rate Limiting ด้วย Throttle, การสร้าง Custom Middleware และการลงทะเบียน Middleware ใน Laravel 12 พร้อมตัวอย่างโค้ดที่ใช้งานได้จริงในโปรดักชัน