373 lines
9.2 KiB
Go
373 lines
9.2 KiB
Go
package seeders
|
|
|
|
import (
|
|
"encoding/csv"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// SeederConfig berisi konfigurasi untuk seeding
|
|
type SeederConfig struct {
|
|
CSVPath string
|
|
TableName string
|
|
ColumnMap map[string]string // mapping CSV header ke field struct
|
|
SkipHeader bool
|
|
BatchSize int
|
|
DryRun bool
|
|
DeleteBefore bool
|
|
}
|
|
|
|
// MasterSeeder adalah struct utama untuk seeding fleksibel
|
|
type MasterSeeder struct {
|
|
db *gorm.DB
|
|
config SeederConfig
|
|
}
|
|
|
|
// NewMasterSeeder membuat instance seeder baru
|
|
func NewMasterSeeder(db *gorm.DB, config SeederConfig) *MasterSeeder {
|
|
if config.BatchSize == 0 {
|
|
config.BatchSize = 100
|
|
}
|
|
return &MasterSeeder{
|
|
db: db,
|
|
config: config,
|
|
}
|
|
}
|
|
|
|
// SeedFromCSV melakukan seeding dari file CSV
|
|
func (s *MasterSeeder) SeedFromCSV(model interface{}) error {
|
|
if s.config.CSVPath == "" {
|
|
return fmt.Errorf("CSV path is required")
|
|
}
|
|
|
|
file, err := os.Open(s.config.CSVPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open CSV file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
reader := csv.NewReader(file)
|
|
reader.Comma = ','
|
|
|
|
// Baca header
|
|
headers, err := reader.Read()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read CSV header: %w", err)
|
|
}
|
|
|
|
log.Printf("CSV Headers: %v", headers)
|
|
|
|
// Hapus data lama jika diminta
|
|
if s.config.DeleteBefore {
|
|
if err := s.db.Where("1=1").Delete(model).Error; err != nil {
|
|
return fmt.Errorf("failed to delete existing data: %w", err)
|
|
}
|
|
log.Printf("Deleted existing data from %s", s.config.TableName)
|
|
}
|
|
|
|
count := 0
|
|
batch := []interface{}{}
|
|
|
|
for {
|
|
record, err := reader.Read()
|
|
if err != nil {
|
|
if err.Error() == "EOF" {
|
|
break
|
|
}
|
|
log.Printf("Error reading row %d: %v", count+1, err)
|
|
continue
|
|
}
|
|
|
|
if len(record) < len(headers) {
|
|
log.Printf("Skipping incomplete row %d", count+1)
|
|
continue
|
|
}
|
|
|
|
// Buat instance model baru
|
|
instance := reflect.New(reflect.TypeOf(model).Elem()).Interface()
|
|
|
|
// Mapping data dari CSV ke struct
|
|
if err := s.mapCSVToStruct(headers, record, instance); err != nil {
|
|
log.Printf("Error mapping row %d: %v", count+1, err)
|
|
continue
|
|
}
|
|
|
|
// Set audit fields
|
|
s.setAuditFields(instance)
|
|
|
|
if s.config.DryRun {
|
|
log.Printf("Dry run - Would insert: %+v", instance)
|
|
} else {
|
|
batch = append(batch, instance)
|
|
|
|
// Insert batch jika sudah penuh
|
|
if len(batch) >= s.config.BatchSize {
|
|
if err := s.insertBatch(batch); err != nil {
|
|
return fmt.Errorf("failed to insert batch: %w", err)
|
|
}
|
|
count += len(batch)
|
|
batch = []interface{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Insert sisa batch
|
|
if len(batch) > 0 && !s.config.DryRun {
|
|
if err := s.insertBatch(batch); err != nil {
|
|
return fmt.Errorf("failed to insert final batch: %w", err)
|
|
}
|
|
count += len(batch)
|
|
}
|
|
|
|
log.Printf("Seeding completed. %d records processed from %s", count, s.config.TableName)
|
|
return nil
|
|
}
|
|
|
|
// mapCSVToStruct mapping data CSV ke struct berdasarkan column map atau reflection
|
|
func (s *MasterSeeder) mapCSVToStruct(headers []string, record []string, instance interface{}) error {
|
|
v := reflect.ValueOf(instance).Elem()
|
|
t := v.Type()
|
|
|
|
for i, header := range headers {
|
|
if i >= len(record) {
|
|
continue
|
|
}
|
|
|
|
value := strings.TrimSpace(record[i])
|
|
if value == "" {
|
|
continue
|
|
}
|
|
|
|
// Cari field berdasarkan column map atau nama header
|
|
fieldName := s.getFieldName(header)
|
|
|
|
field, found := t.FieldByName(fieldName)
|
|
if !found {
|
|
// Coba dengan nama field yang berbeda case
|
|
for j := 0; j < t.NumField(); j++ {
|
|
f := t.Field(j)
|
|
if strings.EqualFold(f.Name, fieldName) {
|
|
field = f
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if found {
|
|
if err := s.setFieldValue(v.FieldByName(field.Name), value); err != nil {
|
|
return fmt.Errorf("failed to set field %s: %w", field.Name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getFieldName mendapatkan nama field dari header CSV
|
|
func (s *MasterSeeder) getFieldName(header string) string {
|
|
// Cek di column map dulu
|
|
if fieldName, exists := s.config.ColumnMap[header]; exists {
|
|
return fieldName
|
|
}
|
|
|
|
// Konversi header ke PascalCase
|
|
words := strings.Split(header, "_")
|
|
for i, word := range words {
|
|
words[i] = strings.Title(strings.ToLower(word))
|
|
}
|
|
return strings.Join(words, "")
|
|
}
|
|
|
|
// setFieldValue mengisi value ke field struct dengan type conversion
|
|
func (s *MasterSeeder) setFieldValue(field reflect.Value, value string) error {
|
|
if !field.CanSet() {
|
|
return nil
|
|
}
|
|
|
|
switch field.Kind() {
|
|
case reflect.String:
|
|
field.SetString(value)
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
// Handle bigserial (int64)
|
|
if value == "" {
|
|
field.SetInt(0)
|
|
} else {
|
|
intVal, err := s.parseInt(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
field.SetInt(intVal)
|
|
}
|
|
case reflect.Bool:
|
|
boolVal := strings.ToLower(value) == "true" || value == "1"
|
|
field.SetBool(boolVal)
|
|
case reflect.Interface:
|
|
// Untuk field interface{} (seperti Code, Name, Status pada Ethnic)
|
|
field.Set(reflect.ValueOf(value))
|
|
default:
|
|
// Handle pointer types
|
|
if field.Kind() == reflect.Ptr {
|
|
if value == "" {
|
|
field.Set(reflect.Zero(field.Type()))
|
|
} else {
|
|
// Create new instance of the pointed type
|
|
newValue := reflect.New(field.Type().Elem())
|
|
if err := s.setFieldValue(newValue.Elem(), value); err != nil {
|
|
return err
|
|
}
|
|
field.Set(newValue)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// parseInt parsing string ke int64
|
|
func (s *MasterSeeder) parseInt(value string) (int64, error) {
|
|
var intVal int64
|
|
_, err := fmt.Sscanf(value, "%d", &intVal)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to parse int from '%s': %w", value, err)
|
|
}
|
|
return intVal, nil
|
|
}
|
|
|
|
// setAuditFields mengisi field audit (CreatedAt, UpdatedAt, DeletedAt)
|
|
func (s *MasterSeeder) setAuditFields(instance interface{}) {
|
|
v := reflect.ValueOf(instance).Elem()
|
|
now := time.Now()
|
|
|
|
// Set CreatedAt jika field ada
|
|
if field := v.FieldByName("CreatedAt"); field.IsValid() && field.CanSet() {
|
|
if field.Kind() == reflect.Struct {
|
|
// Handle time.Time
|
|
field.Set(reflect.ValueOf(now))
|
|
} else if field.Kind() == reflect.Interface {
|
|
field.Set(reflect.ValueOf(now))
|
|
}
|
|
}
|
|
|
|
// Set UpdatedAt jika field ada (pointer)
|
|
if field := v.FieldByName("UpdatedAt"); field.IsValid() && field.CanSet() {
|
|
if field.Kind() == reflect.Ptr {
|
|
field.Set(reflect.ValueOf(&now))
|
|
}
|
|
}
|
|
|
|
// Set DeletedAt ke nil jika field ada
|
|
if field := v.FieldByName("DeletedAt"); field.IsValid() && field.CanSet() {
|
|
if field.Kind() == reflect.Ptr {
|
|
field.Set(reflect.Zero(field.Type()))
|
|
}
|
|
}
|
|
}
|
|
|
|
// insertBatch insert batch data ke database
|
|
func (s *MasterSeeder) insertBatch(batch []interface{}) error {
|
|
return s.db.Transaction(func(tx *gorm.DB) error {
|
|
for _, item := range batch {
|
|
// Gunakan FirstOrCreate untuk menghindari duplicate
|
|
v := reflect.ValueOf(item).Elem()
|
|
|
|
// Buat kondisi where berdasarkan field utama (Code atau ID)
|
|
var condition interface{}
|
|
if codeField := v.FieldByName("Code"); codeField.IsValid() {
|
|
condition = map[string]interface{}{"Code": codeField.Interface()}
|
|
} else if idField := v.FieldByName("Id"); idField.IsValid() {
|
|
condition = map[string]interface{}{"Id": idField.Interface()}
|
|
}
|
|
|
|
if condition != nil {
|
|
result := tx.Where(condition).FirstOrCreate(item)
|
|
if result.Error != nil {
|
|
return result.Error
|
|
}
|
|
} else {
|
|
// Jika tidak ada kondisi, insert langsung
|
|
if err := tx.Create(item).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// Helper functions untuk seeding spesifik
|
|
|
|
// SeedProvinces seed data provinsi dari CSV
|
|
func SeedProvinces(db *gorm.DB, csvPath string) error {
|
|
config := SeederConfig{
|
|
CSVPath: csvPath,
|
|
TableName: "Province",
|
|
ColumnMap: map[string]string{"Code": "Code", "Name": "Name"},
|
|
SkipHeader: true,
|
|
BatchSize: 50,
|
|
DeleteBefore: true,
|
|
}
|
|
|
|
seeder := NewMasterSeeder(db, config)
|
|
|
|
// Definisikan struct untuk Province
|
|
type Province struct {
|
|
Id int64 `gorm:"primaryKey;autoIncrement"`
|
|
Code string `gorm:"uniqueIndex;not null"`
|
|
Name string `gorm:"not null"`
|
|
CreatedAt time.Time
|
|
UpdatedAt *time.Time
|
|
DeletedAt *time.Time
|
|
}
|
|
|
|
return seeder.SeedFromCSV(&Province{})
|
|
}
|
|
|
|
// SeedRegencies seed data kabupaten dari CSV
|
|
func SeedRegencies(db *gorm.DB, csvPath string) error {
|
|
config := SeederConfig{
|
|
CSVPath: csvPath,
|
|
TableName: "Regency",
|
|
ColumnMap: map[string]string{"Code": "Code", "Province_Code": "ProvinceCode", "Name": "Name"},
|
|
SkipHeader: true,
|
|
BatchSize: 100,
|
|
DeleteBefore: true,
|
|
}
|
|
|
|
seeder := NewMasterSeeder(db, config)
|
|
|
|
type Regency struct {
|
|
Id int64 `gorm:"primaryKey;autoIncrement"`
|
|
Code string `gorm:"uniqueIndex;not null"`
|
|
ProvinceCode string `gorm:"index;not null"`
|
|
Name string `gorm:"not null"`
|
|
CreatedAt time.Time
|
|
UpdatedAt *time.Time
|
|
DeletedAt *time.Time
|
|
}
|
|
|
|
return seeder.SeedFromCSV(&Regency{})
|
|
}
|
|
|
|
// SeedEthnics seed data suku bangsa dari CSV
|
|
// func SeedEthnics(db *gorm.DB, csvPath string) error {
|
|
// config := SeederConfig{
|
|
// CSVPath: csvPath,
|
|
// TableName: "Ethnic",
|
|
// ColumnMap: map[string]string{"Code": "Code", "Name": "Name", "Status": "Status"},
|
|
// SkipHeader: true,
|
|
// BatchSize: 50,
|
|
// DeleteBefore: true,
|
|
// }
|
|
|
|
// seeder := NewMasterSeeder(db, config)
|
|
|
|
// // Gunakan struct Ethnic yang sudah ada
|
|
// return seeder.SeedFromCSV(&Ethnic{})
|
|
// }
|