Database, Migration, dan Eloquent untuk API - Perwira Learning Center



1. LATAR BELAKANG

    Setelah berhasil membuat routing dan controller untuk API kita, tantangan berikutnya adalah bagaimana menyimpan dan mengelola data secara permanen. Pada artikel sebelumnya, kita masih menggunakan data statis di dalam controller. Tentunya ini tidak realistis untuk aplikasi production. Di sinilah database berperan penting.

Pada hari ketiga minggu ini, saya mempelajari bagaimana Laravel berinteraksi dengan database melalui tiga komponen utama: Migration untuk membuat dan memodifikasi struktur tabel, Eloquent ORM untuk berinteraksi dengan data secara intuitif, dan Database Seeding untuk mengisi data dummy. Ketiga komponen ini bekerja bersama untuk memudahkan developer mengelola data tanpa harus menulis query SQL mentah yang panjang dan rawan error.

Yang paling menarik dari Eloquent adalah pendekatannya yang elegan—setiap tabel database direpresentasikan sebagai sebuah Model, dan setiap baris data sebagai objek. Ini membuat kode kita lebih bersih, lebih aman dari SQL injection, dan sangat ekspresif.


2. ALAT DAN BAHAN

2.1 Perangkat Lunak

  • Laravel 11 - Project yang sudah berjalan
  • MySQL/MariaDB - Database management system
  • phpMyAdmin/TablePlus - Untuk melihat data di database (opsional)
  • Postman/Thunder Client - Untuk testing endpoint API
  • Git Bash/Terminal - Untuk menjalankan perintah Artisan

2.2 Perangkat Keras

  • Laptop dengan spesifikasi standar

3. PEMBAHASAN

3.1 Konfigurasi Database di Laravel

    Langkah pertama adalah mengkonfigurasi koneksi database. Semua konfigurasi disimpan di file .env yang terletak di root project:

env
# File: .env

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=perpustakaan_api   # Nama database yang akan kita buat
DB_USERNAME=root
DB_PASSWORD=

Catatan Penting:

  • File .env tidak boleh di-commit ke Git (sudah otomatis diabaikan oleh .gitignore)
  • Untuk development lokal, biasanya DB_HOST=127.0.0.1 dan DB_PASSWORD kosong (default XAMPP/Laragon)
  • Nama database harus dibuat terlebih dahulu di MySQL sebelum menjalankan migration

3.2 Memahami Migration di Laravel

    Migration adalah fitur version control untuk database. Dengan migration, kita bisa membuat, mengubah, atau menghapus tabel database menggunakan kode PHP, bukan SQL mentah. Ini memudahkan kolaborasi tim karena perubahan struktur database bisa di-track melalui Git.

Membuat Migration Baru:

bash
# Membuat migration untuk tabel posts
php artisan make:migration create_posts_table

# Membuat migration untuk menambah kolom ke tabel yang sudah ada
php artisan make:migration add_category_id_to_posts_table

# Membuat migration dengan opsi --create (langsung dengan schema)
php artisan make:migration create_comments_table --create=comments

# Membuat migration dengan opsi --table (untuk modify tabel)
php artisan make:migration add_is_published_to_posts --table=posts

Struktur File Migration:

Mari kita lihat file migration untuk tabel posts yang akan kita buat:

php
<?php
// File: database/migrations/2026_02_25_000001_create_posts_table.php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     * Method ini akan dijalankan saat perintah `php artisan migrate`
     */
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();  // Auto-increment primary key
            $table->string('title');  // VARCHAR
            $table->text('content');  // TEXT
            $table->string('slug')->unique();  // VARCHAR dengan unique constraint
            $table->string('image')->nullable();  // Boleh null
            $table->foreignId('category_id')->constrained();  // Foreign key ke categories
            $table->foreignId('user_id')->constrained();  // Foreign key ke users
            $table->integer('views')->default(0);  // Integer dengan default 0
            $table->boolean('is_published')->default(false);  // Boolean
            $table->timestamp('published_at')->nullable();  // Timestamp boleh null
            $table->timestamps();  // Membuat kolom created_at dan updated_at
        });
    }

    /**
     * Reverse the migrations.
     * Method ini akan dijalankan saat perintah `php artisan migrate:rollback`
     */
    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};

Tipe Data yang Sering Digunakan:

Tipe DataDeskripsiContoh Penggunaan
$table->id()Auto-increment big integer (primary key)ID record
$table->string('kolom', 100)VARCHAR dengan panjang maksimalJudul, nama, email
$table->text('kolom')TEXT untuk teks panjangKonten artikel
$table->integer('kolom')IntegerJumlah view, umur
$table->boolean('kolom')Boolean (true/false)Status aktif/publish
$table->date('kolom')Date (YYYY-MM-DD)Tanggal lahir
$table->datetime('kolom')Datetime (YYYY-MM-DD HH:MM:SS)Waktu event
$table->foreignId('kolom')Unsigned big integer untuk foreign keycategory_id, user_id
$table->timestamps()created_at dan updated_atWaktu record dibuat/diupdate
$table->softDeletes()deleted_at untuk soft deleteMenandai data terhapus

Menjalankan Migration:

bash
# Menjalankan semua migration yang belum dijalankan
php artisan migrate

# Melihat status migration
php artisan migrate:status

# Rollback migration terakhir
php artisan migrate:rollback

# Rollback semua migration lalu migrate ulang
php artisan migrate:refresh

# Refresh dan jalankan semua seeder
php artisan migrate:refresh --seed

3.3 Membuat Model Eloquent

Model adalah jembatan antara database dan kode Laravel kita. Setiap model biasanya mewakili satu tabel database.

bash
# Membuat model Post
php artisan make:model Post

# Membuat model dengan migration dan controller (recommended)
php artisan make:model Post -m

# Membuat model dengan migration, controller, factory, seeder
php artisan make:model Post -mcf

# Membuat model untuk API Resource (dengan --api)
php artisan make:model Post -m --api

Struktur Model Dasar:

php
<?php
// File: 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;  // SoftDeletes untuk fitur hapus sementara

    /**
     * Nama tabel yang terhubung dengan model ini
     * (jika nama tabel tidak sama dengan pluralisasi nama model)
     */
    protected $table = 'posts';

    /**
     * Primary key tabel (default: 'id')
     */
    protected $primaryKey = 'id';

    /**
     * Tipe data primary key (default: 'int')
     */
    protected $keyType = 'int';

    /**
     * Apakah primary key auto-increment?
     */
    public $incrementing = true;

    /**
     * Kolom yang bisa diisi (mass assignment)
     */
    protected $fillable = [
        'title',
        'content',
        'slug',
        'image',
        'category_id',
        'user_id',
        'views',
        'is_published',
        'published_at'
    ];

    /**
     * Kolom yang tidak boleh diisi (mass assignment)
     * Kebalikan dari $fillable
     */
    protected $guarded = ['id', 'created_at', 'updated_at'];

    /**
     * Kolom yang harus di-cast ke tipe data tertentu
     */
    protected $casts = [
        'is_published' => 'boolean',
        'published_at' => 'datetime',
        'views' => 'integer',
        'created_at' => 'datetime',
        'updated_at' => 'datetime'
    ];

    /**
     * Relasi ke model Category (Many to One)
     */
    public function category()
    {
        return $this->belongsTo(Category::class);
    }

    /**
     * Relasi ke model User (Many to One)
     */
    public function author()
    {
        return $this->belongsTo(User::class, 'user_id');
    }

    /**
     * Relasi ke model Comment (One to Many)
     */
    public function comments()
    {
        return $this->hasMany(Comment::class);
    }

    /**
     * Scope untuk mengambil post yang sudah dipublish
     */
    public function scopePublished($query)
    {
        return $query->where('is_published', true)
                     ->whereNotNull('published_at')
                     ->where('published_at', '<=', now());
    }

    /**
     * Scope untuk pencarian
     */
    public function scopeSearch($query, $keyword)
    {
        return $query->where('title', 'LIKE', "%{$keyword}%")
                     ->orWhere('content', 'LIKE', "%{$keyword}%");
    }

    /**
     * Accessor: mengolah data saat diambil
     */
    public function getExcerptAttribute()
    {
        return substr($this->content, 0, 100) . '...';
    }

    /**
     * Mutator: mengolah data sebelum disimpan
     */
    public function setTitleAttribute($value)
    {
        $this->attributes['title'] = $value;
        $this->attributes['slug'] = str()->slug($value);
    }
}

3.4 Operasi CRUD dengan Eloquent di Controller API

Sekarang mari kita integrasikan Model ke dalam Controller API yang sudah kita buat di artikel sebelumnya:

