September 27, 2026

Eloquent ORM.

Dasar Eloquent di Laravel 13: model, relasi, eager loading & masalah N+1, query scope, accessor/mutator/casts, serta mass assignment.

Eloquent adalah ORM bawaan Laravel: setiap tabel diwakili sebuah model, dan setiap baris menjadi objek. Catatan ini merangkum hal-hal yang paling sering dipakai sehari-hari, dengan sintaks Laravel 13 (PHP 8.3+). Laravel 13 memperluas dukungan PHP attribute di model; cara lama (property $fillable, prefix scopeX) tetap berfungsi, jadi keduanya ditunjukkan.


1. Membuat Model

php artisan make:model Post -mfsc
# -m migration, -f factory, -s seeder, -c controller
php artisan make:model Post --all   # semuanya, termasuk policy & form request

Konvensi default:

KonvensiDefaultOverride
Nama tabelPost → posts#[Table('blog_posts')] atau protected $table
Primary keyid auto increment#[Table(key: 'uuid', keyType: 'string', incrementing: false)]
Timestampcreated_at, updated_at#[WithoutTimestamps] atau public $timestamps = false
Koneksikoneksi default#[Connection('mysql')]
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

#[Fillable(['title', 'slug', 'body', 'published_at', 'user_id'])]
class Post extends Model
{
    use HasFactory;
}

2. CRUD Singkat

$post = Post::create(['title' => 'Halo', 'slug' => 'halo', 'body' => '...', 'user_id' => 1]);

$post = Post::find(1);                 // null jika tidak ada
$post = Post::findOrFail(1);           // 404 otomatis di controller
$post = Post::where('slug', 'halo')->firstOrFail();

$posts = Post::where('published_at', '<=', now())
    ->latest('published_at')
    ->paginate(10);

$post->update(['title' => 'Halo Dunia']);
$post->delete();

Post::updateOrCreate(['slug' => 'halo'], ['title' => 'Halo']);
Post::firstOrCreate(['slug' => 'baru'], ['title' => 'Baru']);

3. Mass Assignment

create() dan update() menerima array — berbahaya jika langsung diisi $request->all(), karena user bisa menyisipkan field seperti is_admin. Eloquent melindunginya lewat mass assignment protection.

// Laravel 13: attribute
#[Fillable(['title', 'body'])]
class Post extends Model {}

// Cara klasik (masih valid)
class Post extends Model
{
    protected $fillable = ['title', 'body'];
    // atau kebalikannya:
    // protected $guarded = ['id', 'is_admin'];
}

Praktik yang aman: isi dari data yang sudah divalidasi.

public function store(StorePostRequest $request)
{
    $post = $request->user()->posts()->create($request->validated());
}

Agar field yang tidak fillable tidak diam-diam dibuang saat development, aktifkan mode strict di AppServiceProvider:

use Illuminate\Database\Eloquent\Model;

public function boot(): void
{
    Model::shouldBeStrict(! $this->app->isProduction());
    // = preventLazyLoading + preventSilentlyDiscardingAttributes + preventAccessingMissingAttributes
}

4. Relasi

RelasiContohMethod
One to oneUser → ProfilehasOne / belongsTo
One to manyUser → PosthasMany / belongsTo
Many to manyPost ↔ Tag (tabel pivot post_tag)belongsToMany
Has many throughCountry → Post (lewat User)hasManyThrough
PolymorphicComment untuk Post & VideomorphTo / morphMany
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Post extends Model
{
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function comments(): HasMany
    {
        return $this->hasMany(Comment::class);
    }

    public function tags(): BelongsToMany
    {
        return $this->belongsToMany(Tag::class)->withTimestamps();
    }
}

Pemakaian:

$post->user->name;                      // property → hasil relasi
$post->comments()->where('approved', true)->get(); // method → query builder
$post->tags()->attach([1, 2]);
$post->tags()->sync([2, 3]);            // hapus yang tidak ada di array
$user->posts()->create([...]);          // user_id otomatis terisi

5. Eager Loading & Masalah N+1

Contoh klasik yang lambat:

$posts = Post::all();               // 1 query
foreach ($posts as $post) {
    echo $post->user->name;         // +1 query untuk SETIAP post
}
// 100 post = 101 query

