diff --git a/README.md b/README.md index 18f9c5b..2d800fd 100644 --- a/README.md +++ b/README.md @@ -638,15 +638,22 @@ root of the project. Each of these connection details can be overriden by an `ENV` variable. -| Setting | Description | YAML variable | Environment variable (ENV) | Default | -| -------- | -------------------------------- | ------------- | -------------------------- | ----------- | -| Host | The database host | `host` | `APP_DATABASE_HOST` | `localhost` | -| Port | The database port | `port` | `APP_DATABASE_PORT` | `3306` | -| Database | The database name | `database` | `APP_DATABASE_DATABASE` | | -| Username | App user name | `username` | `APP_DATABASE_USERNAME` | | -| Password | App user password | `password` | `APP_DATABASE_PASSWORD` | | -| Pool | Connection pool size | `pool` | `APP_DATABASE_POOL` | `5` | -| Timeout | Connection timeout (in seconds) | `timeout` | `APP_DATABASE_TIMEOUT` | `1s` | +| Setting | Description | YAML variable | Environment variable (ENV) | Default | +|-------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------|-------------------------------------------------|-------------| +| Host | The database host | `host` | `APP_DATABASE_HOST` | `localhost` | +| Port | The database port | `port` | `APP_DATABASE_PORT` | `3306` | +| Database | The database name | `database` | `APP_DATABASE_DATABASE` | | +| Username | App user name | `username` | `APP_DATABASE_USERNAME` | | +| Password | App user password | `password` | `APP_DATABASE_PASSWORD` | | +| Pool | Connection pool size | `pool` | `APP_DATABASE_POOL` | `5` | +| Timeout | Connection timeout (in seconds) | `timeout` | `APP_DATABASE_TIMEOUT` | `1s` | +| MaxOpenConnections | Sets the maximum number of open connections to the database. | `max_open_connections` | `APP_DATABASE_MAX_OPEN_CONNECTIONS` | `0` | +| ConnectionMaxIdleTime | Sets the maximum amount of time a connection may be idle. | `connection_max_idle_time` | `APP_DATABASE_CONNECTION_MAX_IDLE_TIME` | `0` | +| ConnectionMaxLifetime | Sets the maximum amount of time a connection may be reused | `connection_max_lifetime` | `APP_DATABASE_CONNECTION_MAX_LIFETIME` | `0s` | +| DisableDefaultGormTransaction | Disables default GORM transaction wrapper for write operations | `disable_default_gorm_transaction` | `APP_DATABASE_DISABLE_DEFAULT_GORM_TRANSACTION` | `false` | +| CachePreparedStatements | Enabled creating a prepared statement when executing any SQL and caches them to speed up future calls | `cache_prepared_statements` | `APP_DATABASE_CACHE_PREPARED_STATEMENTS` | `false` | +| MysqlInterpolateParams | If set to `true`, placeholders (?) in calls to db.Query() and db.Exec() are interpolated into a single query string with given parameters. | `mysql_interpolate_params` | `APP_DATABASE_MYSQL_INTERPOLATE_PARAMS` | `false` | + An example `database.yml`: diff --git a/pkg/database/config.go b/pkg/database/config.go index a88ccde..2c2b0c3 100644 --- a/pkg/database/config.go +++ b/pkg/database/config.go @@ -23,6 +23,11 @@ type Config struct { MaxOpenConnections int `mapstructure:"max_open_connections"` ConnectionMaxIdleTime time.Duration `mapstructure:"connection_max_idle_time"` ConnectionMaxLifetime time.Duration `mapstructure:"connection_max_lifetime"` + + // Performance settings + DisableDefaultGormTransaction bool `mapstructure:"disable_default_gorm_transaction"` + CachePreparedStatements bool `mapstructure:"cache_prepared_statements"` + MysqlInterpolateParams bool `mapstructure:"mysql_interpolate_params"` } // NewConfig returns a new Config instance. diff --git a/pkg/database/config_test.go b/pkg/database/config_test.go index cad4976..cf7d584 100644 --- a/pkg/database/config_test.go +++ b/pkg/database/config_test.go @@ -16,32 +16,38 @@ func TestNewConfig(t *testing.T) { }) testCases := []struct { - name string - wantError bool - host string - port int - username string - password string - database string - timeout string - pool int - maxOpenConnections int - connectionMaxIdleTime time.Duration - connectionMaxLifetime time.Duration + name string + wantError bool + host string + port int + username string + password string + database string + timeout string + pool int + maxOpenConnections int + connectionMaxIdleTime time.Duration + connectionMaxLifetime time.Duration + disableDefaultGormTransaction bool + cachePreparedStatements bool + mysqlInterpolateParams bool }{ { - name: "NewWithoutConfigFileFails", - wantError: true, - host: "", - port: 0, - username: "", - password: "", - database: "", - timeout: "", - pool: 0, - maxOpenConnections: 0, - connectionMaxIdleTime: 0, - connectionMaxLifetime: 0, + name: "NewWithoutConfigFileFails", + wantError: true, + host: "", + port: 0, + username: "", + password: "", + database: "", + timeout: "", + pool: 0, + maxOpenConnections: 0, + connectionMaxIdleTime: 0, + connectionMaxLifetime: 0, + disableDefaultGormTransaction: false, + cachePreparedStatements: false, + mysqlInterpolateParams: false, }, } @@ -62,6 +68,9 @@ func TestNewConfig(t *testing.T) { assert.Equal(t, c.MaxOpenConnections, tc.maxOpenConnections) assert.Equal(t, c.ConnectionMaxIdleTime, tc.connectionMaxIdleTime) assert.Equal(t, c.ConnectionMaxLifetime, tc.connectionMaxLifetime) + assert.Equal(t, c.DisableDefaultGormTransaction, tc.disableDefaultGormTransaction) + assert.Equal(t, c.CachePreparedStatements, tc.cachePreparedStatements) + assert.Equal(t, c.MysqlInterpolateParams, tc.mysqlInterpolateParams) }) } } diff --git a/pkg/database/connection_details.go b/pkg/database/connection_details.go index 520f857..f8d463f 100644 --- a/pkg/database/connection_details.go +++ b/pkg/database/connection_details.go @@ -7,29 +7,31 @@ import ( // ConnectionDetails represents database connection details. type ConnectionDetails struct { - Dialect string - Username string - Password string - Host string - Port int - Database string - Encoding string - Timeout string - Pool int + Dialect string + Username string + Password string + Host string + Port int + Database string + Encoding string + Timeout string + Pool int + MysqlInterpolateParams bool } // NewConnectionDetails creates a new ConnectionDetails struct from a DB configuration. func NewConnectionDetails(config *Config) ConnectionDetails { return ConnectionDetails{ - Dialect: "mysql", - Username: config.Username, - Password: config.Password, - Host: config.Host, - Port: config.Port, - Database: config.Database, - Encoding: "utf8mb4_unicode_ci", - Timeout: config.Timeout, - Pool: config.Pool, + Dialect: "mysql", + Username: config.Username, + Password: config.Password, + Host: config.Host, + Port: config.Port, + Database: config.Database, + Encoding: "utf8mb4_unicode_ci", + Timeout: config.Timeout, + Pool: config.Pool, + MysqlInterpolateParams: config.MysqlInterpolateParams, } } @@ -69,6 +71,9 @@ func (cd ConnectionDetails) opts() string { "loc": "Local", "timeout": cd.Timeout, } + if cd.MysqlInterpolateParams { + options["interpolateParams"] = "true" + } var opts []string diff --git a/pkg/database/connection_details_test.go b/pkg/database/connection_details_test.go index da86d65..640bb07 100644 --- a/pkg/database/connection_details_test.go +++ b/pkg/database/connection_details_test.go @@ -22,6 +22,7 @@ func TestNewConnectionDetails(t *testing.T) { assert.Equal(t, details.Encoding, "utf8mb4_unicode_ci") assert.Equal(t, details.Timeout, config.Timeout) assert.Equal(t, details.Pool, config.Pool) + assert.Equal(t, details.MysqlInterpolateParams, config.MysqlInterpolateParams) } func TestString(t *testing.T) { @@ -34,12 +35,13 @@ func TestString(t *testing.T) { { name: "WithAllAttributesPresent", config: &Config{ - Host: "192.168.1.1", - Port: 8080, - Username: "john", - Password: "doe", - Database: "microlith", - Timeout: "10s", + Host: "192.168.1.1", + Port: 8080, + Username: "john", + Password: "doe", + Database: "microlith", + Timeout: "10s", + MysqlInterpolateParams: true, }, connectionString: "john:doe@tcp(192.168.1.1:8080)/microlith", optionsString: "timeout=10s", @@ -118,12 +120,13 @@ func TestStringWithoutDB(t *testing.T) { func TestOpts(t *testing.T) { cases := []struct { - name string - details ConnectionDetails - timeout string - charset string - parseTime string - loc string + name string + details ConnectionDetails + timeout string + charset string + parseTime string + loc string + interpolateParams bool }{ { name: "WithPresentTimeout", @@ -145,6 +148,24 @@ func TestOpts(t *testing.T) { parseTime: "parseTime=True", loc: "loc=Local", }, + { + name: "WithMysqlInterpolateParams", + timeout: "timeout=1s", + charset: "charset=utf8", + parseTime: "parseTime=True", + loc: "loc=Local", + details: ConnectionDetails{ + MysqlInterpolateParams: true, + }, + interpolateParams: true, + }, + { + name: "WithNoMysqlInterpolateParams", + timeout: "timeout=1s", + charset: "charset=utf8", + parseTime: "parseTime=True", + loc: "loc=Local", + }, } for _, c := range cases { @@ -155,6 +176,12 @@ func TestOpts(t *testing.T) { assert.Contains(t, got, c.charset) assert.Contains(t, got, c.parseTime) assert.Contains(t, got, c.loc) + + if c.interpolateParams { + assert.Contains(t, got, "interpolateParams=true") + } else { + assert.NotContains(t, got, "interpolateParams=true") + } }) } } diff --git a/pkg/database/gorm.go b/pkg/database/gorm.go index 5be1200..191732d 100644 --- a/pkg/database/gorm.go +++ b/pkg/database/gorm.go @@ -41,7 +41,16 @@ func NewConnection(config *Config, environment, appName string) (*gorm.DB, error databasePoolSettings(sqlDB, config) dialector := mysql.New(mysql.Config{Conn: sqlDB}) - db, err := gormtrace.Open(dialector, nil, gormtrace.WithServiceName(serviceName)) + + gormConfig := &gorm.Config{} + if config.DisableDefaultGormTransaction { + gormConfig.SkipDefaultTransaction = true + } + if config.CachePreparedStatements { + gormConfig.PrepareStmt = true + } + + db, err := gormtrace.Open(dialector, gormConfig, gormtrace.WithServiceName(serviceName)) if err != nil { return nil, err }