php
<?php
// File: app/Http/Controllers/Api/PostController.php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Post;
use App\Models\Category;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class PostController extends Controller
{
    /**
     * GET /api/posts
     * Menampilkan daftar post dengan berbagai fitur
     */
    public function index(Request $request)
    {
        // Query builder mulai dari model Post
        $query = Post::with(['category', 'author'])  // Eager loading relasi
                     ->withCount('comments');        // Menghitung jumlah komentar

        // Filter berdasarkan kategori (jika ada parameter category_id)
        if ($request->has('category_id')) {
            $query->where('category_id', $request->category_id);
        }

        // Filter berdasarkan status publish
        if ($request->has('published')) {
            if ($request->published) {
                $query->published();  // Menggunakan scopePublished()
            }
        }

        // Pencarian berdasarkan keyword
        if ($request->has('search')) {
            $query->search($request->search);  // Menggunakan scopeSearch()
        }

        // Sorting
        $sortField = $request->get('sort_by', 'created_at');
        $sortOrder = $request->get('order', 'desc');
        $query->orderBy($sortField, $sortOrder);

        // Pagination (mengambil data per halaman)
        $perPage = $request->get('per_page', 15);
        $posts = $query->paginate($perPage);

        // Return response dengan struktur yang informatif
        return response()->json([
            'success' => true,
            'message' => 'Daftar post berhasil diambil',
            'data' => $posts->items(),  // Data untuk halaman ini
            'meta' => [
                'current_page' => $posts->currentPage(),
                'last_page' => $posts->lastPage(),
                'per_page' => $posts->perPage(),
                'total' => $posts->total(),
                'from' => $posts->firstItem(),
                'to' => $posts->lastItem()
            ],
            'links' => [
                'first' => $posts->url(1),
                'last' => $posts->url($posts->lastPage()),
                'prev' => $posts->previousPageUrl(),
                'next' => $posts->nextPageUrl()
            ]
        ]);
    }

    /**
     * POST /api/posts
     * Menyimpan post baru ke database
     */
    public function store(Request $request)
    {
        // Validasi input
        $validated = $request->validate([
            'title' => 'required|max:255',
            'content' => 'required',
            'category_id' => 'required|exists:categories,id',
            'image' => 'nullable|url',  // Validasi URL gambar
            'is_published' => 'boolean'
        ]);

        // Tambahkan user_id dari user yang sedang login (nanti pakai auth)
        $validated['user_id'] = 1; // Sementara hardcode dulu

        // Buat slug dari title (otomatis di-handle oleh mutator di model)
        // Simpan ke database menggunakan Eloquent
        $post = Post::create($validated);

        // Load relasi untuk response yang lebih lengkap
        $post->load(['category', 'author']);

        return response()->json([
            'success' => true,
            'message' => 'Post berhasil dibuat',
            'data' => $post
        ], Response::HTTP_CREATED);  // 201 Created
    }

    /**
     * GET /api/posts/{id}
     * Menampilkan detail satu post
     */
    public function show($id)
    {
        // Mencari post berdasarkan ID, jika tidak ada otomatis 404
        $post = Post::with(['category', 'author', 'comments' => function($query) {
                        $query->latest();  // Komentar terbaru
                    }])
                    ->withCount('comments')
                    ->findOrFail($id);

        // Increment views (tambah jumlah pembaca)
        $post->increment('views');

        return response()->json([
            'success' => true,
            'message' => 'Detail post berhasil diambil',
            'data' => $post
        ]);
    }

    /**
     * PUT/PATCH /api/posts/{id}
     * Mengupdate post di database
     */
    public function update(Request $request, $id)
    {
        $post = Post::findOrFail($id);

        // Validasi input (sometimes berarti hanya validasi jika field ada)
        $validated = $request->validate([
            'title' => 'sometimes|required|max:255',
            'content' => 'sometimes|required',
            'category_id' => 'sometimes|required|exists:categories,id',
            'image' => 'nullable|url',
            'is_published' => 'boolean'
        ]);

        // Update data
        $post->update($validated);

        return response()->json([
            'success' => true,
            'message' => 'Post berhasil diupdate',
            'data' => $post->fresh(['category', 'author'])  // fresh() untuk mengambil data terbaru
        ]);
    }

    /**
     * DELETE /api/posts/{id}
     * Menghapus post dari database (soft delete)
     */
    public function destroy($id)
    {
        $post = Post::findOrFail($id);
        
        // Hapus post (soft delete jika menggunakan SoftDeletes)
        $post->delete();

        return response()->json([
            'success' => true,
            'message' => 'Post berhasil dihapus'
        ]);
    }

    /**
     * GET /api/posts/trashed
     * Menampilkan post yang sudah dihapus (soft delete)
     */
    public function trashed()
    {
        $trashedPosts = Post::onlyTrashed()->get();

        return response()->json([
            'success' => true,
            'data' => $trashedPosts
        ]);
    }

    /**
     * POST /api/posts/{id}/restore
     * Mengembalikan post yang dihapus
     */
    public function restore($id)
    {
        $post = Post::onlyTrashed()->findOrFail($id);
        $post->restore();

        return response()->json([
            'success' => true,
            'message' => 'Post berhasil dikembalikan',
            'data' => $post
        ]);
    }
}

