feat: load explicit configuration references and remove implicit env overlays
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
|
||||
`database` 包提供基于标准库 `database/sql` 的连接池生命周期,不绑定 ORM 或驱动:
|
||||
|
||||
- `Config` / `ConfigFromEnv`:连接池参数和 `GO_OMNI_DB_*` 环境变量。
|
||||
- `Config`:驱动、DSN 与连接池参数;嵌入项目配置,通过文件显式引用变量。
|
||||
- `Open` / `OpenConnector`:通过 DSN 或标准 Connector 创建连接池、设置连接上限并在返回前执行 `PingContext`。
|
||||
- `Health`:用于就绪检查,等待连接和 Ping 均受 `PingTimeout` 限制。
|
||||
- `InTx`:统一事务提交和回滚;工作函数返回错误或 panic 时回滚。
|
||||
@@ -17,11 +17,12 @@
|
||||
```go
|
||||
import (
|
||||
"context"
|
||||
"git.yuntuo.hk/k2safe/go-omni/config"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"git.yuntuo.hk/k2safe/go-omni/database"
|
||||
)
|
||||
|
||||
cfg, err := database.ConfigFromEnv("GO_OMNI_DB_")
|
||||
cfg, err := config.LoadProject("etc/database.yaml", database.DefaultConfig())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -32,19 +33,15 @@ if err != nil {
|
||||
defer db.Close()
|
||||
```
|
||||
|
||||
连接池相关环境变量:
|
||||
对应文件(框架不规定变量名):
|
||||
|
||||
```text
|
||||
GO_OMNI_DB_DRIVER=mysql
|
||||
GO_OMNI_DB_DSN=user:password@tcp(127.0.0.1:3306)/app?parseTime=true
|
||||
GO_OMNI_DB_MAX_OPEN_CONNS=10
|
||||
GO_OMNI_DB_MAX_IDLE_CONNS=5
|
||||
GO_OMNI_DB_CONN_MAX_LIFETIME=30m
|
||||
GO_OMNI_DB_CONN_MAX_IDLE_TIME=5m
|
||||
GO_OMNI_DB_PING_TIMEOUT=5s
|
||||
```yaml
|
||||
driver: mysql
|
||||
dsn: "${DB_DSN}"
|
||||
maxOpenConns: "${DB_MAX_OPEN:-10}"
|
||||
```
|
||||
|
||||
使用 `ConfigFromEnv` / `Open` 时 `DSN` 必填,环境变量入口的驱动默认是 `mysql`。数据库建库、迁移、表结构和具体 Repository 属于业务项目或可选模块,不会被核心包自动执行。
|
||||
Open 使用的 Driver、DSN 必填。建库、迁移、表结构和业务 Repository 由项目负责。v0.22 删除 ConfigFromEnv/ApplyEnv;不再隐式扫描数据库变量。
|
||||
|
||||
`MaxIdleConns=0` 会禁用空闲连接。初始化 Ping 失败会关闭已创建的连接池。
|
||||
|
||||
@@ -62,12 +59,9 @@ python3 scripts/verifyDatabase.py --version v0.2.0
|
||||
结束时清理容器、匿名数据卷和临时项目。常规 `go test ./...` 不依赖 Docker。
|
||||
|
||||
|
||||
## 项目配置叠加
|
||||
## 项目配置
|
||||
|
||||
`database.ApplyEnv(cfg, prefix)` 在已有文件/默认配置上覆盖非空环境变量,保留未覆盖的池参数。
|
||||
`Config` 的 JSON/YAML 字段使用 lowerCamelCase,`DSN` 使用 `json:"-"`,连接密钥从环境变量加载。
|
||||
API 脚手架 `--database mysql` 使用此流程,并要求正的 `maxOpenConns`,提供数据库 readiness 与幂等清理。
|
||||
具体表结构、迁移和 Repository 留在业务项目,参考 `examples/userService`。
|
||||
配置结构作为项目 Config 的字段,Default() 初始化连接池,文件按需覆盖。DSN 可使用 `dsn: "${DB_DSN}"`;database.Config 的 JSON 序列化不输出 DSN 原值。不要打印原始配置对象。当前 API 脚手架使用结构化 MySQL 适配器,由 svc 统一初始化与关闭。
|
||||
|
||||
## Connector 接入(v0.9.1-preview)
|
||||
|
||||
@@ -103,7 +97,7 @@ func Open(ctx context.Context, address, user, password, name string,
|
||||
}
|
||||
```
|
||||
|
||||
调用时以 `database.DefaultPoolConfig()` 为起点,按项目需要调整。已有 `database.Config` 可调用 `.PoolConfig()` 提取池设置;`ConfigFromEnv` / `ApplyEnv` 仍是 DSN 入口,不用为 Connector 填假 DSN。`PoolConfig` 支持通过 `config.DecodeFile` 加载项目的 JSON/YAML 嵌套配置,环境覆盖由项目按自己的驱动配置组织。
|
||||
调用时以 `database.DefaultPoolConfig()` 为起点,按项目需要调整。已有 `database.Config` 可调用 `.PoolConfig()` 提取池设置;Connector 不需要填写 DSN。`PoolConfig` 支持通过 `config.DecodeFile` 加载项目的 JSON/YAML 嵌套配置,环境引用由项目在文件里显式声明。
|
||||
|
||||
| PoolConfig 字段 | 默认值 / 含义 |
|
||||
| --- | --- |
|
||||
|
||||
@@ -2,19 +2,27 @@ package database
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"git.yuntuo.hk/k2safe/go-omni/config"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestApplyEnvPreservesFilePoolAndHidesDSN(t *testing.T) {
|
||||
func TestExplicitConfigPreservesPoolAndHidesDSN(t *testing.T) {
|
||||
cfg := DefaultConfig()
|
||||
cfg.Driver = "mysql"
|
||||
path := filepath.Join(t.TempDir(), "db.yaml")
|
||||
if err := os.WriteFile(path, []byte("dsn: \"${OMNI_TEST_DSN}\"\nmaxIdleConns: \"${OMNI_TEST_MAX_IDLE_CONNS}\"\nmaxOpenConns: \"${OMNI_TEST_MAX_OPEN_CONNS:-3}\""), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg.MaxOpenConns = 3
|
||||
cfg.MaxIdleConns = 1
|
||||
cfg.PingTimeout = time.Second
|
||||
t.Setenv("OMNI_TEST_DSN", "user:secret@tcp(localhost:3306)/test")
|
||||
t.Setenv("OMNI_TEST_MAX_IDLE_CONNS", "0")
|
||||
got, err := ApplyEnv(cfg, "OMNI_TEST_")
|
||||
got, err := config.LoadProject(path, cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -29,7 +37,7 @@ func TestApplyEnvPreservesFilePoolAndHidesDSN(t *testing.T) {
|
||||
t.Fatalf("unsafe config encoding: %s", data)
|
||||
}
|
||||
t.Setenv("OMNI_TEST_MAX_OPEN_CONNS", "bad")
|
||||
if _, err = ApplyEnv(cfg, "OMNI_TEST_"); err == nil {
|
||||
if _, err = config.LoadProject(path, cfg); err == nil {
|
||||
t.Fatal("invalid override accepted")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,17 +5,16 @@ package database
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Driver string `json:"driver"`
|
||||
DSN string `json:"-"`
|
||||
DSN string `json:"dsn"`
|
||||
MaxOpenConns int `json:"maxOpenConns"`
|
||||
MaxIdleConns int `json:"maxIdleConns"`
|
||||
ConnMaxLifetime time.Duration `json:"connMaxLifetime"`
|
||||
@@ -23,6 +22,14 @@ type Config struct {
|
||||
PingTimeout time.Duration `json:"pingTimeout"`
|
||||
}
|
||||
|
||||
// MarshalJSON omits the connection credential from serialized diagnostics.
|
||||
func (c Config) MarshalJSON() ([]byte, error) {
|
||||
type plain Config
|
||||
safe := plain(c)
|
||||
safe.DSN = ""
|
||||
return json.Marshal(safe)
|
||||
}
|
||||
|
||||
func DefaultConfig() Config { return DefaultPoolConfig().config() }
|
||||
|
||||
// PoolConfig returns driver-independent connection pool settings.
|
||||
@@ -34,49 +41,6 @@ func (c Config) PoolConfig() PoolConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigFromEnv reads GO_OMNI_DB_* style settings. DSN is required.
|
||||
func ConfigFromEnv(prefix string) (Config, error) {
|
||||
return ApplyEnv(DefaultConfig(), prefix)
|
||||
}
|
||||
|
||||
// ApplyEnv overlays nonempty environment settings on file/default configuration.
|
||||
// DSN is environment-only when Config is decoded from JSON or YAML.
|
||||
func ApplyEnv(cfg Config, prefix string) (Config, error) {
|
||||
prefix = strings.TrimSpace(prefix)
|
||||
if prefix == "" {
|
||||
prefix = "GO_OMNI_DB_"
|
||||
}
|
||||
if value := strings.TrimSpace(os.Getenv(prefix + "DRIVER")); value != "" {
|
||||
cfg.Driver = value
|
||||
}
|
||||
if cfg.Driver == "" {
|
||||
cfg.Driver = "mysql"
|
||||
}
|
||||
if value := strings.TrimSpace(os.Getenv(prefix + "DSN")); value != "" {
|
||||
cfg.DSN = value
|
||||
}
|
||||
if cfg.DSN == "" {
|
||||
return Config{}, fmt.Errorf("%sDSN is required", prefix)
|
||||
}
|
||||
var err error
|
||||
if cfg.MaxOpenConns, err = envInt(prefix+"MAX_OPEN_CONNS", cfg.MaxOpenConns); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if cfg.MaxIdleConns, err = envInt(prefix+"MAX_IDLE_CONNS", cfg.MaxIdleConns); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if cfg.ConnMaxLifetime, err = envDuration(prefix+"CONN_MAX_LIFETIME", cfg.ConnMaxLifetime); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if cfg.ConnMaxIdleTime, err = envDuration(prefix+"CONN_MAX_IDLE_TIME", cfg.ConnMaxIdleTime); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if cfg.PingTimeout, err = envDuration(prefix+"PING_TIMEOUT", cfg.PingTimeout); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
return cfg, cfg.Validate()
|
||||
}
|
||||
|
||||
func (c Config) Validate() error {
|
||||
if strings.TrimSpace(c.Driver) == "" {
|
||||
return errors.New("database driver is required")
|
||||
@@ -165,27 +129,3 @@ func (db *DB) InTx(ctx context.Context, options *sql.TxOptions, work func(*sql.T
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func envInt(key string, fallback int) (int, error) {
|
||||
value := strings.TrimSpace(os.Getenv(key))
|
||||
if value == "" {
|
||||
return fallback, nil
|
||||
}
|
||||
parsed, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("%s must be an integer: %w", key, err)
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func envDuration(key string, fallback time.Duration) (time.Duration, error) {
|
||||
value := strings.TrimSpace(os.Getenv(key))
|
||||
if value == "" {
|
||||
return fallback, nil
|
||||
}
|
||||
parsed, err := time.ParseDuration(value)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("%s must be a duration: %w", key, err)
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
@@ -2,54 +2,10 @@ package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestConfigFromEnv(t *testing.T) {
|
||||
keys := []string{
|
||||
"GO_OMNI_DB_DRIVER", "GO_OMNI_DB_DSN", "GO_OMNI_DB_MAX_OPEN_CONNS",
|
||||
"GO_OMNI_DB_MAX_IDLE_CONNS", "GO_OMNI_DB_CONN_MAX_LIFETIME",
|
||||
"GO_OMNI_DB_CONN_MAX_IDLE_TIME", "GO_OMNI_DB_PING_TIMEOUT",
|
||||
}
|
||||
for _, key := range keys {
|
||||
t.Setenv(key, "")
|
||||
_ = os.Unsetenv(key)
|
||||
}
|
||||
t.Setenv("GO_OMNI_DB_DSN", "root:secret@tcp(127.0.0.1:3306)/demo")
|
||||
t.Setenv("GO_OMNI_DB_MAX_OPEN_CONNS", "20")
|
||||
t.Setenv("GO_OMNI_DB_MAX_IDLE_CONNS", "8")
|
||||
t.Setenv("GO_OMNI_DB_CONN_MAX_LIFETIME", "2m")
|
||||
t.Setenv("GO_OMNI_DB_PING_TIMEOUT", "750ms")
|
||||
cfg, err := ConfigFromEnv("")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.Driver != "mysql" || cfg.DSN == "" || cfg.MaxOpenConns != 20 || cfg.MaxIdleConns != 8 || cfg.ConnMaxLifetime != 2*time.Minute || cfg.PingTimeout != 750*time.Millisecond {
|
||||
t.Fatalf("unexpected config: %+v", cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigFromEnvRejectsInvalidValues(t *testing.T) {
|
||||
t.Setenv("GO_OMNI_DB_DSN", "demo")
|
||||
t.Setenv("GO_OMNI_DB_MAX_OPEN_CONNS", "bad")
|
||||
if _, err := ConfigFromEnv(""); err == nil || !strings.Contains(err.Error(), "MAX_OPEN_CONNS") {
|
||||
t.Fatalf("expected invalid pool limit error, got %v", err)
|
||||
}
|
||||
t.Setenv("GO_OMNI_DB_MAX_OPEN_CONNS", "2")
|
||||
t.Setenv("GO_OMNI_DB_MAX_IDLE_CONNS", "3")
|
||||
if _, err := ConfigFromEnv(""); err == nil || !strings.Contains(err.Error(), "max idle") {
|
||||
t.Fatalf("expected pool relationship error, got %v", err)
|
||||
}
|
||||
t.Setenv("GO_OMNI_DB_MAX_IDLE_CONNS", "1")
|
||||
t.Setenv("GO_OMNI_DB_PING_TIMEOUT", "0s")
|
||||
if _, err := ConfigFromEnv(""); err == nil || !strings.Contains(err.Error(), "timeouts") {
|
||||
t.Fatalf("expected timeout error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenValidatesBeforeLoadingDriver(t *testing.T) {
|
||||
_, err := Open(context.Background(), Config{Driver: "", DSN: "demo"})
|
||||
if err == nil || !strings.Contains(err.Error(), "driver") {
|
||||
|
||||
@@ -10,13 +10,14 @@
|
||||
go-omni new --database mysql --module example.com/billing ./billing
|
||||
```
|
||||
|
||||
`etc/config.yaml` 使用结构化配置,环境覆盖优先:
|
||||
`etc/config.yaml` 使用结构化配置;需要环境变量时在字段值明确写引用,固定值不被覆盖:
|
||||
|
||||
```yaml
|
||||
database:
|
||||
host: db.example.com
|
||||
port: 3306
|
||||
user: app
|
||||
password: "${DB_PASSWORD}"
|
||||
database: billing
|
||||
parseTime: true
|
||||
location: UTC
|
||||
@@ -37,7 +38,7 @@ database:
|
||||
pingTimeout: 5s
|
||||
```
|
||||
|
||||
通过 `GO_OMNI_DB_PASSWORD` 传密码;还有 HOST、PORT、USER、DATABASE、TLS_CA_FILE、TLS_CERT_FILE、TLS_KEY_FILE、POOL_MAX_OPEN_CONNS 等。业务也可自定义 env 标签。新生成服务不再读取 DSN;独立数据库迁移 CLI 和手写 `database.Open/OpenConnector` 的原接口保留。
|
||||
示例通过文件里的 `${DB_PASSWORD}` 读取密码;HOST、PORT、TLS 和池参数同样按需在文件写引用,不自动扫描变量、不使用 env 标签。新生成服务使用结构化连接参数;独立迁移 CLI 的显式 DSN 参数及 Open/OpenConnector 接口保留。
|
||||
|
||||
默认启用 TLS 并校验系统信任链与主机名;私有 CA 用 caFile,双向 TLS 额外设置 certFile/keyFile。只有明确设置 `tls.mode: disabled` 才使用明文。Unix socket 用 socket,若在 socket 上使用 TLS,必须提供 serverName。不会注册全局 TLS 配置或全局驱动 Logger。
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ type Config struct {
|
||||
Port int `json:"port"`
|
||||
Socket string `json:"socket"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password" env:",secret"`
|
||||
Password string `json:"password"`
|
||||
Database string `json:"database"`
|
||||
ParseTime bool `json:"parseTime"`
|
||||
Location string `json:"location"`
|
||||
|
||||
Reference in New Issue
Block a user