# Eloquent ORM: รูปแบบและการเพิ่มประสิทธิภาพสำหรับ Laravel > เชี่ยวชาญ Eloquent ORM ด้วยรูปแบบขั้นสูงและเทคนิคการเพิ่มประสิทธิภาพ ทั้ง eager loading, query scope, accessor, mutator และประสิทธิภาพสำหรับแอป Laravel - Published: 2026-02-10 - Updated: 2026-04-28 - Author: SharpSkill - Tags: laravel, eloquent, php, orm, database - Reading time: 18 min --- Eloquent ORM แปลงการโต้ตอบกับฐานข้อมูลให้กลายเป็นการดำเนินการที่ลื่นไหลและเข้าใจง่าย นอกเหนือจากไวยากรณ์ที่สวยงามแล้ว การเชี่ยวชาญรูปแบบขั้นสูงและเทคนิคการเพิ่มประสิทธิภาพยังเป็นตัวกำหนดประสิทธิภาพของแอปพลิเคชัน Laravel ในระบบจริง > **ประสิทธิภาพต้องมาก่อน** > > ปัญหา N+1 เป็นสาเหตุหลักของความช้าในแอปพลิเคชัน Eloquent ทุกความสัมพันธ์ที่ไม่ได้รับการเพิ่มประสิทธิภาพจะสร้างคำสั่ง SQL เพิ่มขึ้นหนึ่งคำสั่งต่อหนึ่งระเบียน ## แก้ไขปัญหา N+1 ด้วย eager loading ปัญหา N+1 เกิดขึ้นเมื่อทุกการวนลูปบนคอลเลกชันกระตุ้นให้เกิดคำสั่งเพิ่มเติมเพื่อโหลดความสัมพันธ์ ด้วยบทความ 100 บทความและผู้เขียน นั่นหมายถึง 101 คำสั่งแทนที่จะเป็นคำสั่งเดียวที่ผ่านการเพิ่มประสิทธิภาพแล้ว Eager loading ดึงความสัมพันธ์ทั้งหมดในคำสั่งสูงสุดหนึ่งหรือสองคำสั่ง ลดเวลาตอบสนองอย่างมาก ```php // app/Http/Controllers/ArticleController.php // 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 ```php // app/Http/Controllers/UserController.php // 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()` ```php // app/Models/Article.php // 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}%")); }); } } ``` ```php // app/Models/Scopes/PublishedScope.php // 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()); } } ``` ```php // 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 ทำให้คำสั่งอ่านง่ายและบำรุงรักษาง่าย โดยรวบรวมตรรกะธุรกิจไว้ในโมเดล ## Accessor และ mutator ด้วย Attribute Laravel 9+ นำเสนอไวยากรณ์แบบรวมศูนย์สำหรับ accessor และ mutator ผ่านคลาส `Attribute` แนวทางสมัยใหม่นี้ทดแทนเมธอด `get*Attribute` และ `set*Attribute` ```php // app/Models/User.php // 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'), ); } } ``` ```php // 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 สำหรับชนิดข้อมูลทางธุรกิจ ```php // app/Casts/MoneyCast.php // 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'); } } ``` ```php // app/ValueObjects/Money.php // 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, ]; } } ``` ```php // app/Models/Order.php // 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', ]; } } ``` ```php // 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 DB ``` Value Object ที่ผสมกับ cast แบบกำหนดเองรับประกันความถูกต้องของข้อมูลทางธุรกิจในขณะที่ยังคงรักษา API ที่สวยงามไว้ ## เพิ่มประสิทธิภาพคำสั่งขนาดใหญ่ การดำเนินการกับข้อมูลหลายล้านระเบียนต้องใช้เทคนิคเฉพาะเพื่อหลีกเลี่ยงการใช้หน่วยความจำจนหมด Chunking และ cursor ประมวลผลข้อมูลเป็นชุด > **ระวังเรื่องหน่วยความจำ** > > `Model::all()` โหลดระเบียนทั้งหมดเข้าสู่หน่วยความจำ บนตารางที่มี 100,000 แถว สิ่งนี้สามารถใช้ RAM หลายกิกะไบต์และทำให้แอปพลิเคชันล่ม ```php // app/Console/Commands/ProcessUsersCommand.php // 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 อนุญาตให้โมเดลหนึ่งสามารถสังกัดโมเดลหลายประเภทผ่านความสัมพันธ์เดียว ความยืดหยุ่นนี้เหมาะสำหรับความคิดเห็น แท็ก หรือไฟล์แนบ ```php // app/Models/Comment.php // 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); } } ``` ```php // app/Models/Article.php // 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'); } } ``` ```php // app/Models/Video.php // 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'); } } ``` ```php // 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']); }); } }; ``` ```php // 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 หลีกเลี่ยงการทำซ้ำตารางและรวบรวมตรรกะสำหรับฟังก์ชันที่ใช้ร่วมกัน ## Trait และ observer สำหรับตรรกะที่นำกลับมาใช้ใหม่ได้ Trait ห่อหุ้มพฤติกรรมที่นำกลับมาใช้ใหม่ได้ระหว่างโมเดล Observer รวมศูนย์ hook บนเหตุการณ์วงจรชีวิต ```php // app/Models/Concerns/HasSlug.php // 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; } } ``` ```php // app/Models/Concerns/HasUuid.php // 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'; } } ``` ```php // app/Observers/ArticleObserver.php // 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; } } ``` ```php // app/Models/Article.php // 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 ที่ถูกสร้างขึ้นและระบุการเพิ่มประสิทธิภาพที่ยังขาดอยู่ --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/laravel/eloquent-orm-patterns-optimizations