3.5 Database Seeding untuk Data Dummy

    Seeder digunakan untuk mengisi database dengan data dummy, sangat berguna untuk testing dan pengembangan.

Membuat Seeder:

bash
# Membuat seeder untuk Post
php artisan make:seeder PostSeeder

# Membuat seeder untuk User (sudah ada default dari Laravel)
php artisan make:seeder UserSeeder

Mengisi Data dengan Factory dan Faker:

php
<?php
// File: database/seeders/PostSeeder.php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\Post;
use App\Models\Category;
use App\Models\User;

class PostSeeder extends Seeder
{
    public function run(): void
    {
        // Pastikan ada kategori dan user
        if (Category::count() == 0) {
            $this->call(CategorySeeder::class);
        }
        
        if (User::count() == 0) {
            $this->call(UserSeeder::class);
        }

        // Membuat 50 post dummy
        Post::factory(50)->create([
            'category_id' => Category::inRandomOrder()->first()->id,
            'user_id' => User::inRandomOrder()->first()->id,
        ]);
    }
}

Membuat Factory untuk Post:

php
<?php
// File: database/factories/PostFactory.php

namespace Database\Factories;

use App\Models\Post;
use Illuminate\Database\Eloquent\Factories\Factory;

class PostFactory extends Factory
{
    protected $model = Post::class;

    public function definition(): array
    {
        $title = $this->faker->sentence();
        
        return [
            'title' => $title,
            'slug' => \Str::slug($title),
            'content' => $this->faker->paragraphs(5, true),
            'image' => $this->faker->imageUrl(800, 400),
            'views' => $this->faker->numberBetween(0, 1000),
            'is_published' => $this->faker->boolean(80),  // 80% kemungkinan true
            'published_at' => $this->faker->dateTimeBetween('-1 year', 'now'),
            'created_at' => $this->faker->dateTimeBetween('-1 year', 'now'),
            'updated_at' => function (array $attributes) {
                return $this->faker->dateTimeBetween($attributes['created_at'], 'now');
            }
        ];
    }

    /**
     * State untuk post yang sudah dipublish
     */
    public function published(): static
    {
        return $this->state(fn (array $attributes) => [
            'is_published' => true,
            'published_at' => now(),
        ]);
    }

    /**
     * State untuk post draft
     */
    public function draft(): static
    {
        return $this->state(fn (array $attributes) => [
            'is_published' => false,
            'published_at' => null,
        ]);
    }
}

Menjalankan Seeder:

bash
# Menjalankan semua seeder
php artisan db:seed

# Menjalankan seeder spesifik
php artisan db:seed --class=PostSeeder

# Migrate refresh dan seed sekaligus
php artisan migrate:refresh --seed

3.6 Relasi Database dengan Eloquent

Eloquent memudahkan kita bekerja dengan relasi antar tabel. Berikut adalah jenis-jenis relasi yang paling umum:

1. One to Many (BelongsTo / HasMany) - Post dan Comments

php
// Di model Post
public function comments()
{
    return $this->hasMany(Comment::class);
}

// Di model Comment
public function post()
{
    return $this->belongsTo(Post::class);
}

// Penggunaan di controller
$post = Post::with('comments')->find(1);
foreach ($post->comments as $comment) {
    echo $comment->content;
}

2. Many to Many - Posts dan Tags

php
// Di model Post
public function tags()
{
    return $this->belongsToMany(Tag::class)
                ->withTimestamps();  // Menyimpan timestamps di pivot
}

// Di model Tag
public function posts()
{
    return $this->belongsToMany(Post::class);
}

// Menambah tag ke post
$post = Post::find(1);
$post->tags()->attach([1, 2, 3]);  // Menambah relasi
$post->tags()->sync([1, 2, 4]);    // Sinkronisasi (hapus yang tidak ada di array)
$post->tags()->detach(1);          // Menghapus relasi

