Rabu, 11 August 2021

Penggunaan Relasi One To Many dalam Laravel

Dalam artikel ini kita akan kupas tuntas tentang bagaimana cara mengimplementasikan relasi One to Many di Laravel.

Laravel
Database

Intro

"One to Many" adalah salah satu tipe relasi paling umum dalam basis data relasional. Dalam relasi ini, satu baris dalam tabel bisa memiliki banyak baris yang berhubungan dengannya di tabel lain, tetapi sebuah baris di tabel kedua hanya bisa berhubungan dengan satu baris di tabel pertama.

Contoh

Sebagai contoh, pertimbangkan skenario di mana Anda memiliki tabel 'Authors' dan 'Books'. Seorang penulis dapat menulis banyak buku, tetapi setiap buku hanya memiliki satu penulis. Ini adalah relasi "One to Many" di mana satu penulis (One) dapat memiliki banyak buku (Many).

Struktur Tabel

Mari kita lihat bagaimana struktur tabel mungkin tampak dalam contoh ini:

Tabel categories:

id name
1 Mystery
2 Horror

Tabel books:

id title category_id
1 Murder on the Orient Express 1
2 The Shining 2
3 Endless Night 1
4 Pet Sematary 2

Dalam kasus ini, kolom category_id pada tabel books adalah kunci asing (foreign key) yang menghubungkan buku ke kategorinya. Setiap buku mengacu pada satu baris di tabel categories.

Relasi semacam ini memungkinkan kita untuk membuat query yang efisien dan mudah dimengerti untuk mengambil data yang terkait. Misalnya, kita dapat dengan mudah mengambil semua buku yang telah dikategorikan dalam 1 klompok.

Relasi "One to Many" sangat penting dalam pembuatan aplikasi berbasis database karena memungkinkan kita untuk menghindari duplikasi data dan menjaga integritas data.

Laravel Relationship

Untuk merepresentasikan relasi ini dalam Laravel, kita akan membuat dua model: satu untuk Kategori dan satu lagi untuk Buku. Di dalam model Kategori, kita akan mendefinisikan metode yang mengembalikan relasi ke Buku. Ini dilakukan dengan memanggil metode hasMany() pada model Kategori dan meneruskan model Buku sebagai argumen.

Di sisi lain, di dalam model Buku, kita akan mendefinisikan metode yang mengembalikan relasi ke Kategori. Ini dilakukan dengan memanggil metode belongsTo() pada model Buku dan meneruskan model Kategori sebagai argumen.

Dengan mendefinisikan relasi ini, Laravel sekarang dapat secara otomatis menangani interaksi antara kategori dan buku. Misalnya, kita dapat dengan mudah mendapatkan semua buku dalam suatu kategori atau menemukan kategori dari buku tertentu. Laravel mengurus semua detail yang rumit di belakang layar, memungkinkan kita untuk fokus pada logika bisnis aplikasi kita.

Mulai Dengan Model

Di dalam laravel, kita bisa membuat model sekalian dengan migration nya. Maka untuk itu, kita bisa menjalankan perintah berikut.

php artisan make:model Category -m

Perintah itu akan memberikan kita 2 file sekaligus, yang pertama adalah app/Models/Category.php, dan yang satunya lagi adalah file migration nya yang berada di database/migrations/2021_07_16_215356_create_categories_table.php.

Sebelum kita melangkah lebih jauh kesana, disini saya ingin memastikan bahwa ketika kita melakukan instalasi laravel, maka kita akan diberikan tabel bawaan seperti tabel users. Laravel sangat detail sekali karena menganggap kita pasti ingin membuat sebuah aplikasi yang itu pasti memerlukan tabel users. Itulah mengapa tabel users ada by default.

Kenapa saya menjelaskan tentang tabel users, dikarenakan pada tabel selanjutnya yaitu books akan kita relasikan dengan tabel users tadi. Seakan-akan tabel users akan menjadi seoarang penulis dari buku itu. Dan pastinya Anda harus tau, bahwa relasi yang kita implementasikan ke dalam tabel categories akan sama dengan tabel users.

Mari kita lanjut untuk membuat tabel books nya dengan menjalankan perintah berikut:

php artisan make:model Book -m

Lagi-lagi disini kita juga menggunakan flag -m agar model ini otomatis membuat migration nya.

Migration

Silakan buka file migration yang pertama kali kita buat yaitu 2021_07_16_215356_create_categories_table.php dan modifikasi isinya seperti:

