Pengantar Pendekatan Versi Schema dan Seed Data
Dalam proyek backend berbasis Go, versi schema database dan seed data bisa menjadi sumber konflik ketika tim berkembang. GORM sebagai ORM utama memberikan fleksibilitas, namun memerlukan pola tersendiri untuk memastikan perubahan schema (migrasi) dan data awal (seed) terdokumentasi, bisa dijalankan beberapa kali tanpa duplikasi, serta sesuai dengan environment. Artikel ini membahas struktur folder migrasi, skrip seed, tabel tracking versi, dan eksekusi seed otomatis saat first run di environment development/staging.
Menyiapkan Struktur Folder Migrations dan Seed
Mulailah dengan struktur folder terpisah untuk migrasi schema dan seed data. Pemisahan memudahkan tim memahami maksud perubahan mana yang memengaruhi schema versus data referensi.
Contoh struktur:
db/migrations/001_create_users.go
db/migrations/002_add_user_profile.go
db/seeds/001_roles.go
db/seeds/002_default_settings.goPenomoran memudahkan urutan eksekusi. Setiap file migrasi/seeding menyertakan fungsi yang bisa dipanggil oleh pengeksekusi utama. Dengan GORM, cukup menggunakan gorm.DB dari paket shared untuk memanggil AutoMigrate atau insert data.
Prinsip Penulisan File Migrasi
Setiap file migrasi berisi struktur yang deterministik dan bersifat idempotent. Contoh migrasi:
package migrations
import "gorm.io/gorm"
type User struct {
ID uint `gorm:"primaryKey"`
Email string `gorm:"unique;not null"`
Name string
}
func M001CreateUsers(db *gorm.DB) error {
return db.AutoMigrate(&User{})
}Gunakan AutoMigrate hanya untuk schema sederhana. Jika perubahan kompleks (misalnya indeks partial), gunakan SQL mentah dengan Exec. Kuncinya memastikan file itu bisa dijalankan berulang tanpa menghapus data.
File Seed Data yang Tersusun
Seed data tidak selalu harus dijalankan di production—cenderung untuk development/staging. GORM tidak menyediakan built-in seeder, jadi buat konvensi:
db/seeds/001_roles.gomenyisipkan data default seperti role atau permission.- Setiap seed harus memeriksa eksistensi record sebelum insert.
- Gunakan transaksi agar partial insert rollback ketika error.
package seeds
import (
"gorm.io/gorm"
"time"
)
type Role struct {
ID uint `gorm:"primaryKey"`
Name string
CreatedAt time.Time
}
func SeedRoles(db *gorm.DB) error {
roles := []string{"admin", "user"}
for _, name := range roles {
if err := db.FirstOrCreate(&Role{}, Role{Name: name}).Error; err != nil {
return err
}
}
return nil
}Dengan FirstOrCreate, kita menghindari duplikasi. Pastikan seed hanya dipanggil pada environment sesuai.
Mengelola Tabel Tracking Versi Database
Konsep paling krusial adalah memastikan setiap migrasi/seed dicatat. Buat tabel sederhana seperti:
CREATE TABLE schema_versions (
id SERIAL PRIMARY KEY,
script VARCHAR(255) UNIQUE NOT NULL,
applied_at TIMESTAMP NOT NULL DEFAULT now()
);
CREATE TABLE seed_versions (
id SERIAL PRIMARY KEY,
script VARCHAR(255) UNIQUE NOT NULL,
applied_at TIMESTAMP NOT NULL DEFAULT now()
);Tabel ini bisa dibuat manual sekali, atau difasilitasi oleh script awal. Saat eksekusi migrasi/seeding, periksa apakah script sudah ada. Bila belum, jalankan perubahan dan insert record.
Implementasi Tracking di Go
Contoh fungsi helper:
func hasRun(db *gorm.DB, table, script string) (bool, error) {
var count int64
err := db.Table(table).Where("script = ?", script).Count(&count).Error
return count > 0, err
}
func markRun(db *gorm.DB, table, script string) error {
return db.Table(table).Create(map[string]interface{}{"script": script}).Error
}Fungsi ini dipakai sebelum memanggil migrasi atau seed. Catatan penting: setidaknya satu eksekutor (misalnya file main.go) harus memanggil fungsi ini secara berurutan.
Alur Eksekusi Migrasi dan Seed
Berikut tahapan umum ketika aplikasi dijalankan:
- Inisialisasi koneksi GORM ke database.
- Pastikan tabel tracking ada (bisa dengan SQL manual atau
AutoMigrate). - Loop setiap file migrasi: jika belum dijalankan, panggil handler dan catat hasilnya.
- Loop setiap file seed hanya pada environment yang relevan: development/staging.
Penting agar seeder tidak otomatis dijalankan di production kecuali diinginkan. Gunakan variabel environment ENV atau flag run-time.
Contoh Pengaturan Runner
func RunMigrations(db *gorm.DB) error {
scripts := []struct {
name string
fn func(*gorm.DB) error
}{
{"M001_create_users", migrations.M001CreateUsers},
{"M002_add_user_profile", migrations.M002AddUserProfile},
}
for _, script := range scripts {
ran, err := hasRun(db, "schema_versions", script.name)
if err != nil {
return err
}
if ran {
continue
}
if err := db.Transaction(func(tx *gorm.DB) error {
if err := script.fn(tx); err != nil {
return err
}
return markRun(tx, "schema_versions", script.name)
}); err != nil {
return err
}
}
return nil
}
Gunakan transaksi untuk memastikan walaupun migrasi gagal, status tidak tercatat.
Seed Otomatis Saat First Run Development dan Staging
Setelah migrasi selesai, seeder dijalankan hanya jika environment mendukung. Strateginya:
- Gunakan flag
SKIP_SEEDuntuk mencegah duplikasi saat tidak diperlukan. - Catat seed via tabel
seed_versionsseperti migrasi. - Untuk development, jalankan semua seed agar developer tidak perlu memasukkan data manual.
- Untuk staging, bisa pilih subset seed (misal hanya data konfigurasi) untuk menjaga konsistensi.
Contoh implementasi conditional:
func RunSeeds(db *gorm.DB, env string) error {
if env == "production" {
return nil
}
scripts := []struct {
name string
fn func(*gorm.DB) error
}{
{"S001_roles", seeds.SeedRoles},
{"S002_settings", seeds.SeedSettings},
}
for _, script := range scripts {
ran, err := hasRun(db, "seed_versions", script.name)
if err != nil {
return err
}
if ran {
continue
}
if err := db.Transaction(func(tx *gorm.DB) error {
if err := script.fn(tx); err != nil {
return err
}
return markRun(tx, "seed_versions", script.name)
}); err != nil {
return err
}
}
return nil
}
Dengan pendekatan ini, saat developer menjalankan make run atau go run cmd/app, database akan menangani versi schema dan seed secara deterministik.
Debugging dan Trade-off
Trade-off: Sistem manual semacam ini memberikan kendali penuh tapi membutuhkan pemeliharaan (penomoran, urutan). Untuk proyek besar, Anda bisa menggunakan tool migrasi seperti golang-migrate dan integrasikan dengan GORM, lalu hanya gunakan GORM untuk model mapping.
Kesalahan umum:
- Lupa catat versi menyebabkan migrasi dijalankan dua kali.
- Seed tanpa
FirstOrCreateatau cek kondisi menghasilkan duplikasi. - Menggabungkan seeder production dengan development tanpa filtering environment.
Debugging tips:
- Periksa tabel
schema_versions/seed_versionsuntuk melihat script mana yang telah dijalankan. - Gunakan logger GORM untuk melihat SQL terkini (
db.Debug()). - Uji migrasi/seed di database kopian lokal sebelum deploy.
Kesimpulan
Pengelolaan versi schema dan seed data dengan GORM memerlukan konvensi folder, penomoran skrip, tabel tracking, dan eksekusi environment-aware. Sistem ini memberi kepastian bahwa migrasi dijalankan satu kali, seed tidak menggandakan data, dan developer baru dapat memulai dengan konfigurasi yang konsisten. Langkah berikutnya adalah mengautomasi runner ini di pipeline CI/CD agar lingkungan staging tetap sinkron dengan schema terbaru.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!