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:
| Konvensi | Default | Override |
|---|---|---|
| Nama tabel | Post → posts | #[Table('blog_posts')] atau protected $table |
| Primary key | id auto increment | #[Table(key: 'uuid', keyType: 'string', incrementing: false)] |
| Timestamp | created_at, updated_at | #[WithoutTimestamps] atau public $timestamps = false |
| Koneksi | koneksi 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
| Relasi | Contoh | Method |
|---|---|---|
| One to one | User → Profile | hasOne / belongsTo |
| One to many | User → Post | hasMany / belongsTo |
| Many to many | Post ↔ Tag (tabel pivot post_tag) | belongsToMany |
| Has many through | Country → Post (lewat User) | hasManyThrough |
| Polymorphic | Comment untuk Post & Video | morphTo / 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()kecreate()→ pakai$request->validated().$post->commentsvs$post->comments()→ property mengembalikan Collection (sudah di-query), method mengembalikan builder (bisa ditambahwhere).get()lalucount()memuat semua baris ke memori. Pakai->count()langsung di query atauwithCount.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.