../../2021_07_16_215356_create_categories_table.php
public function up()
{
    Schema::create('categories', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->string('slug');
        $table->timestamps();
    });
}

Setelah itu, silakan buka file migration yang kedua kita buat yaitu 2021_07_16_215433_create_books_table.php. Buka file nya dan silakan modifikasi seperti:

../../2021_07_16_215433_create_books_table.php
public function up()
{
    Schema::create('books', function (Blueprint $table) {
        $table->id();
        $table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
        $table->bigInteger('category_id');
        $table->string('title');
        $table->text('description');
        $table->timestamps();
    });
}

Jika Anda perhatikan, bahwa disini kita memastikan bahwa ketika penulisnya di hapus, maka harusnya buku nya juga ikut terhapus. Tapi tidak dengan kategori pastinya. Karena jika kategori di hapus, kita tentu tidak mau menghapus kategorinya. Sengaja saya buat seperti itu, agar nanti Anda mengerti bagaimana mengatasi errornya.

Migrate

Setelah kedua migration itu selesai di edit, maka kita bisa menjalankan yang namanya migrate dengan menjalankan perintah berikut:

php artisan migrate

Dengan perintah itu, maka akan terbuat ke database yang Anda miliki saat ini.

Relasi Dalam Model

Belongs To

Pertama, silakan buka model app/Models/Book.php, karena model ini akan berelasi ke 2 tabel yaitu users dan categories, maka kita menggunakan method yang namanya belongsTo.

app\Models\Book.php
public function category(): BelongsTo
{
    return $this->belongsTo(Category::class, 'category_id');
}

public function user(): BelongsTo
{
    return $this->belongsTo(User::class, 'user_id');
}

Ada BelongsTo yang saya tambah setelah nama relasinya. Itu hanya istilah return type dari method tersebut. Silakan import class nya di bawah namespace seperti:

app\Models\Book.php
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

HasMany

Kemudian buka model app/Models/Category.php dan mari kita tambahkan relasi ke tabel books nya seperti:

app/Models/Category.php
use Illuminate\Database\Eloquent\Relations\HasMany; // tambahkan di bawah namespace

public function books(): HasMany
{
    return $this->hasMany(Book::class, 'category_id');
}

Setelah itu, buka juga model app/Models/User.php dan tambahkan relasi yang sama seperti:

app/Models/User.php
use Illuminate\Database\Eloquent\Relations\HasMany; // tambahkan di bawah namespace

public function books(): HasMany
{
    return $this->hasMany(Book::class, 'user_id');
}

Jika Anda perhatikan baik-baik, ada kode yang sama disini, hanya saja mereka berbeda file. Nanti kita akan coba untuk refactor. Untuk sekarang, pastikan Anda sudah paham dulu mengenai konsep ini.

key category_id dan user_id adalah foreign key yang ada di tabel itu, jadi istilanya mereka akan kita relasikan dari tabel itu.

Jika Anda ingin mengetahui tentang ini lebih jauh, Anda bisa lihat video di bawah ini:

Tampilkan Data

Harusnya Anda sudah pasti mengetahui bagaimana cara menampilkan di Laravel dengan menggunakan table.

routes/web.php
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\DB;

Route::get('/', function () {
    return DB::table('books')->get();
});

Oia disini saya sudah menganggap bahwa Anda sudah mempunyai beberapa data di database, jika Anda tidak mengetahui caranya, maka Anda bisa langsung menonton video di bawah ini:

Terkait tadi cara kita menampilkan semua books menggunakanan DB Facade, karena kita sudah mempunyai model, maka kita menampilkan semua bukunya dengan model yang telah kita buat tadi.

routes/web.php
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return \App\Models\Book::query()->get();
});

Dengan begitu, jauh lebih simpel dan hasilnya akan tetap sama saja. Pada saat ini, kita hanya akan mendapatkan data yang kurang lebih seperti ini:

[
  {
    "id": 1,
    "user_id": 1,
    "category_id": 1,
    "title": "consequuntur",
    "description": "Iusto et qui nostrum cum. Maxime et hic nihil ut itaque numquam consequuntur.",
    "created_at": "2023-07-16T15:28:13.000000Z",
    "updated_at": "2023-07-16T15:28:13.000000Z"
  },
  {
    "id": 2,
    "user_id": 1,
    "category_id": 2,
    "title": "in",
    "description": "Enim repellat distinctio id eius. Rerum aliquam ad veniam ut. Nostrum rerum qui et rerum mollitia rerum.",
    "created_at": "2023-07-16T15:28:13.000000Z",
    "updated_at": "2023-07-16T15:28:13.000000Z"
  },
  ...
]