3. HasManyThrough - Relasi melalui tabel perantara

php
// Di model User
public function postComments()
{
    // Mengambil semua komentar dari post yang dibuat user
    return $this->hasManyThrough(Comment::class, Post::class);
}

// Penggunaan
$user = User::find(1);
$comments = $user->postComments;  // Semua komentar di semua post user

3.7 Query Scope untuk Query yang Sering Digunakan

Scope membantu kita menulis query yang bersih dan reusable:

php
// Di model Post
public function scopePopular($query, $minViews = 100)
{
    return $query->where('views', '>=', $minViews)
                 ->orderBy('views', 'desc');
}

public function scopeFromCategory($query, $categoryId)
{
    return $query->where('category_id', $categoryId);
}

public function scopeRecent($query, $days = 7)
{
    return $query->where('created_at', '>=', now()->subDays($days));
}

// Penggunaan di controller
$popularPosts = Post::popular(500)  // Post dengan views > 500
                    ->fromCategory(1)  // Dari kategori ID 1
                    ->recent(30)  // 30 hari terakhir
                    ->get();

3.8 Testing API dengan Database

Sekarang mari kita test endpoint yang sudah terhubung dengan database menggunakan Postman:

1. GET /api/posts?category_id=1&search=laravel&sort_by=views&order=desc

text
Endpoint dengan berbagai parameter untuk filter, search, dan sorting

2. POST /api/posts

json
{
    "title": "Belajar Eloquent ORM di Laravel",
    "content": "Eloquent adalah ORM yang sangat powerfull...",
    "category_id": 1,
    "image": "https://example.com/image.jpg",
    "is_published": true
}

3. GET /api/posts/1
Response akan menampilkan detail post termasuk relasi category, author, comments, dan comment_count.

4. GET /api/posts?page=2
Mengambil halaman ke-2 dari hasil paginasi.

3.9 Kendala dan Solusi

Kendala yang Dihadapi:

  1. N+1 Query Problem: Saat mengambil data dengan relasi di loop, terjadi banyak query ke database.

    • Solusi: Gunakan with() untuk eager loading

    php
    // Buruk (N+1 queries)
    $posts = Post::all();
    foreach ($posts as $post) {
        echo $post->category->name;  // Query tambahan setiap loop
    }
    
    // Baik (2 queries)
    $posts = Post::with('category')->get();
  2. Mass Assignment Error: Lupa mendefinisikan $fillable di model.

    • Solusi: Selalu definisi kolom yang boleh diisi di model.

  3. Lupa Migrate: Data tidak muncul karena lupa menjalankan migration.

    • Solusi: Biasakan menjalankan php artisan migrate:fresh --seed setiap kali pull project baru.

  4. Query yang Lambat: Data besar tanpa paginasi.

    • Solusi: Selalu gunakan paginate() daripada all() untuk data banyak.


4. KESIMPULAN

    Integrasi database adalah langkah krusial dalam pengembangan API. Laravel menyediakan tiga alat yang sangat powerfull:

  1. Migration - Version control untuk database, memudahkan kolaborasi tim dan deployment

  2. Eloquent ORM - Interaksi database yang intuitif, aman dari SQL injection, dan mendukung relasi kompleks

  3. Seeder & Factory - Membuat data dummy untuk testing dengan cepat

Dengan ketiga alat ini, kita bisa membangun API yang benar-benar dinamis dengan data yang tersimpan secara permanen. Pada artikel selanjutnya, kita akan belajar tentang Validasi, Error Handling, dan API Resource untuk membuat response API kita lebih profesional dan terstruktur.


5. DAFTAR PUSTAKA

  1. Laravel Official Documentation. (n.d.). *Laravel 11.x Documentation - Database: Migrations*. https://laravel.com/docs/11.x/migrations
  2. Laravel Official Documentation. (n.d.). *Laravel 11.x Documentation - Eloquent ORM*. https://laravel.com/docs/11.x/eloquent
  3. Laravel Official Documentation. (n.d.). *Laravel 11.x Documentation - Database: Seeding*. https://laravel.com/docs/11.x/seeding
  4. Laravel Official Documentation. (n.d.). *Laravel 11.x Documentation - Database: Query Builder*. https://laravel.com/docs/11.x/queries
  5. Perwira Learning Center. (2026). Modul Pelatihan Backend Development: Laravel REST API - Database dan Eloquent.

Posting Komentar

0 Komentar