first commit
This commit is contained in:
No files matched your search
@@ -0,0 +1,372 @@
|
||||
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{})
|
||||
// }
|
||||
Reference in New Issue
Block a user