Eager Loading

Yang mana pada saat ini, kita tidak mendapatkan informasi seperti penulis dan juga kategorinya. Untuk menambahkan nya, kita bisa menggunakan teknik yang nama nya eager loading.

Route::get('/', function () {
    return \App\Models\Book::query()
        ->with(['category', 'user'])
        ->get();
});

Dengan begitu, maka kita akan mendapatkan hasil seperti:

[
  {
    "id": 1,
    "user_id": 1,
    "category_id": 1,
    "title": "consequuntur",
    "description": "Iusto et qui nostrum cum. Maxime et hic nihil ut itaque numquam consequuntur.",
    "created_at": "2023-07-16T15:28:13.000000Z",
    "updated_at": "2023-07-16T15:28:13.000000Z",
    "category": {
      "id": 1,
      "name": "autem",
      "slug": "autem",
      "created_at": "2023-07-16T15:28:13.000000Z",
      "updated_at": "2023-07-16T15:28:13.000000Z"
    },
    "user": {
      "id": 1,
      "name": "Jodie Lehner DDS",
      "email": "[email protected]",
      "email_verified_at": "2023-07-16T15:28:13.000000Z",
      "created_at": "2023-07-16T15:28:13.000000Z",
      "updated_at": "2023-07-16T15:28:13.000000Z"
    }
  },
  ...
]

Pilih Kolom Yang Penting

Baik, namun ada beberapa yang harus Anda fikirkan, masak ia semua informasi harus di tunjukkan. Harusnya tidak, karena itu juga mempengaruhi kinerja dari aplikasi Anda. Untuk itu, kita bisa memodifikasi query nya dengan cara seperti:

\App\Models\Book::query()
        ->select(['id', 'user_id', 'category_id', 'title', 'description'])
        ->with(['category:id,name,slug', 'user:id,name'])
        ->get();

Dengan begitu, kita akan mendapatkan hasil seperti:

[
  {
    "id": 1,
    "user_id": 1,
    "category_id": 1,
    "title": "consequuntur",
    "description": "Iusto et qui nostrum cum. Maxime et hic nihil ut itaque numquam consequuntur.",
    "category": {
      "id": 1,
      "name": "autem",
      "slug": "autem"
    },
    "user": {
      "id": 1,
      "name": "Jodie Lehner DDS"
    }
  },
  ...
]

View

Jika semua sudah kita ketahui, maka kita bisa lanjut untuk langsung menggunakan views. Pertama sekali, saya akan merubah route ini menjadi seperti:

Route::get('/', function () {
    return view('books.index', [
        'books' => \App\Models\Book::query()
            ->select(['id', 'user_id', 'category_id', 'title', 'description'])
            ->with(['category:id,name,slug', 'user:id,name'])
            ->get()
    ]);
});

Setelah itu, mari kita buat folder baru dengan nama books dan di dalamnya buat file dengan nama index.blade.php. Di dalam nya silakan loop seperti:

.../views/books/index.blade.php
<script src="https://cdn.tailwindcss.com"></script>
<div class="grid grid-cols-3 gap-6">
    @foreach($books as $book)
        <div class="p-4">
            <h2 class="text-2xl font-bold">{{ $book->title }}</h2>
            <p>{{ $book->description }}</p>
            <hr class="my-5"/>
            <div>
                Penulis: <strong>{{ $book->user->name }}</strong>
            </div>
            <div>
                Kategori: <strong>{{ $book->category->name }}</strong>
            </div>
        </div>
    @endforeach
</div>

Dengan begitu, kita akan mendapatkan hasil yang sesuai dengan kemauan kita.

Tampilkan dari Relasinya

Jika Anda ingin menampilkan semua buku dari seorang penulis, maka bisa dengan mudah membuat query seperti:

Route::get('books/author/{user}', function (App\Models\User $user) {
    return \App\Models\Book::query()
        ->select(['id', 'category_id', 'user_id', 'title', 'description'])
        ->whereBelongsTo($user, 'user')
        ->with(['category:id,name,slug'])
        ->get();
});

