db.go 6.1 KB

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