Panduan Laravel Eloquent ORM

| | Laravel
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->posts daripada query manual dengan JOIN.
  • mass assignment protection lewat $fillable atau $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 type void.
  • Schema::create('posts', function (Blueprint $table) {...}) : bikin tabel baru dengan schema.
  • $table->id() : kolom id bigInteger auto-increment primary key.
  • $table->foreignId('user_id')->constrained() : kolom user_id foreign key ke tabel users (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 tambah created_at dan updated_at.
  • $table->softDeletes() : tambah kolom deleted_at untuk 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 via Post::create([...]) atau $post->fill([...]). penting untuk keamanan mass assignment.
  • protected $casts : otomatis konversi tipe data. is_published jadi boolean, published_at jadi 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) daripada Post::all(). Untuk infinite scroll, cursorPaginate() lebih efisien.
  • UUID/ULID kalau butuh ID tidak sequential: pakai HasUuids atau HasUlids trait + $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 (scopePublished jadi Post::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.

Sigit Nurhanafi avatar
Full-stack developer & technical writer. Berpengalaman di PHP, Laravel, NodeJS, MySQL, dan Python. Aktif menulis tutorial pemrograman dan maintaining open-source projects di PemburuKode sejak 2021.