Solusinya eager loading dengan with():

$posts = Post::with('user')->get();          // 2 query saja
$posts = Post::with(['user', 'tags', 'comments.user'])->get(); // nested
$posts = Post::with(['comments' => fn ($q) => $q->latest()->limit(5)])->get();

// hanya hitung, tanpa memuat relasi
$posts = Post::withCount('comments')->get(); // $post->comments_count

// sudah terlanjur di-load
$posts->load('tags');
$post->loadMissing('user');

Deteksi N+1 sejak dini: Model::preventLazyLoading() (bagian dari shouldBeStrict) akan melempar exception ketika relasi di-load secara lazy di environment non-produksi. Laravel Debugbar atau Telescope juga membantu melihat jumlah query per request.


6. Query Scope

Scope membungkus kondisi query yang sering dipakai.

use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;

class Post extends Model
{
    #[Scope]
    protected function published(Builder $query): void
    {
        $query->whereNotNull('published_at')->where('published_at', '<=', now());
    }

    #[Scope]
    protected function byAuthor(Builder $query, int $userId): void
    {
        $query->where('user_id', $userId);
    }
}

Post::published()->byAuthor(3)->latest()->get();

Sintaks lama public function scopePublished(Builder $query) masih didukung. Global scope (diterapkan otomatis ke semua query model) bisa dipasang dengan #[ScopedBy([ActiveScope::class])]; contoh bawaan adalah SoftDeletes.


7. Accessor, Mutator & Casts

Casts

Mengubah tipe kolom secara otomatis saat dibaca/ditulis:

use App\Enums\PostStatus;

protected function casts(): array
{
    return [
        'published_at' => 'datetime',
        'is_featured'  => 'boolean',
        'meta'         => 'array',          // kolom JSON ↔ array PHP
        'status'       => PostStatus::class, // backed enum
        'price'        => 'decimal:2',
        'secret_note'  => 'encrypted',
    ];
}

Accessor & Mutator

use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Str;

protected function title(): Attribute
{
    return Attribute::make(
        get: fn (string $value) => Str::title($value),   // accessor
        set: fn (string $value) => trim($value),         // mutator
    );
}

// Atribut virtual (tidak ada kolomnya)
protected function excerpt(): Attribute
{
    return Attribute::make(
        get: fn () => Str::limit(strip_tags($this->body), 120),
    );
}

Atribut virtual tidak ikut di toArray()/JSON kecuali ditambahkan ke $appends. Sebaliknya, sembunyikan kolom sensitif dengan $hidden (misal password, remember_token).


8. Fitur Lain yang Sering Dipakai

use Illuminate\Database\Eloquent\SoftDeletes;

class Post extends Model
{
    use SoftDeletes;   // butuh kolom deleted_at ($table->softDeletes())
}

Post::withTrashed()->find(1)?->restore();

// memproses data besar tanpa memenuhi memori
Post::where('published_at', '<', now()->subYear())->chunkById(500, function ($posts) {
    // ...
});

foreach (Post::lazy() as $post) { /* ... */ }

9. Pitfalls

  • N+1 query di loop atau di Resource/Inertia props → selalu cek with().
  • $request->all() ke create() → pakai $request->validated().
  • $post->comments vs $post->comments() → property mengembalikan Collection (sudah di-query), method mengembalikan builder (bisa ditambah where).
  • get() lalu count() memuat semua baris ke memori. Pakai ->count() langsung di query atau withCount.
  • update() massal (Post::where(...)->update([...])) tidak memicu event model/observer dan tidak melewati mutator.
  • Serialisasi model ke frontend (JSON, Inertia) → semua kolom ikut terkirim. Batasi dengan $hidden, API Resource, atau ->only([...]).

Eloquent jadi fondasi di project Laravel saya seperti Warung Online , Kos Management App , dan Watch Store . Lanjutan: Laravel Queue , Inertia.js .

Referensi: Eloquent: Getting Started , Relationships , Mutators & Casting .

Hey! I’m Fanny, the software engineer tending to this digital garden. You can read more about me, or subscribe by email.

Comments