Jumat, 18 August 2023

Tutorial Laravel Pruning: Menjaga Database Anda Tetap Ringan dan Efisien

Bosan dengan kekacauan data yang tak terkelola di database Anda? Laravel Pruning adalah solusi yang Anda cari! Pelajari cara mengautomasi proses pemangkasan data yang tidak lagi diperlukan dengan panduan lengkap ini.

Database
Laravel

Dalam artikel ini kita akan belajar tentang schedule / penjadwalan pada laravel, saya akan berusaha sebisa mungkin untuk menjelaskan case ini agar Anda dapat mengerti dengan baik.

Instal Laravel

Kita akan memulai dari awal di sini, jadi yang pertama, mari kita lakukan instalasi Laravel. Buka terminal Anda dan jalankan perintah berikut.

Terminal
laravel new schedule

Jika Anda tidak dapat menginstal Laravel menggunakan laravel cli, Anda dapat menggunakan Composer seperti berikut:

Terminal
composer create-project laravel/laravel schedule

Jika sudah, sekarang kita akan menjalankan perintah migrate, namun sebelum itu, pastikan Anda sudah memiliki database dan kemudian sesuaikan pada file .env.

.env
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=schedule
DB_USERNAME=root
DB_PASSWORD=

Setelah itu, mari kita lakukan migrate dengan menjalan perintah berikut.

Terminal
php artisan migrate

Factories dan Seeders

Oia, sepertinya kita memerlukan data untuk itu. Oleh karena itu, mari kita lakukan itu dengan factory. By default, Laravel akan membawa factory untuk User yang itu bisa dilihat di database/factories/UserFactory.php, dan jika kita buka file nya kurang lebih akan seperti ini:

.../factories/UserFactory.php
namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
 */
class UserFactory extends Factory
{
    public function definition(): array
    {
        return [
            'name' => fake()->name(),
            'email' => fake()->unique()->safeEmail(),
            'email_verified_at' => now(),
            'password' => bcrypt('password'),
            'remember_token' => Str::random(10),
        ];
    }

    public function unverified(): static
    {
        return $this->state(fn (array $attributes) => [
            'email_verified_at' => null,
        ]);
    }
}

Sebelum kita menjalankan nya, saya akan modifikasi dulu terlebih dahulu untuk field created_at and updated_at nya jadi random.

database/factories/UserFactory.php
public function definition(): array
{
    return [
        'name' => fake()->name(),
        'email' => fake()->unique()->safeEmail(),
        'email_verified_at' => now(),
        'password' => bcrypt('password'),
        'remember_token' => Str::random(10),
        'created_at' => $createdAt = collect([
            now()->subMonth(2),
            now()->subMonth(1),
            now()->subWeek(2),
            now()->subWeek(1),
            now()
        ])->random(1)->first(),
        'updated_at' => $createdAt,
    ];
}

Setalah itu, silakan hilangkan komentar factory pada file database/seeders/DatabaseSeeder.php.

.../seeders/DatabaseSeeder.php {11}
namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        \App\Models\User::factory(30)->create();

        // \App\Models\User::factory()->create([
        //     'name' => 'Test User',
        //     'email' => '[email protected]',
        // ]);
    }
}

Jika sudah, sekarang kita buka terminal kembali, dan sekarang kita akan menjalankan perintah untuk seeder nya seperti:

php artisan migrate:fresh --seed

Dan jika sudah, pada tabel users seharusnya sudah terbuat 30 user dengan created_at dan updated_at yang berbeda. Untuk melihatnya, Anda bisa langsung pergi ke tinker dengan menjalankan perintah berikut:

Terminal
php artisan tinker

Dan kemudian pluck created_at nya seperti:

Psy Shell v0.11.9 (PHP 8.1.11 — cli) by Justin Hileman
> App\Models\User::pluck('created_at');

Dan harusnya output yang keluar pastinya seperti berikut:

= Illuminate\Support\Collection {#4639
    all: [
      Illuminate\Support\Carbon @1669116509 {#4636
        date: 2022-11-22 11:28:29.0 UTC (+00:00),
      },
      Illuminate\Support\Carbon @1667906909 {#4635
        date: 2022-11-08 11:28:29.0 UTC (+00:00),
      },
      Illuminate\Support\Carbon @1668511709 {#4634
        date: 2022-11-15 11:28:29.0 UTC (+00:00),

Perhatikan tanggal tersebut; Anda akan melihat perbedaannya bila dilihat secara teliti. Namun, itu saja tidak cukup. Di sini, kami akan memberikan contoh cara menyusun jadwal untuk menghapus pengguna yang tidak memverifikasi email mereka, yang telah terdaftar setengah bulan lalu.

Karena itu, kami akan langsung menggunakan tinker untuk membuat factory dengan state unverified yang disediakan oleh Laravel.

.../factories/UserFactory.php
public function unverified(): static
{
    return $this->state(fn (array $attributes) => [
        'email_verified_at' => null,
    ]);
}

Nah untuk itu, buka silakan masuk lagi ke tinker dengan menjalankan php artisan tinker, setelah itu lakukan perintah berikut.

> App\Models\User::factory(30)->unverified()->create();

Setelah itu, maka kita akan mempunyai total 60 pengguna, 30 yang verified dan 30 lagi tidak verified. Verified akan di lihat dari null nya field dari email_verified_at nya.

Sehingga sekarang, jika kita buat di tinker perintah:

> App\Models\User::whereNull('email_verified_at')->count();
= 30

Model Pruning

Pertama-tama, mari kita buat fitur pemangkasan (pruning) pada tabel pengguna, sehingga penghapusan akan dilakukan secara otomatis jika pengguna tidak melakukan verifikasi dalam jangka waktu setengah bulan, sebagai contoh.

Untuk menggunakan fitur pruning ini, kita perlu menambahkan trait Prunable. Caranya, buka model User.php dan masukkan trait tersebut seperti berikut:

app/Models/User.php
...
use Illuminate\Database\Eloquent\Prunable;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable, Prunable;

Setelah itu, kita akan menambahkan metode baru pada model ini dengan nama prunable seperti:

app/Models/User.php
class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable, Prunable;

    protected $fillable = ['name', 'email', 'password'];

    protected $hidden = ['password', 'remember_token'];

    protected $casts = ['email_verified_at' => 'datetime'];

    public function prunable()
    {
        return static::whereNull('email_verified_at')
                ->where('created_at', '<=', now()
                ->subMonth());
    }
}

Isi dari query tersebut bertujuan untuk memastikan bahwa pengguna yang tidak melakukan verifikasi selama satu bulan terakhir akan dihapus. Sekarang, Anda dapat membuka file app/Console/Kernel.php dan menambahkan jadwal tersebut tepat di dalam metode schedule, seperti berikut:

app/Console/Kernel.php
class Kernel extends ConsoleKernel
{
    protected function schedule(Schedule $schedule): void
    {
        $schedule->command('model:prune')->weekly();
        // $schedule->command('inspire')->hourly();
    }
    
    protected function commands(): void{...}
}

Setelah itu, kita akan memeriksa apakah schedule itu sudah masuk atau belum di dalam tasklist nya dengan menjalankan perintah berikut:

Terminal
php artisan schedule:list

Maka harusnya, output yang dihasilkan kurang lebih akan seperti ini:

0 0 * * 0  php artisan model:prune ........................ Next Due: 4 days from now

Nah, untuk menjalankannya, kita menggunakan perintah schedule:run seperti:

Terminal
php artisan schedule:run

Dan harusnya, yang muncul adalah seperti:

INFO  No scheduled commands are ready to run.

Arti dari informasi tersebut adalah "belum ada jadwal yang akan dijalankan". Hal ini masuk akal, mengingat jika kita melihat daftar jadwal yang ada, itu akan dijalankan 4 hari lagi dari sekarang. Oleh karena itu, kita dapat memodifikasinya dengan mengganti weekly() dengan everyMinute() hanya untuk contoh ini.

app/Console/Kernel.php
class Kernel extends ConsoleKernel
{
    protected function schedule(Schedule $schedule): void
    {
        $schedule->command('model:prune')->everyMinute();
        // $schedule->command('inspire')->hourly();
    }
    
    protected function commands(): void{...}
}

Pastinya jika ini di production, harusnya untuk case ini kita bisa jalankan sekali seminggu saja, namun untuk test saja, kita akan buat dia everyMinute agar kita yang mana telah di hapusnya.

Menjalankan Schedule di Lokal

Sekarang, untuk menjalankan schedule tersebut di background, kita bisa menggunakan perintah schedule:work seperti berikut:

Terminal
php artisan schedule:work

Sekarang jika Anda lihat outputnya setelah 1 menit akan seperti berikut:

 INFO  Running schedule tasks every minute.

  2022-11-22 11:55:00 Running ['artisan' model:prune] ............. 234ms DONE
  ⇂ '/opt/homebrew/Cellar/php/8.1.11/bin/php' 'artisan' model:prune > '/dev/null' 2>&1

Dan perintah tersebut akan terus jalan setiap 1 menit. Maka nya sekarang di saya sudah 2 kali:

  INFO  Running schedule tasks every minute.

  2022-11-22 11:55:00 Running ['artisan' model:prune] ............. 234ms DONE
  ⇂ '/opt/homebrew/Cellar/php/8.1.11/bin/php' 'artisan' model:prune > '/dev/null' 2>&1

  2022-11-22 11:56:00 Running ['artisan' model:prune] ............. 183ms DONE
  ⇂ '/opt/homebrew/Cellar/php/8.1.11/bin/php' 'artisan' model:prune > '/dev/null' 2>&1

Anda bisa tekan ctrl+c untuk menghentikan perintahnya. Dan sekarang, mari kita kembali ke tinker untuk menjalakan perintah yang tadi yaitu melihat jumlah yang tidak verifikasi tinggal berapa:

Terminal
php artisan tinker

Dan jalankan query seperti yang tadi:

Terminal
User::whereNull('email_verified_at')->count()
= 20

Karena tadi kita buat random, maka hasilnya harusnya beda dengan Anda, kalau di saya hasilnya 20 seperti yang Anda lihat di atas, mungkin Anda bisa saja entah berapa, karena tergantung field created_at yang telah dibuat factory tadi.

Pastinya Anda akan menggunakan php artisan schedule:run pada saat production mode.

Dalam artikel ini, kita hanya memperaktekkan everyMinute, namun sebenarnya itu terserah Anda. Seperti yang saya bilang, harusnya untuk case ini, biasa saya hanya melakukan weekly dari pada everyMinute, karena jika ini terus-terus di jalankan, beban pada server akan semakin besar.

Daftar Penjadwalan

Berikut ini adalah metode-metode yang bisa Anda gunakan untuk pilihan waktu menjalankannya.

Metode Deskripsi
->cron('* * * * *'); Menjalankan schedule dengan pilihan waktu sendiri
->everyMinute(); Jalankan tugas setiap menit
->everyTwoMinutes(); Jalankan tugas setiap dua menit
->everyThreeMinutes(); Jalankan tugas setiap tiga menit
->everyFourMinutes(); Jalankan tugas setiap empat menit
->everyFiveMinutes(); Jalankan tugas setiap lima menit
->everyTenMinutes(); Jalankan tugas setiap sepuluh menit
->everyFifteenMinutes(); Jalankan tugas setiap lima belas menit
->everyThirtyMinutes(); Jalankan tugas setiap tiga puluh menit
->hourly(); Jalankan tugas setiap jam
->hourlyAt(17); Jalankan tugas setiap jam pada 17 menit lewat satu jam
->everyOddHour(); Jalankan tugas setiap jam ganjil
->everyTwoHours(); Jalankan tugas setiap dua jam
->everyThreeHours(); Jalankan tugas setiap tiga jam
->everyFourHours(); Jalankan tugas setiap empat jam
->everySixHours(); Jalankan tugas setiap enam jam
->daily(); Jalankan tugas setiap hari pada tengah malam
->dailyAt('13:00'); Jalankan tugas setiap hari pukul 13:00
->twiceDaily(1, 13); Jalankan tugas setiap hari pada pukul 1:00 dan 13:00
->twiceDailyAt(1, 13, 15); Jalankan tugas setiap hari pada 1:15 & 13:15
->weekly(); Jalankan tugas setiap hari Minggu pukul 00:00
->weeklyOn(1, '8:00'); Jalankan tugas setiap minggu pada hari Senin pukul 8:00
->monthly(); Jalankan tugas pada hari pertama setiap bulan pukul 00:00
->monthlyOn(4, '15:00'); Jalankan tugas setiap bulan pada tanggal 4 pukul 15:00
->twiceMonthly(1, 16, '13:00'); Jalankan tugas setiap bulan pada tanggal 1 dan 16 pukul 13:00
->lastDayOfMonth('15:00'); Jalankan tugas pada hari terakhir bulan itu pukul 15:00
->quarterly(); Jalankan tugas pada hari pertama setiap kuartal pukul 00:00
->quarterlyOn(4, '14:00'); Jalankan tugas setiap kuartal pada tanggal 4 pukul 14:00
->yearly(); Jalankan tugas pada hari pertama setiap tahun pukul 00:00
->yearlyOn(6, 1, '17:00'); Jalankan tugas setiap tahun pada tanggal 1 Juni pukul 17:00
->timezone('America/New_York'); Tetapkan zona waktu untuk tugas tersebut

Metode di atas itu semua dari dokumentasi yang sudah saya artikan ke bahasa.

Hati - Hati

Pastikan untuk command schedule nya sudah cocok dengan crontab nya. Jika kalian menggunakan model 1/2 jam seperti misalnya menjalankan 04:30, maka pastikan Anda juga sudah yakin bahwa crontab nya jalan setiap 30 menit.

$schedule->command('backup:run --only-db')->daily()->at('04.30');

Karena jika crontab jalan setiap 1 jam, maka schedule di atas tidak akan pernah jalan. Jadi pastikan Anda telah membuat sinkronisasi yang pasti antara crontab dan juga perintah schedule nya.

Penting Untuk Dipahami

Bayangkan Anda memiliki beberapa tugas terjadwal, di mana salah satunya memiliki banyak tugas yang memerlukan waktu lama.

Secara default, jadwal akan menjalankan tugas-tugas tersebut tergantung pada urutan penempatan tugas yang didefinisikan. Jadi, jika kita memiliki tugas A, B, C yang dijadwalkan pada waktu yang sama, maka mereka akan dijalankan berurutan dari yang pertama.

Misalkan B memiliki tugas yang membutuhkan waktu sangat lama, maka C harus menunggu hingga B selesai. Untuk mengatasi hal ini, kita bisa menambahkan metode runInBackground setelah waktu yang ditentukan, seperti:

app/Console/Kernel.php
$schedule->command('analytics:report')
         ->daily()
         ->runInBackground();

Dengan demikian, C tidak akan perlu menunggu hingga B selesai untuk mulai menjalankan tugasnya.

Produksi

Jika kalian menggunakan Laravel forge, maka hal tersebut akan sangat mudah untuk di lakukan pada menu scheduler.

Laravel forge

Dan jika kalian tidak menggunakan Laravel forge, harusnya kalian bisa menggunakan Crontab Jika Anda tidak menggunakan Laravel Forge, Anda dapat menggunakan Crontab untuk menjalankan jadwal tersebut.

Kesimpulan

Fitur pemangkasan (pruning) di Laravel memungkinkan penghapusan otomatis data yang tidak lagi diperlukan dari database, seperti pengguna yang tidak melakukan verifikasi email dalam jangka waktu tertentu, misalnya setengah bulan.

Untuk mengimplementasikannya, diperlukan penambahan trait Prunable pada model yang relevan, seperti User.php. Pruning ini membantu menjaga database agar tetap bersih dan efisien dengan menghilangkan entri yang sudah tidak relevan atau aktif.


Semoga artikel ini bermanfaat untuk kita semua. Jika ada kesalahan kata, saya mohon maaf. Saya Irsyad, sampai jumpa lagi.