vault backup: 2026-04-28 20:56:51
This commit is contained in:
+136
-26
@@ -1,5 +1,5 @@
|
||||
---
|
||||
tags: [GORM, Go, ORM, 日志, Debug, SlowQueryThreshold, Logger]
|
||||
tags: [GORM, Go, ORM, 日志, Debug, SlowQueryThreshold, Logger, TraceID, DryRun, ToSQL]
|
||||
create time: 2026-04-28 00:00
|
||||
---
|
||||
|
||||
@@ -7,7 +7,7 @@ create time: 2026-04-28 00:00
|
||||
|
||||
## 概述
|
||||
|
||||
在生产环境中,你能看到的往往只有两条信息:**API 返回了什么**和**数据库执行了什么**。GORM 内置了灵活的日志系统,让你能够在不同环境下精准控制输出粒度,从「生产静默」到「开发透明」自由切换。
|
||||
在生产环境中,你能看到的往往只有两条信息:**API 返回了什么**和**数据库执行了什么**。GORM 内置了灵活的日志系统,让你能够在不同环境下精准控制输出粒度——从「生产静默」到「开发透明」自由切换。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
@@ -19,9 +19,11 @@ flowchart TD
|
||||
C --> |Warn| F["慢查询 + 错误"]
|
||||
C --> |Info| G["所有 SQL + 耗时<br/>开发推荐"]
|
||||
|
||||
style Start fill:#4FC08D,color:#fff
|
||||
style Info fill:#3B82F6,color:#fff
|
||||
style Panic fill:#EF4444,color:#fff
|
||||
style A fill:#4FC08D,color:#fff
|
||||
style D fill:#EF4444,color:#fff
|
||||
style E fill:#F97316,color:#fff
|
||||
style F fill:#EAB308,color:#fff
|
||||
style G fill:#3B82F6,color:#fff
|
||||
```
|
||||
|
||||
## 日志级别速览
|
||||
@@ -65,7 +67,7 @@ db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||
### 自定义 Logger(对接 Zap / Logrus 等)
|
||||
|
||||
```go
|
||||
// 用 Zap 替代默认日志
|
||||
// 用 Zap 替代默认日志(需 import "context"、"zap")
|
||||
type ZapLogger struct {
|
||||
logger.Interface
|
||||
zapLogger *zap.Logger
|
||||
@@ -180,49 +182,154 @@ func RecordQueryDuration(op, table string, dur time.Duration) {
|
||||
|
||||
## 常见调试技巧
|
||||
|
||||
### 打印最终 SQL(不执行)
|
||||
### 打印最终 SQL(不执行)—— DryRun 模式
|
||||
|
||||
DryRun 模式会构建完整的 SQL 语句并输出到日志,但**不实际连接数据库执行**。这是最安全的调试方式:
|
||||
|
||||
```go
|
||||
// DryRun 模式:构建完整 SQL 并输出,但不实际执行
|
||||
db.Session(&gorm.Session{DryRun: true}).First(&user, 1)
|
||||
// 输出:[info] ... [rows:0] SELECT * FROM users WHERE id = 1 -- dry run
|
||||
// 输出:[info] ... [rows:0] SELECT * FROM users WHERE id = ?
|
||||
// 此时你可以拿到完整的 SQL 去客户端手动验证
|
||||
|
||||
// 配合复杂查询链 —— 确认 Join / Preload 生成的子查询是否正确
|
||||
db.Session(&gorm.Session{
|
||||
DryRun: true,
|
||||
}).Preload("Orders").Joins("Profile").Where("status = ?", "active").Find(&users)
|
||||
```
|
||||
|
||||
### 获取生成的 SQL 字符串
|
||||
> [!question] DryRun vs Debug 有什么区别?
|
||||
>
|
||||
> | 特性 | `DryRun` | `Debug()` |
|
||||
> |------|---------|-----------|
|
||||
> | **是否执行 SQL** | ❌ 不执行 | ✅ 执行 |
|
||||
> | **适用场景** | 审计 SQL 语法、安全审查 | 排查运行时产生的错误 SQL |
|
||||
> | **性能开销** | 零(无网络 IO) | 有正常查询开销 |
|
||||
> | **能否看到关联表 SQL** | ✅ Preload/Joins 都输出 | ✅ 同上 |
|
||||
> >
|
||||
> > **答案**:用不同的场景。想确认 "这条链式调用会生成什么 SQL" → DryRun;想确认 "生产上跑的这条 SQL 到底慢在哪里" → Debug。实际项目中推荐在 CI/CD pipeline 中跑 DryRun 做 SQL 规范校验。
|
||||
|
||||
#### DryRun 在单元测试中的妙用
|
||||
|
||||
```go
|
||||
// 使用 Statement 直接构造并获取 SQL
|
||||
stmt := db.Model(&User{}).Where("name = ?", "john").Statement
|
||||
db.Statement.Build(stmt.DB.Build("WHERE"))
|
||||
fmt.Println(stmt.SQL.String())
|
||||
// SELECT * FROM users WHERE name = 'john'
|
||||
func TestUserQuerySQL(t *testing.T) {
|
||||
// 用 SQLite 内存库创建独立 DB(不需要 MySQL/PG 服务)
|
||||
sqlDB, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
|
||||
sql := sqlDB.ToSQL(func(tx *gorm.DB) *gorm.DB {
|
||||
return tx.Model(&User{}).
|
||||
Where("status = ?", "active").
|
||||
Order("created_at DESC").
|
||||
Limit(10).
|
||||
Find(&User{})
|
||||
})
|
||||
|
||||
// 断言 SQL 中包含关键片段
|
||||
assert.Contains(t, sql, "WHERE status =")
|
||||
assert.Contains(t, sql, "ORDER BY created_at DESC")
|
||||
assert.Contains(t, sql, "LIMIT 10")
|
||||
}
|
||||
```
|
||||
|
||||
### 获取生成的 SQL 字符串(ToSQL)
|
||||
|
||||
GORM v2.4+ 提供了 `ToSQL` 方法,无需开启 DryRun 也能拿到最终的 SQL:
|
||||
|
||||
```go
|
||||
import "gorm.io/gorm/schema"
|
||||
|
||||
// 构建一个独立的 DB 实例(不连接数据库)
|
||||
sqlDB, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
|
||||
// ToSQL 返回执行后的 SQL 字符串(不含参数展开)
|
||||
sql := sqlDB.ToSQL(func(tx *gorm.DB) *gorm.DB {
|
||||
return tx.Model(&User{}).Where("name = ?", "john").Find(&User{})
|
||||
})
|
||||
fmt.Println(sql)
|
||||
// SELECT * FROM `users` WHERE name = 'john'
|
||||
```
|
||||
|
||||
> [!tip] 何时需要拿原始 SQL?
|
||||
> - **生成 SQL 后交给他人审计**:把 ORM 生成的语句导出给 DBA 审查索引使用情况
|
||||
> - **单元测试断言**:对比预期 SQL 是否符合安全规范(如是否包含未参数化的拼接)
|
||||
> - **ORM 到原生迁移**:性能优化时,从 GORM 逐步替换为 Raw SQL 的过渡手段
|
||||
|
||||
### 日志中加入 TraceID(链路追踪)
|
||||
|
||||
在生产环境中,单条 SQL 日志的价值取决于能否关联到完整请求链路。通过 `context.Context` 传递 TraceID 是行业标准做法:
|
||||
|
||||
```go
|
||||
func WithTraceID(db *gorm.DB, traceID string) *gorm.DB {
|
||||
return db.Session(&gorm.Session{
|
||||
Context: context.WithValue(context.Background(), "trace_id", traceID),
|
||||
})
|
||||
type contextKey struct{}
|
||||
|
||||
// 中间件:从 HTTP Header 提取 trace_id 注入 Context
|
||||
func TraceMiddleware(db *gorm.DB) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
traceID := r.Header.Get("X-Trace-Id")
|
||||
if traceID == "" {
|
||||
traceID = generateUUID() // 兜底生成
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), contextKey{}, traceID)
|
||||
|
||||
// 用带 trace_id 的 Context 创建新的 DB 实例
|
||||
tracedDB := db.Session(&gorm.Session{Context: ctx})
|
||||
// 这里可以将 tracedDB 存到 Context 供 handler 使用
|
||||
_ = tracedDB
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义日志输出中包含 trace_id
|
||||
type TracedLogger struct {
|
||||
// GORM 自定义 Logger:在日志中附加 TraceID
|
||||
type TraceLogger struct {
|
||||
logger.Interface
|
||||
}
|
||||
|
||||
func (t TracedLogger) Info(ctx context.Context, msg string, data ...interface{}) {
|
||||
traceID, _ := ctx.Value("trace_id").(string)
|
||||
if traceID != "" {
|
||||
func (t TraceLogger) Info(ctx context.Context, msg string, data ...interface{}) {
|
||||
if traceID, ok := ctx.Value(contextKey{}).(string); ok && traceID != "" {
|
||||
msg = fmt.Sprintf("[trace:%s] %s", traceID, msg)
|
||||
}
|
||||
t.Interface.Info(ctx, msg, data...)
|
||||
}
|
||||
```
|
||||
|
||||
## 日志配置决策图
|
||||
> [!tip] OpenTelemetry 集成要点
|
||||
> 如果你已接入 OpenTelemetry,可以直接复用 SpanContext 中的 trace ID,无需自己定义 context key:
|
||||
> ```go
|
||||
> import "go.opentelemetry.io/otel/trace"
|
||||
>
|
||||
> func extractTraceID(ctx context.Context) string {
|
||||
> span := trace.SpanFromContext(ctx)
|
||||
> if span.SpanContext().IsValid() {
|
||||
> return span.SpanContext().TraceID().String()
|
||||
> }
|
||||
> return ""
|
||||
> }
|
||||
> ```
|
||||
|
||||
### 数据库方言对日志输出的影响
|
||||
|
||||
不同数据库驱动会影响最终生成的 SQL 语法和参数占位符:
|
||||
|
||||
| 驱动 | 参数占位符 | 字符串引号 | 日期格式 |
|
||||
|------|-----------|-----------|---------|
|
||||
| MySQL (`go-sql-driver/mysql`) | `?` | `'单引号'` | `'2026-04-28'` |
|
||||
| PostgreSQL (`pgx`) | `$1`, `$2`... | `'单引号'` | `TIMESTAMP '...'` |
|
||||
| SQLite (`mattn/go-sqlite3`) | `?` | `'单引号'` 或 `"双引号"` | `'...'` |
|
||||
| SQL Server (`microsoft/mssql-go`) | `@p1`, `@p2`... | `'单引号'` | `DATETIME2 '...'` |
|
||||
|
||||
> [!note] DryRun 结果对比示例
|
||||
>
|
||||
> ```go
|
||||
> // MySQL DryRun → SELECT * FROM users WHERE id = ?
|
||||
> // PostgreSQL DryRun → SELECT * FROM users WHERE id = $1
|
||||
> // SQL Server DryRun → SELECT * FROM users WHERE id = @p1
|
||||
> //
|
||||
> // 这意味着测试时要用对应驱动初始化 ToSQL/DryRun,否则拿到的 SQL 无法直接在其他数据库中执行。
|
||||
> ```
|
||||
|
||||
## 常见调试技巧总结
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
@@ -254,9 +361,12 @@ flowchart TD
|
||||
|------|------|---------|
|
||||
| 生产环境日志太多导致磁盘爆满 | Logger 级别设太高 | 生产用 Warn 或 Error |
|
||||
| 日志中没有 TraceID 难以定位 | 没传 Context | 用 Session + Context 传递 |
|
||||
| DryRun 拿到的 SQL 参数是 ? | 预编译语句的参数未展开 | 这是正常行为,参数化查询安全性的体现 |
|
||||
| Debug() 改了全局状态 | Debug 返回新的 *gorm.DB,不影响原实例 | 放心使用,它是纯函数式的 |
|
||||
| DryRun 拿到的 SQL 参数是 `?` | 预编译语句的参数未展开 | 这是正常行为,参数化查询安全性的体现 |
|
||||
| Debug() 影响了其他查询的输出 | 以为会改全局配置 | `Debug()` 返回新 `*gorm.DB` 实例,不影响原对象 |
|
||||
| 慢查询阈值设太低 | 正常查询也被标记为 slow | 先收集基线数据再设定合理阈值 |
|
||||
| PostgreSQL 下 `?` 占位符报错 | PG 使用 `$n` 占位符 | 切换驱动或注意不同驱动的 DryRun 输出差异 |
|
||||
| 自定义 Logger 丢失默认行为 | 忘记实现所有四个方法 | 继承 `logger.Interface`,只覆盖需要的方法 |
|
||||
| 并发场景下 Logger 线程不安全 | 多个 goroutine 同时写日志 | 使用 Zap、Logrus 等并发安全的日志库 |
|
||||
|
||||
## 关联笔记
|
||||
|
||||
|
||||
Reference in New Issue
Block a user