Dengan begitu, kita bisa mengunjungi url http://localhost:8000/books/author/2 kita akan mendapatkan hasil seperti:

[
  {
    "id": 6,
    "category_id": 6,
    "title": "voluptatem",
    "description": "Porro rerum perferendis ratione sequi. Esse eveniet ratione est molestias quo est qui. Occaecati consequatur minus id dolore ad porro recusandae.",
    "category": {
      "id": 6,
      "name": "fuga",
      "slug": "fuga"
    }
  },
  {
    "id": 7,
    "category_id": 7,
    "title": "rem",
    "description": "Veritatis tempore sed aut et quo est dolores. Doloremque hic et aut deleniti accusantium omnis. Ea nam vel est incidunt esse. Natus quis officiis dicta modi id et aspernatur.",
    "category": {
      "id": 7,
      "name": "expedita",
      "slug": "expedita"
    }
  },
  ...
]

Atau dengan cara lain, kita bisa memanggilnya melalui relasi yang ada pada model user tadi seperti:

return $user->books()
    ->select(['id', 'category_id', 'user_id', 'title', 'description'])
    ->with(['category:id,name,slug'])
    ->get();

Dengan begitu kita akan mendapatkan hasil yang sama. Dan harusnya, Anda akan sudah mengetahui bagaimana cara menampilkan berdasarkan dari kategori. Karena mereka menggunakan konsep relasi yang sama.

Refactoring

Perhatikan pada model User dan juga Category, ada kode yang duplikat yaitu relasi untuk tabel buku. Oleh karena itu, kita bisa membuat nya ke dalam trait. Silakan buat 1 folder tepat di dalam folder Models dengan nama Traits, dan didalamnya silakan buat 1 file dengan nama HasBooks. Kemudian, isinya silakan buat seperti ini:

app/Models/Traits/HasBooks.php
namespace App\Models\Traits;

use App\Models\Book;
use Illuminate\Database\Eloquent\Relations\HasMany;

trait HasBooks
{
    public function books(): HasMany
    {
        return $this->hasMany(Book::class);
    }
}

Oia, perhatikan baik-baik, kita disini bisa menggunakan traits, karena kita mengikuti aturan yang dibuat oleh laravel, yaitu akan menggap bahwa foreign key dari method itu adalah nama model yang diikuti dengan _id. Jika seandainya kita memakai trait ini di user, maka laravel akan otomatis menganggap bahwa foreign key dari method books itu adalah user_id, begitu juga dengan category.

Setelah itu, silakan hapus method books yang ada pada model User dan juga Category. Dan tambahkan trait nya seperti:

app/Models/User.php
//...
use App\Models\Traits\HasBooks;

class User extends Authenticatable
{
    use HasBooks;
    use HasApiTokens;
    use HasFactory;
    use Notifiable;
    
    protected $fillable = [
        'name',
        'email',
        'password',
    ];
    protected $hidden = [
        'password',
        'remember_token',
    ];
    protected $casts = [
        'email_verified_at' => 'datetime',
        'password' => 'hashed',
    ];
}

Kemudian begitu juga dengan model Category.

app/Models/Category.php
use App\Models\Traits\HasBooks;

class Category extends Model
{
    use HasBooks;
    use HasFactory;

    protected $fillable = [
        'name',
        'slug',
    ];
}

Dengan begitu, maka kita sudah melakukan hal yang bagus yaitu menghindari duplikasi.

"Explicit" lebih baik dari pada "Implicit"

Kesimpulan

Menggunakan relasi "One to Many" di Laravel adalah praktek yang penting dan umum dalam pengembangan aplikasi yang berinteraksi dengan basis data relasional. Dengan memahami dan menerapkan relasi ini, kita dapat merancang struktur data yang lebih efisien dan logis, yang pada akhirnya akan meningkatkan kinerja aplikasi kita.

Selain itu, fitur relasi yang disediakan oleh Laravel memudahkan kita dalam penanganan dan manipulasi data antar tabel. Dengan demikian, kita dapat lebih fokus pada logika bisnis aplikasi dan meningkatkan produktivitas pengembangan kita.

Terakhir, memahami relasi "One to Many" juga penting sebagai langkah awal untuk memahami jenis relasi lainnya seperti "Many to Many", "Polymorphic Relations", dan lainnya yang ditawarkan oleh Laravel.

Semoga artikel ini bermanfaat untuk Anda dan begitu juga dengan saya. Saya irsyad, sampai jumpa lagi.