67 lines
2.6 KiB
Go
67 lines
2.6 KiB
Go
// Package database owns everything related to talking to MySQL: opening
|
|||
|
|
// the connection pool and running startup migrations. Nothing outside this
|
||
|
|
// package (and the repositories in internal/models) should import the
|
||
|
|
// MySQL driver directly - that keeps the database technology an
|
||
|
|
// implementation detail hidden behind a plain *sql.DB.
|
||
|
|
package database
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"database/sql"
|
||
|
|
"fmt"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
// Blank import: we never call anything from this package by name.
|
||
|
|
// Importing it purely for its side effect - its init() function
|
||
|
|
// registers "mysql" as a driver name with database/sql, which is what
|
||
|
|
// lets sql.Open("mysql", ...) below work at all.
|
||
|
|
_ "github.com/go-sql-driver/mysql"
|
||
|
|
|
||
|
|
"git.hamidsoltani.com/hamid/go-simple-api/internal/config"
|
||
|
|
)
|
||
|
|
|
||
|
|
// NewMySQL opens a connection pool to MySQL, configures reasonable pool
|
||
|
|
// limits, and verifies connectivity with a real ping before returning -
|
||
|
|
// so a bad connection string / unreachable database fails loudly at
|
||
|
|
// startup instead of silently on the first real query.
|
||
|
|
func NewMySQL(ctx context.Context, cfg config.Config) (*sql.DB, error) {
|
||
|
|
// DSN = Data Source Name. Format: user:password@tcp(host:port)/dbname?options
|
||
|
|
// parseTime=true tells the driver to convert MySQL DATETIME/TIMESTAMP
|
||
|
|
// columns into Go time.Time values automatically.
|
||
|
|
dsn := fmt.Sprintf(
|
||
|
|
"%s:%s@tcp(%s:%s)/%s?parseTime=true",
|
||
|
|
cfg.DBUser, cfg.DBPassword, cfg.DBHost, cfg.DBPort, cfg.DBName,
|
||
|
|
)
|
||
|
|
|
||
|
|
// sql.Open does NOT connect yet - it just validates the DSN and
|
||
|
|
// returns a *sql.DB, which represents a pool of connections managed
|
||
|
|
// for you, not a single live connection.
|
||
|
|
db, err := sql.Open("mysql", dsn)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("open mysql: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Pool tuning:
|
||
|
|
// MaxOpenConns - hard ceiling on simultaneous connections, protects
|
||
|
|
// the database from being overwhelmed by this app.
|
||
|
|
// MaxIdleConns - how many connections to keep warm/ready even when
|
||
|
|
// idle, so we don't pay reconnect cost constantly.
|
||
|
|
// ConnMaxLifetime - force connections to be recycled periodically,
|
||
|
|
// useful behind load balancers or if MySQL itself
|
||
|
|
// closes long-lived idle connections.
|
||
|
|
db.SetMaxOpenConns(10)
|
||
|
|
db.SetMaxIdleConns(5)
|
||
|
|
db.SetConnMaxLifetime(5 * time.Minute)
|
||
|
|
|
||
|
|
// Give the ping a hard deadline instead of letting it hang forever if
|
||
|
|
// the database host is unreachable.
|
||
|
|
pingCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||
|
|
defer cancel()
|
||
|
|
|
||
|
|
if err := db.PingContext(pingCtx); err != nil {
|
||
|
|
return nil, fmt.Errorf("ping mysql: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
return db, nil
|
||
|
|
}
|