Panduan Laravel Eloquent ORM

eloquent adalah active record orm laravel yang membuat interaksi database jadi lebih intuitif. untuk setup laravel, baca dulu tutorial laravel lengkap dan laravel validation & form request.
bagi saya, eloquent adalah alasan utama memilih laravel dibanding menulis SQL manual: relasi seperti hasMany dan belongsTo menghemat ratusan baris query yang harus saya tulis sendiri dulu. bagian yang paling berdampak di project nyata adalah eager loading untuk menghindari masalah N+1, akan dibahas detail di artikel ini.
1. apa itu eloquent?
eloquent adalah ORM (Object-Relational Mapper) bawaan Laravel. pakai pattern Active Record, artinya:
- setiap tabel database di-representasi oleh satu class Model.
- setiap baris di tabel adalah satu instance Model.
- setiap kolom adalah property di Model.
keuntungan dibanding query manual:
- method chaining yang intuitif:
User::where('active', 1)->orderBy('name')->get(). - relasi sebagai method:
$user->postsdaripada query manual dengan JOIN. - mass assignment protection lewat
$fillableatau$guarded. - event lifecycle: creating, created, updating, updated, saving, saved, deleting, deleted.
2. model + migration
setiap Model biasanya dibuat bareng migration untuk skema tabel:
php artisan make:model Post -m
perintah ini bikin dua file:
app/Models/Post.php: class Model kosong.database/migrations/xxxx_create_posts_table.php: migration file.
migration
// database/migrations/xxxx_create_posts_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('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->text('body');
$table->boolean('is_published')->default(false);
$table->timestamp('published_at')->nullable();
$table->timestamps();
$table->softDeletes(); // untuk soft delete
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
};
penjelasan per baris:
return new class extends Migration: anonymous class, syntax modern PHP 7+.public function up(): void: method yang jalan saat migrate. return typevoid.Schema::create('posts', function (Blueprint $table) {...}): bikin tabel baru dengan schema.$table->id(): kolomidbigInteger auto-increment primary key.$table->foreignId('user_id')->constrained(): kolomuser_idforeign key ke tabelusers(konvensi: singular +_id).->cascadeOnDelete(): kalau user dihapus, post-nya ikut terhapus.$table->string('title'): kolom VARCHAR(255).$table->text('body'): kolom TEXT untuk teks panjang.$table->boolean('is_published')->default(false): boolean dengan default false.$table->timestamp('published_at')->nullable(): timestamp nullable.$table->timestamps(): otomatis tambahcreated_atdanupdated_at.$table->softDeletes(): tambah kolomdeleted_atuntuk soft delete.public function down(): void: method rollback.
model
// app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Post extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = ['title', 'body', 'user_id', 'is_published', 'published_at'];
protected $casts = [
'is_published' => 'boolean',
'published_at' => 'datetime',
];
}
penjelasan per baris:
use HasFactory, SoftDeletes: trait. HasFactory untuk factory testing, SoftDeletes untuk soft delete.protected $fillable: whitelist kolom yang boleh diisi viaPost::create([...])atau$post->fill([...]). penting untuk keamanan mass assignment.protected $casts: otomatis konversi tipe data.is_publishedjadi boolean,published_atjadi Carbon datetime.
3. CRUD dasar
// Create
$post = Post::create([
'title' => 'Judul Pertama',
'body' => 'Isi artikel...',
'user_id' => 1,
]);
// Read satu
$post = Post::find(1); // by id
$post = Post::where('slug', 'artikel-1')->first();
$post = Post::findOrFail(1); // throw 404 kalau tidak ada
// Read banyak
$posts = Post::all();
$posts = Post::where('is_published', true)->get();
$posts = Post::where('user_id', 1)->orderBy('created_at', 'desc')->limit(10)->get();
$posts = Post::where('title', 'like', '%laravel%')->get();
// Count
$count = Post::count();
$count = Post::where('is_published', true)->count();
// Update
$post = Post::find(1);
$post->update(['title' => 'Judul Baru']);
// atau
$post->title = 'Judul Baru';
$post->save();
// Delete
$post->delete(); // hard delete, atau soft delete kalau pakai trait
// Restore (kalau soft delete)
$post->restore();
// Force delete (hapus permanen walau soft delete aktif)
$post->forceDelete();
4. relasi database
eloquent mendefinisikan relasi sebagai method di Model. ada 3 tipe utama + polymorphic.
one-to-one (hasOne, belongsTo)
satu User punya satu Profile:
// User.php
public function profile()
{
return $this->hasOne(Profile::class);
}
// Profile.php
public function user()
{
return $this->belongsTo(User::class);
}
// Pakai
$user = User::find(1);
$profile = $user->profile; // otomatis query ke profiles
one-to-many (hasMany, belongsTo)
satu User punya banyak Post:
// User.php
public function posts()
{
return $this->hasMany(Post::class);
}
// Post.php (sudah didefinisi)
public function user()
{
return $this->belongsTo(User::class);
}
// Pakai
$user = User::find(1);
$posts = $user->posts; // collection of Post
// Filter relasi
$published = $user->posts()->where('is_published', true)->get();
// Eager load
$user = User::with('posts')->find(1);
many-to-many (belongsToMany)
satu Post punya banyak Tag, satu Tag ada di banyak Post. butuh tabel pivot:
php artisan make:migration create_post_tag_table
// migration
Schema::create('post_tag', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')->constrained()->cascadeOnDelete();
$table->foreignId('tag_id')->constrained()->cascadeOnDelete();
$table->timestamps();
});
// Post.php
public function tags()
{
return $this->belongsToMany(Tag::class);
}
// Tag.php
public function posts()
{
return $this->belongsToMany(Post::class);
}
// Pakai
$post = Post::find(1);
$tags = $post->tags; // collection of Tag
$tagNames = $post->tags->pluck('name'); // ['laravel', 'php']
// Attach, detach, sync
$post->tags()->attach($tagId);
$post->tags()->detach($tagId);
$post->tags()->sync([1, 2, 3]); // set tags ke id 1, 2, 3
hasManyThrough
satu Country punya banyak User, setiap User punya banyak Post. query langsung post via country:
// Country.php
public function posts()
{
return $this->hasManyThrough(Post::class, User::class);
}
5. polymorphic relasi
satu Comment bisa untuk Post atau Video. satu model bisa dimiliki banyak model lain.
// Post.php dan Video.php
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
// Comment.php
public function commentable()
{
return $this->morphTo();
}
commentable_type dan commentable_id di tabel comments menentukan record parent.
6. eager loading : cegah N+1
ini topik paling penting untuk performa. tanpa eager loading, query lambat.
masalah N+1:
// ❌ Tanpa eager loading: 1 query untuk posts, N query untuk user (N = jumlah post)
$posts = Post::all();
foreach ($posts as $post) {
echo $post->user->name; // query ke users setiap loop
}
kalau ada 100 post, total 101 query ke database.
solusi: eager loading
// ✅ Dengan eager loading: hanya 2 query (1 untuk posts, 1 untuk semua user)
$posts = Post::with('user')->get();
foreach ($posts as $post) {
echo $post->user->name; // sudah di-cache
}
nested eager loading:
// Load post + user + profile user (3 query total)
$posts = Post::with('user.profile')->get();
lazy eager loading:
kalau sudah punya $posts tapi lupa with():
$posts->load('user'); // load user untuk collection yang sudah ada
eager load dengan filter:
Post::with(['user' => function ($query) {
$query->where('active', true);
}])->get();
7. query optimization untuk data besar
untuk table jutaan baris, query biasa bisa timeout atau makan memory besar.
chunk : batch processing
Post::chunk(200, function ($posts) {
foreach ($posts as $post) {
// proses 200 post sekaligus
}
});
chunk ambil 200 baris per batch, hemat memory.
cursor : memory-efficient streaming
foreach (Post::cursor() as $post) {
// proses satu per satu, memory konstan
}
cursor pakai PDO unbuffered query, memory-nya kecil tapi cuma bisa iterate sekali.
lazy : alternatif cursor
foreach (Post::lazy(500) as $post) {
// batch 500, default
}
lebih fleksibel dari cursor, bisa di-iterate ulang.
select kolom spesifik
// ❌ SELECT * (semua kolom)
$posts = Post::all();
// ✅ Hanya kolom yang dibutuhkan
$posts = Post::select('id', 'title', 'created_at')->get();
8. local scope
local scope = method di Model untuk query yang sering dipakai:
// Post.php
public function scopePublished($query)
{
return $query->where('is_published', true)->whereNotNull('published_at');
}
public function scopeByUser($query, $userId)
{
return $query->where('user_id', $userId);
}
// Pakai
$posts = Post::published()->get();
$posts = Post::published()->byUser(1)->get();
// Chain dengan query builder
$posts = Post::published()
->where('created_at', '>', now()->subWeek())
->orderBy('views', 'desc')
->get();
aturan penamaan: scope + nama method (PascalCase). method bisa nerima parameter.
9. global scope
global scope otomatis diterapkan ke semua query Model. paling sering untuk soft delete:
// SoftDeletes trait otomatis tambahkan global scope
// query Post::all() tidak termasuk yang soft deleted
// Custom global scope
class ActiveScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
$builder->where('is_active', true);
}
}
// Di Model
protected static function booted(): void
{
static::addGlobalScope(new ActiveScope);
}
untuk bypass global scope (misal mau include soft deleted):
Post::withoutGlobalScope(ActiveScope::class)->get();
Post::withTrashed()->get(); // untuk soft delete
Post::onlyTrashed()->get(); // hanya yang soft deleted
10. accessor dan mutator
accessor = transform nilai saat baca dari database:
// Post.php
protected function title(): Attribute
{
return Attribute::make(
get: fn ($value) => ucfirst($value),
);
}
// Pakai
$post = Post::find(1);
echo $post->title; // output: "Judul Pertama" (otomatis capitalize)
mutator = transform nilai saat simpan ke database:
protected function title(): Attribute
{
return Attribute::make(
get: fn ($value) => ucfirst($value),
set: fn ($value) => strtolower($value), // simpan selalu lowercase
);
}
11. observer
observer = class terpisah yang handle event lifecycle Model. bagus untuk logic yang kompleks:
php artisan make:observer PostObserver --model=Post
// app/Observers/PostObserver.php
namespace App\Observers;
use App\Models\Post;
class PostObserver
{
public function creating(Post $post): void
{
$post->slug = \Str::slug($post->title);
}
public function updated(Post $post): void
{
// Kirim notifikasi, log, dll
}
}
register di AppServiceProvider:
public function boot(): void
{
Post::observe(PostObserver::class);
}
12. soft delete
fitur untuk tandai record terhapus tanpa hapus dari database. berguna untuk audit, restore, atau foreign key constraint.
// Model
use Illuminate\Database\Eloquent\SoftDeletes;
class Post extends Model
{
use SoftDeletes;
}
// Pakai
$post->delete(); // cuma set deleted_at, data masih ada
// Query default tidak include yang soft deleted
$posts = Post::all(); // exclude yang deleted
// Include soft deleted
$posts = Post::withTrashed()->get();
// Hanya yang soft deleted
$posts = Post::onlyTrashed()->get();
// Restore
$post->restore();
// Permanent delete
$post->forceDelete();
13. update 2026: casts() method, upsert & withWhereHas
Laravel 11/12 bawa beberapa perubahan kecil tapi berdampak. kalau kalian ikut tutorial lama, tiga ini paling sering bikin bingung:
casts() method ganti $casts
Sejak Laravel 11, casts bisa ditulis sebagai method casts(): array biar bisa pakai static helper AsEnumCollection, AsCollection, dll. Masih kompatibel dengan $casts, tapi method lebih direkomendasikan:
use Illuminate\Database\Eloquent\Casts\AsEnumCollection;
use App\Enums\PostStatus;
protected function casts(): array
{
return [
'is_published' => 'boolean',
'published_at' => 'datetime',
'status' => AsEnumCollection::of(PostStatus::class),
'options' => 'hashed', // 'hashed' untuk password otomatis hash
];
}
upsert & updateOrCreate
Untuk sinkronisasi data massal jangan loop first() lalu save(). Pakai yang atomic:
// Insert atau update kalau email sudah ada (butuh unique index di email)
User::upsert([
['email' => '[email protected]', 'name' => 'Andi'],
['email' => '[email protected]', 'name' => 'Budi'],
], uniqueBy: ['email'], update: ['name']);
// Single row
User::updateOrCreate(['email' => '[email protected]'], ['name' => 'Andi']);
User::firstOrCreate(['email' => '[email protected]'], ['name' => 'Cici']);
upsert butuh unique index, jauh lebih cepat untuk 10k+ row daripada updateOrCreate yang select dulu.
withWhereHas & whereRelation
Sejak Laravel 9.17 ada withWhereHas biar tidak tulis kondisi dua kali:
// Dulu harus tulis dua kali
User::whereHas('posts', fn($q) => $q->where('is_published', true))
->with(['posts' => fn($q) => $q->where('is_published', true)])->get();
// Sekarang cukup
User::withWhereHas('posts', fn($q) => $q->where('is_published', true))->get();
// Shortcut filter langsung ke relasi
Post::whereRelation('user', 'is_active', true)->get();
Post::whereDoesntHave('comments')->get();
chunkById untuk update sambil iterate
Kalau mau update data sambil chunk, chunk() bisa skip/double karena offset. Pakai chunkById() atau lazyById():
Post::where('is_published', false)->chunkById(500, function ($posts) {
foreach ($posts as $post) $post->update(['is_published' => true]);
});
14. tips produksi
- selalu eager load relasi yang dipakai di view. N+1 adalah bug performa paling umum. Cek N+1 pakai Laravel Telescope atau
DB::enableQueryLog(). - pakai index di kolom yang sering di-WHERE, JOIN, atau ORDER BY. migration:
$table->string('email')->index(); - select kolom spesifik untuk query yang return banyak data.
SELECT *boros memory. - chunkById/lazy untuk table besar yang di-update sambil jalan. jangan
->get()juta baris sekaligus. - pakai cache untuk query yang jarang berubah:
Cache::remember('popular_posts', 3600, fn() => Post::popular()->get()). - monitor slow query dengan Laravel Telescope atau paket seperti Spatie Laravel Query Analyzer.
- transaction untuk operasi multi-query:
DB::transaction(function () {...}). - paginate untuk list panjang:
Post::paginate(20)daripadaPost::all(). Untuk infinite scroll,cursorPaginate()lebih efisien. - UUID/ULID kalau butuh ID tidak sequential: pakai
HasUuidsatauHasUlidstrait +$table->uuid('id')->primary().
Kesimpulan
- Eloquent = Active Record ORM Laravel, representasi tabel sebagai class Model
- relasi utama:
hasOne,belongsTo,hasMany,belongsToMany, plus polymorphic - eager loading dengan
with()wajib untuk hindari N+1 (1 + N query jadi 2 query) - chunk/cursor/lazy untuk optimasi query table besar
- local scope untuk query yang sering dipakai (
scopePublishedjadiPost::published()) - observer untuk logic kompleks di lifecycle Model
- soft delete untuk hapus tanpa kehilangan data
- untuk performa produksi: index, select kolom spesifik, cache, paginate
untuk topik terkait, ada tutorial laravel middleware (10/13 publish) yang bahas proteksi route dengan auth dan policy.
Baca Juga Mengenai :
Pertanyaan yang Sering Diajukan
Apa itu Eloquent ORM?
Eloquent adalah Active Record ORM bawaan Laravel. setiap tabel database punya model PHP yang representasikan. dengan Eloquent, query database jadi method chaining yang intuitif, tidak perlu tulis SQL manual.
Apa beda Eloquent dengan Query Builder?
Eloquent = Active Record, return object Model yang bisa dimanipulasi. Query Builder = lower-level, return stdClass. pakai Eloquent untuk kebanyakan kasus karena ada relasi, accessor, event. Query Builder untuk query kompleks yang tidak perlu model.
Bagaimana cara menghindari N+1 query problem?
pakai eager loading dengan with(). contoh: Post::with("user")->get() untuk load semua post + user terkait dalam 2 query, bukan 1 + N query. tambahkan lazy eager loading untuk relasi bersarang.
Apa itu soft delete di Eloquent?
fitur untuk tandai record sebagai terhapus tanpa hapus dari database. pakai trait SoftDeletes. data masih ada di tabel, tapi tidak muncul di query default. bisa di-restore kapan saja.
Bisa pakai Eloquent di luar Laravel?
bisa, Eloquent adalah package (illuminate/database). install via composer require illuminate/database. tapi kebanyakan orang pakai di dalam Laravel karena integrasi dengan facade dan service container.
Apa beda hasMany dan belongsToMany?
hasMany untuk relasi one-to-many (satu User punya banyak Post). belongsToMany untuk many-to-many (Post punya banyak Tag, Tag punya banyak Post, butuh tabel pivot).
Bagaimana cara optimasi query Eloquent untuk data besar?
pakai chunk() untuk batch processing, cursor() untuk memory-efficient streaming, atau lazy() sebagai alternatif. untuk eager loading, selalu specify kolom yang dibutuhkan dengan select() untuk hindari SELECT *.
Apa itu local scope dan global scope di Eloquent?
local scope: method di model untuk query yang sering dipakai, misal scopeActive() jadi Post::active(). global scope: otomatis diterapkan ke semua query model, misal soft delete global scope.

