2025-06-05 21:21:28 +07:00

155 lines
3.5 KiB
Go

package database
import (
"context"
"database/sql"
"fmt"
"sync"
"time"
"starter-kit/internal/helper/config"
"starter-kit/internal/helper/logger"
"github.com/sirupsen/logrus"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
var (
// dbInstance is the singleton database instance
dbInstance *gorm.DB
dbOnce sync.Once
dbErr error
)
// Database wraps the database connection
type Database struct {
DB *gorm.DB
Config *config.DatabaseConfig
Logger *logrus.Logger
}
// NewConnection creates a new database connection based on configuration
func NewConnection(cfg *config.DatabaseConfig) (*gorm.DB, error) {
dbOnce.Do(func() {
switch cfg.Driver {
case "postgres":
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
cfg.Host, cfg.Port, cfg.Username, cfg.Password, cfg.Database, cfg.SSLMode)
dbInstance, dbErr = gorm.Open(postgres.Open(dsn), &gorm.Config{})
case "mysql":
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
cfg.Username, cfg.Password, cfg.Host, cfg.Port, cfg.Database)
dbInstance, dbErr = gorm.Open(mysql.Open(dsn), &gorm.Config{})
case "sqlite":
dbInstance, dbErr = gorm.Open(sqlite.Open(cfg.Database), &gorm.Config{})
default:
dbErr = fmt.Errorf("unsupported database driver: %s", cfg.Driver)
return
}
if dbErr != nil {
dbErr = fmt.Errorf("failed to connect to database: %w", dbErr)
return
}
// Set connection pool settings
var sqlDB *sql.DB
sqlDB, dbErr = dbInstance.DB()
if dbErr != nil {
dbErr = fmt.Errorf("failed to get database instance: %w", dbErr)
return
}
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
sqlDB.SetConnMaxLifetime(time.Duration(cfg.ConnMaxLifetime) * time.Second)
logger.Info("Database connection established successfully")
})
return dbInstance, dbErr
}
// Close closes the database connection
func Close() error {
if dbInstance == nil {
return nil
}
sqlDB, err := dbInstance.DB()
if err != nil {
return fmt.Errorf("failed to get database instance: %w", err)
}
if err := sqlDB.Close(); err != nil {
return fmt.Errorf("error closing database: %w", err)
}
logger.Info("Database connection closed")
return nil
}
// Migrate runs database migrations
func Migrate(cfg config.DatabaseConfig) error {
_, err := NewConnection(&cfg)
if err != nil {
return fmt.Errorf("failed to connect to database: %w", err)
}
// TODO: Add your database migrations here
// Example: return dbInstance.AutoMigrate(&models.User{}, &models.Post{})
return nil
}
// GetDB returns the database instance
func GetDB() *gorm.DB {
return dbInstance
}
// WithTransaction executes a function within a database transaction
func WithTransaction(fn func(tx *gorm.DB) error) error {
tx := dbInstance.Begin()
if tx.Error != nil {
return tx.Error
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
panic(r)
}
}()
if err := fn(tx); err != nil {
if rbErr := tx.Rollback().Error; rbErr != nil {
return fmt.Errorf("tx: %v, rb err: %v", err, rbErr)
}
return err
}
return tx.Commit().Error
}
// HealthCheck checks if the database is reachable
func HealthCheck() error {
if dbInstance == nil {
return fmt.Errorf("database not initialized")
}
sqlDB, err := dbInstance.DB()
if err != nil {
return fmt.Errorf("failed to get database instance: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return sqlDB.PingContext(ctx)
}