Eloquent ORM: รูปแบบและการเพิ่มประสิทธิภาพสำหรับ Laravel

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

รูปแบบและการเพิ่มประสิทธิภาพ Eloquent ORM สำหรับ Laravel

Eloquent ORM แปลงการโต้ตอบกับฐานข้อมูลให้กลายเป็นการดำเนินการที่ลื่นไหลและเข้าใจง่าย นอกเหนือจากไวยากรณ์ที่สวยงามแล้ว การเชี่ยวชาญรูปแบบขั้นสูงและเทคนิคการเพิ่มประสิทธิภาพยังเป็นตัวกำหนดประสิทธิภาพของแอปพลิเคชัน Laravel ในระบบจริง

ประสิทธิภาพต้องมาก่อน

ปัญหา N+1 เป็นสาเหตุหลักของความช้าในแอปพลิเคชัน Eloquent ทุกความสัมพันธ์ที่ไม่ได้รับการเพิ่มประสิทธิภาพจะสร้างคำสั่ง SQL เพิ่มขึ้นหนึ่งคำสั่งต่อหนึ่งระเบียน

แก้ไขปัญหา N+1 ด้วย eager loading

ปัญหา N+1 เกิดขึ้นเมื่อทุกการวนลูปบนคอลเลกชันกระตุ้นให้เกิดคำสั่งเพิ่มเติมเพื่อโหลดความสัมพันธ์ ด้วยบทความ 100 บทความและผู้เขียน นั่นหมายถึง 101 คำสั่งแทนที่จะเป็นคำสั่งเดียวที่ผ่านการเพิ่มประสิทธิภาพแล้ว

Eager loading ดึงความสัมพันธ์ทั้งหมดในคำสั่งสูงสุดหนึ่งหรือสองคำสั่ง ลดเวลาตอบสนองอย่างมาก

app/Http/Controllers/ArticleController.phpphp
// 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

app/Http/Controllers/UserController.phpphp
// 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()

app/Models/Article.phpphp
// 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}%"));
        });
    }
}
app/Models/Scopes/PublishedScope.phpphp
// 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 ทำให้คำสั่งอ่านง่ายและบำรุงรักษาง่าย โดยรวบรวมตรรกะธุรกิจไว้ในโมเดล

พร้อมที่จะพิชิตการสัมภาษณ์ Laravel แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

Accessor และ mutator ด้วย Attribute

Laravel 9+ นำเสนอไวยากรณ์แบบรวมศูนย์สำหรับ accessor และ mutator ผ่านคลาส Attribute แนวทางสมัยใหม่นี้ทดแทนเมธอด get*Attribute และ set*Attribute

app/Models/User.phpphp
// 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 สำหรับชนิดข้อมูลทางธุรกิจ

app/Casts/MoneyCast.phpphp
// 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');
    }
}
app/ValueObjects/Money.phpphp
// 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,
        ];
    }
}
app/Models/Order.phpphp
// 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 หลายกิกะไบต์และทำให้แอปพลิเคชันล่ม

app/Console/Commands/ProcessUsersCommand.phpphp
// 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 อนุญาตให้โมเดลหนึ่งสามารถสังกัดโมเดลหลายประเภทผ่านความสัมพันธ์เดียว ความยืดหยุ่นนี้เหมาะสำหรับความคิดเห็น แท็ก หรือไฟล์แนบ

app/Models/Comment.phpphp
// 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);
    }
}
app/Models/Article.phpphp
// 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');
    }
}
app/Models/Video.phpphp
// 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 หลีกเลี่ยงการทำซ้ำตารางและรวบรวมตรรกะสำหรับฟังก์ชันที่ใช้ร่วมกัน

พร้อมที่จะพิชิตการสัมภาษณ์ Laravel แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

Trait และ observer สำหรับตรรกะที่นำกลับมาใช้ใหม่ได้

Trait ห่อหุ้มพฤติกรรมที่นำกลับมาใช้ใหม่ได้ระหว่างโมเดล Observer รวมศูนย์ hook บนเหตุการณ์วงจรชีวิต

app/Models/Concerns/HasSlug.phpphp
// 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;
    }
}
app/Models/Concerns/HasUuid.phpphp
// 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';
    }
}
app/Observers/ArticleObserver.phpphp
// 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;
    }
}
app/Models/Article.phpphp
// 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 ที่ถูกสร้างขึ้นและระบุการเพิ่มประสิทธิภาพที่ยังขาดอยู่

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

ชาเลนจ์ประจำวัน

คุณหาบั๊กใน Laravel เจอไหม

โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

Anthony Fillion-Maillet

เขียนโดย

Anthony Fillion-Maillet

ผู้ก่อตั้ง SharpSkill

เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่

อัปเดตเมื่อ 28 เมษายน 2569

แท็ก

#laravel
#eloquent
#php
#orm
#database

แชร์

บทความที่เกี่ยวข้อง