db.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. // Package db 数据库连接池装配 + AutoMigrate 收口(架构规格 §3.5/§3.6,GORM + Repository 分层)。
  2. //
  3. // 职责边界(依赖方向铁律:infra 不依赖任何业务域):
  4. // - 本包**只提供**"建连接池 + 执行 AutoMigrate"两个通用能力,**不**内置任何业务 model;
  5. // 具体要迁哪些表由**调用方**(cmd/api 装配层 / 集成测试)传入 model 列表——
  6. // 这样 infra 保持零业务域依赖(若在本包 import identity/model、org/model 会违反铁律)。
  7. // - 业务域 model 的"全表清单"由各域 model 包提供(identity/model.AllTables / org/model.AllTables),
  8. // 装配层合并后传入 Migrate。
  9. //
  10. // 与 DDL 的关系(unified/02-data-model/core-model-ddl.sql):
  11. // - 生产环境割接以 **DDL 文件为准**(PolarDB 建表,information_schema 核对);
  12. // - AutoMigrate 用于 **dev 环境快速建表 + e2e 测试隔离建表**(幂等:表存在则对齐,不存在则建);
  13. // - 两者必须逐列一致(T2 验收:GORM model tag 与 DDL 列名/类型/可空性对齐,单测对账)。
  14. //
  15. // 典型使用(cmd/api/main.go,T4 接线后):
  16. //
  17. // db, err := db.NewDB(cfg.DB) // 建连接池 + ping
  18. // all := append(append(identitymodel.AllTables(), orgmodel.AllTables()...), task.Task{})
  19. // if err := db.Migrate(db, all...); err != nil { log.Fatalf("migrate: %v", err) }
  20. // defer db.Close(db)
  21. //
  22. // 双实例(ADR-015):两个 Go 进程各调一次 NewDB(同一 DSN 连同一库),instanceId 只影响
  23. // 日志/临时目录,不影响连接池——DB 连接池天然共享同一个 PolarDB,无需"双 DSN"。
  24. package db
  25. import (
  26. "fmt"
  27. "time"
  28. "gorm.io/driver/mysql"
  29. "gorm.io/gorm"
  30. "gorm.io/gorm/logger"
  31. "yuansiwei/unified-backend/internal/infra/config"
  32. )
  33. // NewDB 建立 MySQL 连接池并验证连通性(fail-fast:连不上库不允许启动,
  34. // 避免运行到首个查询才暴露"DSN 配错/库没起")。
  35. //
  36. // 入参:
  37. // - cfg config.DBCfg:PolarDB/MySQL 配置(DSN + 连接池参数,见 config.DBCfg)。
  38. //
  39. // 出参:
  40. // - *gorm.DB:就绪的 GORM 句柄(连接池已按 cfg 调参、已 ping 成功)。
  41. // - error:以下场景返回错误:
  42. // - DSN 为空(fail-fast,配置缺失);
  43. // - gorm.Open 失败(驱动初始化/连接失败);
  44. // - Ping 失败(网络不通/账密错/库不存在)。
  45. //
  46. // 连接池参数(§3.6 数据访问纪律):
  47. // - MaxOpenConns:限制并发连接,防双实例合计打满 PolarDB max_connections;
  48. // - MaxIdleConns:空闲连接复用,降低握手开销;
  49. // - ConnMaxLifetimeSec:连接最长存活,防连接池漂移(DB 侧 wait_timeout 与客户端对齐)。
  50. //
  51. // 外键约束:gorm.Config 显式 DisableForeignKeyConstraintWhenMigrating=true——
  52. // 本项目"外键不建物理 FK,应用层 Repository 强制作用域保证"(P0-11 收敛规则 7 / DDL 注释),
  53. // AutoMigrate 不得自动补 FK 约束(与 DDL 的 FOREIGN_KEY_CHECKS=0 语义一致)。
  54. func NewDB(cfg config.DBCfg) (*gorm.DB, error) {
  55. if cfg.DSN == "" {
  56. return nil, fmt.Errorf("db: DSN 为空(须经 config/dev.json 或环境变量 CONFIG_DB__DSN 提供)")
  57. }
  58. gdb, err := gorm.Open(mysql.Open(cfg.DSN), &gorm.Config{
  59. // 与 DDL"不建物理外键"对齐:迁移时不自动生成 FK 约束
  60. DisableForeignKeyConstraintWhenMigrating: true,
  61. // 生产/双实例默认静默 SQL 日志(避免高频只读接口刷日志);联调期可换 logger.Warn
  62. Logger: logger.Default.LogMode(logger.Silent),
  63. })
  64. if err != nil {
  65. return nil, fmt.Errorf("db: gorm.Open 失败: %w", err)
  66. }
  67. sqlDB, err := gdb.DB()
  68. if err != nil {
  69. return nil, fmt.Errorf("db: 取底层 *sql.DB 失败: %w", err)
  70. }
  71. // 连接池调参(0 值表示用 driver 默认;显式设置保证行为可预期)
  72. if cfg.MaxOpenConns > 0 {
  73. sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
  74. }
  75. if cfg.MaxIdleConns > 0 {
  76. sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
  77. }
  78. if cfg.ConnMaxLifetimeSec > 0 {
  79. sqlDB.SetConnMaxLifetime(time.Duration(cfg.ConnMaxLifetimeSec) * time.Second)
  80. }
  81. // 连通性验证(fail-fast)
  82. if err := sqlDB.Ping(); err != nil {
  83. return nil, fmt.Errorf("db: Ping 失败(网络/账密/库名): %w", err)
  84. }
  85. return gdb, nil
  86. }
  87. // Migrate 对给定模型执行 AutoMigrate(幂等建表/对齐,dev + e2e 用)。
  88. //
  89. // 入参:
  90. // - gdb *gorm.DB:NewDB 返回的句柄。
  91. // - models ...any:要迁移的模型(*struct 指针,如 &model.User{};由调用方合并各域 AllTables 传入)。
  92. //
  93. // 出参:
  94. // - error:AutoMigrate 失败时返回(DDL 语法错/权限不足/表结构冲突)。
  95. //
  96. // 幂等语义:表不存在则按 model 建;已存在则逐列比对,缺列补列、类型不符告警
  97. // (GORM AutoMigrate 不删多余列、不强改已有列类型——避免误删生产数据,对齐"迁移最易失真处"纪律)。
  98. //
  99. // 说明:本函数**不硬编码**任何 model(保持 infra 零业务依赖);
  100. // 传入空 models 时为无操作(不报错,便于条件迁移)。
  101. func Migrate(gdb *gorm.DB, models ...any) error {
  102. if len(models) == 0 {
  103. return nil
  104. }
  105. if err := gdb.AutoMigrate(models...); err != nil {
  106. return fmt.Errorf("db: AutoMigrate 失败(%d 个模型): %w", len(models), err)
  107. }
  108. return nil
  109. }
  110. // Close 关闭连接池(优雅退出路径:cmd/api 收到 SIGTERM 后先排空在途任务再 Close,
  111. // 防连接泄漏——GORM 底层 *sql.DB 不显式 Close 会残留连接占用 PolarDB max_connections)。
  112. //
  113. // 入参:
  114. // - gdb *gorm.DB:NewDB 返回的句柄(nil 安全)。
  115. //
  116. // 出参:
  117. // - error:底层 sqlDB.Close 失败时返回(通常仅进程即将退出时调用,错误可忽略)。
  118. func Close(gdb *gorm.DB) error {
  119. if gdb == nil {
  120. return nil
  121. }
  122. sqlDB, err := gdb.DB()
  123. if err != nil {
  124. return fmt.Errorf("db: 取底层 *sql.DB 失败: %w", err)
  125. }
  126. return sqlDB.Close()
  127. }