b0350abfbe
- Fix DB_NAME=db_yal -> mydb in api_yal .env - Add connection pool (MaxOpenConns 25, MaxIdleConns 10, ConnMaxLifetime 30m) - Replace GORM AutoMigrate with golang-migrate in api_yal and api_bb - Create embedded SQL migrations for both APIs - Add DB_SCHEMA support to api_bb config - Consolidate to single Postgres: db_bb -> schema 'bb' on db container - Remove db_bb service, bb-network, db_bb volume from compose - Remove api_tp targets from Makefile - Clean up old migrate.go
184 lines
4.2 KiB
Go
184 lines
4.2 KiB
Go
package database
|
|
|
|
import (
|
|
"api_bb/migrations"
|
|
"database/sql"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/golang-migrate/migrate/v4"
|
|
"github.com/golang-migrate/migrate/v4/database/postgres"
|
|
"github.com/golang-migrate/migrate/v4/source/iofs"
|
|
"go.uber.org/zap"
|
|
"gorm.io/driver/postgres"
|
|
"gorm.io/gorm"
|
|
|
|
"api_bb/pkg/logger"
|
|
)
|
|
|
|
type Database struct {
|
|
DB *gorm.DB
|
|
cfg *Config
|
|
}
|
|
|
|
type Config struct {
|
|
URL string
|
|
Schema string
|
|
}
|
|
|
|
func NewDatabase(cfg *Config) *Database {
|
|
if cfg.Schema == "" {
|
|
cfg.Schema = "public"
|
|
}
|
|
return &Database{
|
|
cfg: cfg,
|
|
}
|
|
}
|
|
|
|
func (d *Database) Connect() error {
|
|
zapLogger := logger.Get()
|
|
|
|
zapLogger.Info("attempting to connect to database",
|
|
zap.String("host", ExtractHostFromDSN(d.cfg.URL)),
|
|
zap.String("database", ExtractDBNameFromDSN(d.cfg.URL)),
|
|
zap.String("schema", d.cfg.Schema),
|
|
)
|
|
|
|
dsn := d.cfg.URL
|
|
if d.cfg.Schema != "public" {
|
|
dsn = dsn + fmt.Sprintf(" search_path=%s", d.cfg.Schema)
|
|
}
|
|
|
|
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
|
if err != nil {
|
|
zapLogger.Error("failed to connect to database",
|
|
zap.Error(err),
|
|
zap.String("database_url", MaskPassword(d.cfg.URL)),
|
|
)
|
|
return fmt.Errorf("failed to connect to database: %w", err)
|
|
}
|
|
|
|
d.DB = db
|
|
|
|
zapLogger.Info("Configure connection pool")
|
|
sqlDB, err := d.DB.DB()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get underlying sql.DB: %w", err)
|
|
}
|
|
sqlDB.SetMaxOpenConns(25)
|
|
sqlDB.SetMaxIdleConns(10)
|
|
sqlDB.SetConnMaxLifetime(30 * time.Minute)
|
|
|
|
zapLogger.Info("Run database migrations")
|
|
if err := d.runMigrations(sqlDB); err != nil {
|
|
return fmt.Errorf("failed to run migrations: %w", err)
|
|
}
|
|
zapLogger.Info("Migrations completed successfully")
|
|
|
|
zapLogger.Info("successfully connected to database",
|
|
zap.String("host", ExtractHostFromDSN(d.cfg.URL)),
|
|
zap.String("database", ExtractDBNameFromDSN(d.cfg.URL)),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *Database) runMigrations(sqlDB *sql.DB) error {
|
|
zapLogger := logger.Get()
|
|
|
|
source, err := iofs.New(migrations.FS, ".")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create migration source: %w", err)
|
|
}
|
|
|
|
driver, err := postgres.WithInstance(sqlDB, &postgres.Config{})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create postgres driver: %w", err)
|
|
}
|
|
|
|
m, err := migrate.NewWithInstance("iofs", source, "postgres", driver)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create migrate instance: %w", err)
|
|
}
|
|
|
|
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
|
|
zapLogger.Error("Migration error", zap.Error(err))
|
|
return fmt.Errorf("failed to apply migrations: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *Database) Ping() error {
|
|
zapLogger := logger.Get()
|
|
|
|
sqlDB, err := d.DB.DB()
|
|
if err != nil {
|
|
zapLogger.Error("failed to get database instance", zap.Error(err))
|
|
return fmt.Errorf("failed to get database instance: %w", err)
|
|
}
|
|
|
|
if err := sqlDB.Ping(); err != nil {
|
|
zapLogger.Error("database ping failed", zap.Error(err))
|
|
return fmt.Errorf("database ping failed: %w", err)
|
|
}
|
|
|
|
zapLogger.Info("database ping successful")
|
|
return nil
|
|
}
|
|
|
|
func (d *Database) Close() error {
|
|
zapLogger := logger.Get()
|
|
|
|
if d.DB == nil {
|
|
return nil
|
|
}
|
|
|
|
sqlDB, err := d.DB.DB()
|
|
if err != nil {
|
|
zapLogger.Error("failed to get database instance for closing", zap.Error(err))
|
|
return fmt.Errorf("failed to get database instance: %w", err)
|
|
}
|
|
|
|
zapLogger.Info("closing database connection")
|
|
if err := sqlDB.Close(); err != nil {
|
|
zapLogger.Error("failed to close database connection", zap.Error(err))
|
|
return fmt.Errorf("failed to close database connection: %w", err)
|
|
}
|
|
|
|
zapLogger.Info("database connection closed successfully")
|
|
return nil
|
|
}
|
|
|
|
func ExtractHostFromDSN(dsn string) string {
|
|
parts := strings.Split(dsn, " ")
|
|
for _, part := range parts {
|
|
if strings.HasPrefix(part, "host=") {
|
|
return strings.TrimPrefix(part, "host=")
|
|
}
|
|
}
|
|
return "unknown"
|
|
}
|
|
|
|
func ExtractDBNameFromDSN(dsn string) string {
|
|
parts := strings.Split(dsn, " ")
|
|
for _, part := range parts {
|
|
if strings.HasPrefix(part, "dbname=") {
|
|
return strings.TrimPrefix(part, "dbname=")
|
|
}
|
|
}
|
|
return "unknown"
|
|
}
|
|
|
|
func MaskPassword(dsn string) string {
|
|
parts := strings.Split(dsn, " ")
|
|
for i, part := range parts {
|
|
if strings.HasPrefix(part, "password=") {
|
|
parts[i] = "password=***"
|
|
break
|
|
}
|
|
}
|
|
return strings.Join(parts, " ")
|
|
}
|