Gorm
GORM 是一个流行的 Go 语言 ORM (Object-Relational Mapping,对象关系映射) 库,用于将 Go 的结构体映射到关系型数据库的表中。它支持主流的数据库系统,包括 MySQL、PostgreSQL、SQLite 和 Microsoft SQL Server。GORM 提供了一个简单而强大的 API,用于处理数据库的 CRUD 操作(创建、读取、更新、删除),并支持关联、事务、迁移等高级功能。

查看更多相关内容
GORM 中的钩子(Hooks)是如何工作的?GORM 的钩子(Hooks)机制允许在数据库操作的不同阶段执行自定义逻辑。钩子函数会在特定操作之前或之后自动调用。
## 钩子类型
### 对象级钩子
这些钩子在对象级别触发,适用于单个对象的操作。
```go
type User struct {
gorm.Model
Name string
Email string
Age int
}
// 创建前钩子
func (u *User) BeforeCreate(tx *gorm.DB) error {
fmt.Println("BeforeCreate: 准备创建用户")
if u.Name == "" {
return errors.New("用户名不能为空")
}
return nil
}
// 创建后钩子
func (u *User) AfterCreate(tx *gorm.DB) error {
fmt.Println("AfterCreate: 用户创建成功")
return nil
}
// 更新前钩子
func (u *User) BeforeUpdate(tx *gorm.DB) error {
fmt.Println("BeforeUpdate: 准备更新用户")
return nil
}
// 更新后钩子
func (u *User) AfterUpdate(tx *gorm.DB) error {
fmt.Println("AfterUpdate: 用户更新成功")
return nil
}
// 保存前钩子(Create 和 Update 都会触发)
func (u *User) BeforeSave(tx *gorm.DB) error {
fmt.Println("BeforeSave: 准备保存用户")
return nil
}
// 保存后钩子(Create 和 Update 都会触发)
func (u *User) AfterSave(tx *gorm.DB) error {
fmt.Println("AfterSave: 用户保存成功")
return nil
}
// 删除前钩子
func (u *User) BeforeDelete(tx *gorm.DB) error {
fmt.Println("BeforeDelete: 准备删除用户")
return nil
}
// 删除后钩子
func (u *User) AfterDelete(tx *gorm.DB) error {
fmt.Println("AfterDelete: 用户删除成功")
return nil
}
// 查询后钩子
func (u *User) AfterFind(tx *gorm.DB) error {
fmt.Println("AfterFind: 用户查询成功")
return nil
}
```
## 查询级钩子
这些钩子在查询级别触发,适用于批量操作。
```go
// 查询前钩子
func (u *User) BeforeQuery(tx *gorm.DB) error {
fmt.Println("BeforeQuery: 准备查询用户")
return nil
}
// 查询后钩子
func (u *User) AfterQuery(tx *gorm.DB) error {
fmt.Println("AfterQuery: 用户查询成功")
return nil
}
```
## 钩子执行顺序
### 创建操作
1. BeforeCreate
2. BeforeSave
3. 执行 INSERT
4. AfterSave
5. AfterCreate
### 更新操作
1. BeforeUpdate
2. BeforeSave
3. 执行 UPDATE
4. AfterSave
5. AfterUpdate
### 删除操作
1. BeforeDelete
2. 执行 DELETE
3. AfterDelete
### 查询操作
1. BeforeQuery
2. 执行 SELECT
3. AfterQuery / AfterFind
## 实际应用场景
### 1. 数据验证
```go
func (u *User) BeforeCreate(tx *gorm.DB) error {
if u.Age < 0 {
return errors.New("年龄不能为负数")
}
if !strings.Contains(u.Email, "@") {
return errors.New("邮箱格式不正确")
}
return nil
}
```
### 2. 自动生成字段
```go
func (u *User) BeforeCreate(tx *gorm.DB) error {
if u.ID == 0 {
u.ID = generateUUID()
}
return nil
}
```
### 3. 数据加密
```go
func (u *User) BeforeSave(tx *gorm.DB) error {
if u.Password != "" {
u.Password = hashPassword(u.Password)
}
return nil
}
```
### 4. 时间戳管理
```go
func (u *User) BeforeCreate(tx *gorm.DB) error {
now := time.Now()
u.CreatedAt = now
u.UpdatedAt = now
return nil
}
func (u *User) BeforeUpdate(tx *gorm.DB) error {
u.UpdatedAt = time.Now()
return nil
}
```
### 5. 审计日志
```go
func (u *User) AfterCreate(tx *gorm.DB) error {
log.Printf("用户创建: ID=%d, Name=%s", u.ID, u.Name)
return nil
}
func (u *User) AfterUpdate(tx *gorm.DB) error {
log.Printf("用户更新: ID=%d, Name=%s", u.ID, u.Name)
return nil
}
func (u *User) AfterDelete(tx *gorm.DB) error {
log.Printf("用户删除: ID=%d", u.ID)
return nil
}
```
### 6. 软删除处理
```go
func (u *User) BeforeDelete(tx *gorm.DB) error {
// 软删除时更新删除时间
if tx.Statement.Unscoped {
// 真正删除
return nil
}
// 软删除,更新 DeletedAt
return nil
}
```
## 钩子中的事务操作
在钩子中可以访问事务上下文:
```go
func (u *User) AfterCreate(tx *gorm.DB) error {
// 在同一个事务中创建关联记录
profile := Profile{
UserID: u.ID,
Bio: "新用户",
}
return tx.Create(&profile).Error
}
```
## 跳过钩子
有时需要跳过钩子执行:
```go
// 跳过所有钩子
db.Session(&gorm.Session{SkipHooks: true}).Create(&user)
// 使用 Unscoped 跳过软删除钩子
db.Unscoped().Delete(&user)
```
## 钩子返回错误
钩子返回错误会阻止操作继续执行:
```go
func (u *User) BeforeCreate(tx *gorm.DB) error {
if u.Name == "admin" {
return errors.New("不允许创建 admin 用户")
}
return nil
}
// 使用
err := db.Create(&user).Error
if err != nil {
fmt.Println("创建失败:", err)
}
```
## 注意事项
1. **性能影响**:钩子会增加操作开销,避免在钩子中执行耗时操作
2. **事务一致性**:钩子中的操作与主操作在同一事务中,注意错误处理
3. **避免循环**:钩子中不要触发会导致无限循环的操作
4. **错误处理**:钩子返回错误会阻止操作,确保正确处理错误
5. **批量操作**:批量操作时,钩子会对每条记录执行,注意性能
6. **测试覆盖**:钩子逻辑需要充分的单元测试覆盖
## 最佳实践
1. **保持简单**:钩子逻辑应该简单明了,避免复杂业务逻辑
2. **单一职责**:每个钩子只做一件事
3. **日志记录**:在钩子中添加适当的日志记录
4. **错误信息**:提供清晰的错误信息,便于调试
5. **文档说明**:为复杂的钩子逻辑添加注释说明
服务端 · 3月7日 19:44
什么是 GORM,它的核心特性有哪些?GORM(Go Object-Relational Mapping)是 Go 语言中最流行的 ORM 库之一,它提供了强大的数据库操作功能。
## 核心特性
1. **全功能 ORM**:支持关联、钩子、预加载、事务等功能
2. **关联关系**:支持 Has One、Has Many、Many To Many、Belongs To 等关联
3. **钩子函数**:支持 Before/After Create、Save、Update、Delete、Find 等钩子
4. **自动迁移**:支持自动创建表、添加外键、索引等
5. **链式调用**:提供流畅的 API 设计,支持链式操作
6. **多数据库支持**:MySQL、PostgreSQL、SQLite、SQL Server 等
## 基本使用示例
```go
// 定义模型
type User struct {
gorm.Model
Name string
Email string `gorm:"type:varchar(100);uniqueIndex"`
Age int
}
// 连接数据库
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
// 创建记录
db.Create(&User{Name: "John", Email: "john@example.com", Age: 30})
// 查询记录
var user User
db.First(&user, 1) // 查询主键为1的记录
// 更新记录
db.Model(&user).Update("Age", 31)
// 删除记录
db.Delete(&user)
```
## 常用方法
- `First()`: 查询第一条记录
- `Find()`: 查询多条记录
- `Where()`: 添加查询条件
- `Create()`: 创建记录
- `Update()`: 更新记录
- `Delete()`: 删除记录
- `Preload()`: 预加载关联数据
- `Joins()`: 执行 JOIN 查询
GORM 通过约定优于配置的设计理念,让开发者能够快速进行数据库操作,同时保持代码的简洁性和可维护性。
服务端 · 3月6日 23:39
GORM 中有哪些性能优化技巧?GORM 提供了多种性能优化技巧,可以帮助开发者提高数据库操作的效率。
## 查询优化
### 1. 选择特定字段
只查询需要的字段,减少数据传输量:
```go
// 不推荐:查询所有字段
var users []User
db.Find(&users)
// 推荐:只查询需要的字段
var users []User
db.Select("id", "name", "email").Find(&users)
```
### 2. 使用索引
为常用查询条件创建索引:
```go
type User struct {
gorm.Model
Name string `gorm:"index:idx_name"`
Email string `gorm:"uniqueIndex"`
Age int `gorm:"index:idx_age"`
}
```
### 3. 分页查询
使用 Limit 和 Offset 实现分页:
```go
// 基础分页
page := 1
pageSize := 10
offset := (page - 1) * pageSize
var users []User
db.Limit(pageSize).Offset(offset).Find(&users)
// 使用游标分页(更高效)
var users []User
db.Where("id > ?", lastID).Limit(pageSize).Find(&users)
```
### 4. 避免 N+1 查询
使用 Preload 预加载关联数据:
```go
// 不推荐:N+1 查询
var users []User
db.Find(&users)
for _, user := range users {
var posts []Post
db.Where("user_id = ?", user.ID).Find(&posts)
}
// 推荐:使用 Preload
var users []User
db.Preload("Posts").Find(&users)
// 条件预加载
db.Preload("Posts", "status = ?", "published").Find(&users)
// 嵌套预加载
db.Preload("Posts.Comments").Find(&users)
```
### 5. 使用 Pluck 提取单列
当只需要单列数据时使用 Pluck:
```go
// 不推荐
var users []User
db.Find(&users)
var names []string
for _, user := range users {
names = append(names, user.Name)
}
// 推荐
var names []string
db.Model(&User{}).Pluck("name", &names)
```
## 批量操作优化
### 1. 批量插入
使用 CreateInBatches 进行批量插入:
```go
// 不推荐:循环插入
for _, user := range users {
db.Create(&user)
}
// 推荐:批量插入
db.CreateInBatches(users, 100)
```
### 2. 批量更新
使用批量更新代替循环更新:
```go
// 不推荐
for _, user := range users {
db.Model(&user).Update("status", "active")
}
// 推荐
db.Model(&User{}).Where("id IN ?", userIDs).Update("status", "active")
```
### 3. 批量删除
使用批量删除代替循环删除:
```go
// 不推荐
for _, user := range users {
db.Delete(&user)
}
// 推荐
db.Where("id IN ?", userIDs).Delete(&User{})
```
## 连接池优化
### 配置连接池
```go
sqlDB, err := db.DB()
if err != nil {
panic(err)
}
// 设置空闲连接池中的最大连接数
sqlDB.SetMaxIdleConns(10)
// 设置数据库的最大打开连接数
sqlDB.SetMaxOpenConns(100)
// 设置连接可复用的最长时间
sqlDB.SetConnMaxLifetime(time.Hour)
```
## 查询缓存
### 使用 GORM 的缓存插件
```go
// 使用 gorm-cache 插件
import "github.com/go-gorm/caches"
db.Use(caches.New(caches.Config{
Redis: redisClient,
ExpireTime: 10 * time.Minute,
}))
```
## 原生 SQL 优化
### 使用原生 SQL 处理复杂查询
```go
// 复杂查询使用原生 SQL
var results []struct {
UserName string
PostCount int
}
db.Raw(`
SELECT u.name as user_name, COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
WHERE u.age > ?
GROUP BY u.id
HAVING COUNT(p.id) > ?
`, 18, 5).Scan(&results)
```
## 事务优化
### 减少事务范围
```go
// 不推荐:大事务
tx := db.Begin()
// ... 大量操作 ...
tx.Commit()
// 推荐:小事务
db.Transaction(func(tx *gorm.DB) error {
// 只包含必要的操作
return nil
})
```
## 数据库设计优化
### 1. 合理使用外键
```go
type Order struct {
gorm.Model
UserID uint `gorm:"index"`
User User `gorm:"foreignKey:UserID;references:ID"`
}
```
### 2. 使用适当的数据类型
```go
type User struct {
ID uint `gorm:"primaryKey"`
Age int8 `gorm:"type:tinyint"` // 使用 tinyint 节省空间
Status string `gorm:"type:char(1)"` // 固定长度字符串
CreatedAt time.Time
}
```
### 3. 分区表
对于大表,考虑使用分区:
```sql
-- MySQL 分表示例
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
created_at DATETIME,
-- 其他字段
) PARTITION BY RANGE (YEAR(created_at)) (
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025),
PARTITION pmax VALUES LESS THAN MAXVALUE
);
```
## 监控和调试
### 1. 启用日志
```go
// 开发环境启用详细日志
db.Logger = logger.Default.LogMode(logger.Info)
// 生产环境只记录慢查询
db.Logger = logger.Default.LogMode(logger.Silent)
db.Callback().Query().Before("gorm:query").Register("slow_query", func(db *gorm.DB) {
start := time.Now()
db.Statement.Callbacks().Query().After("gorm:query").Register("log_slow_query", func(db *gorm.DB) {
if time.Since(start) > time.Second {
log.Printf("Slow query: %s", db.Statement.SQL.String())
}
})
})
```
### 2. 使用 Explain 分析查询
```go
var users []User
result := db.Explain("SELECT * FROM users WHERE age > ?", 18)
fmt.Println(result)
```
## 性能测试
### Benchmark 测试
```go
func BenchmarkGORMQuery(b *testing.B) {
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
b.ResetTimer()
for i := 0; i < b.N; i++ {
var users []User
db.Select("id", "name").Limit(10).Find(&users)
}
}
```
## 最佳实践总结
1. **只查询需要的字段**:使用 Select 指定字段
2. **合理使用索引**:为常用查询条件创建索引
3. **避免 N+1 查询**:使用 Preload 预加载关联数据
4. **批量操作**:使用批量插入、更新、删除
5. **配置连接池**:根据应用负载调整连接池参数
6. **使用分页**:避免一次性加载大量数据
7. **优化事务**:保持事务范围尽可能小
8. **监控性能**:启用日志和慢查询监控
9. **使用原生 SQL**:复杂查询使用原生 SQL
10. **定期维护**:定期分析表、优化索引
## 常见性能问题
### Q: 如何解决 N+1 查询问题?
A: 使用 Preload 预加载关联数据,或者使用 Joins 进行关联查询。
### Q: 批量插入性能慢怎么办?
A: 使用 CreateInBatches 方法,并合理设置批次大小(通常 100-1000)。
### Q: 如何优化慢查询?
A: 使用 Explain 分析查询计划,检查索引使用情况,优化查询条件。
### Q: 连接池应该设置多大?
A: 根据应用并发量和数据库服务器性能调整,通常 MaxOpenConns 设置为 CPU 核心数的 2-4 倍。
服务端 · 3月6日 21:37
GORM 中如何使用原生 SQL?GORM 提供了多种方法来处理原生 SQL 查询,当 ORM 的功能无法满足需求时,可以使用原生 SQL。
## 执行原生 SQL
### Exec() - 执行不返回数据的 SQL
```go
// 创建表
db.Exec("CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100))")
// 插入数据
db.Exec("INSERT INTO users (name, email) VALUES (?, ?)", "John", "john@example.com")
// 更新数据
db.Exec("UPDATE users SET name = ? WHERE id = ?", "Jane", 1)
// 删除数据
db.Exec("DELETE FROM users WHERE id = ?", 1)
```
### Raw() - 执行返回数据的 SQL
```go
// 查询单条记录
var user User
db.Raw("SELECT * FROM users WHERE id = ?", 1).Scan(&user)
// 查询多条记录
var users []User
db.Raw("SELECT * FROM users WHERE age > ?", 18).Scan(&users)
// 查询特定字段
var results []struct {
Name string
Email string
}
db.Raw("SELECT name, email FROM users").Scan(&results)
```
## 原生 SQL 与 ORM 混合使用
### 在查询中使用原生 SQL
```go
// 使用原生 SQL 作为子查询
var users []User
db.Where("age > (?)", db.Raw("SELECT AVG(age) FROM users")).Find(&users)
// 使用原生 SQL 条件
db.Where(db.Raw("DATE(created_at) = ?", "2024-01-01")).Find(&users)
```
### 使用 Joins 执行原生 SQL
```go
var users []User
db.Joins("LEFT JOIN profiles ON users.id = profiles.user_id").
Where("profiles.status = ?", "active").
Find(&users)
```
## 高级原生 SQL 查询
### 复杂聚合查询
```go
type Result struct {
UserName string
PostCount int
}
var results []Result
db.Raw(`
SELECT u.name as user_name, COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
WHERE u.age > ?
GROUP BY u.id
HAVING COUNT(p.id) > ?
ORDER BY post_count DESC
LIMIT ?
`, 18, 5, 10).Scan(&results)
```
### 使用 CTE (Common Table Expression)
```go
var results []struct {
UserName string
TotalAmount float64
}
db.Raw(`
WITH user_orders AS (
SELECT user_id, SUM(amount) as total
FROM orders
WHERE created_at > ?
GROUP BY user_id
)
SELECT u.name as user_name, o.total as total_amount
FROM users u
JOIN user_orders o ON u.id = o.user_id
`, time.Now().AddDate(0, -1, 0)).Scan(&results)
```
### 使用窗口函数
```go
var results []struct {
UserName string
Amount float64
Rank int
}
db.Raw(`
SELECT
u.name as user_name,
o.amount,
RANK() OVER (PARTITION BY o.user_id ORDER BY o.amount DESC) as rank
FROM orders o
JOIN users u ON o.user_id = u.id
`).Scan(&results)
```
## 原生 SQL 事务
### 在事务中使用原生 SQL
```go
err := db.Transaction(func(tx *gorm.DB) error {
// 使用原生 SQL 插入
if err := tx.Exec("INSERT INTO users (name) VALUES (?)", "John").Error; err != nil {
return err
}
// 使用原生 SQL 更新
if err := tx.Exec("UPDATE users SET email = ? WHERE name = ?", "john@example.com", "John").Error; err != nil {
return err
}
return nil
})
```
## 命名参数
### 使用命名参数
```go
// MySQL
db.NamedExec("INSERT INTO users (name, email) VALUES (:name, :email)",
map[string]interface{}{"name": "John", "email": "john@example.com"})
// PostgreSQL
db.NamedExec("INSERT INTO users (name, email) VALUES ($name, $email)",
map[string]interface{}{"name": "John", "email": "john@example.com"})
```
### 使用结构体作为参数
```go
type UserParams struct {
Name string `db:"name"`
Email string `db:"email"`
}
params := UserParams{Name: "John", Email: "john@example.com"}
db.NamedExec("INSERT INTO users (name, email) VALUES (:name, :email)", params)
```
## 原生 SQL 最佳实践
### 1. 使用参数化查询防止 SQL 注入
```go
// 不安全
db.Raw(fmt.Sprintf("SELECT * FROM users WHERE name = '%s'", userInput))
// 安全
db.Raw("SELECT * FROM users WHERE name = ?", userInput)
```
### 2. 使用 Scan 映射结果
```go
type UserSummary struct {
Name string
Count int
}
var summaries []UserSummary
db.Raw(`
SELECT name, COUNT(*) as count
FROM users
GROUP BY name
`).Scan(&summaries)
```
### 3. 使用 Rows 处理大量数据
```go
rows, err := db.Raw("SELECT * FROM users").Rows()
if err != nil {
panic(err)
}
defer rows.Close()
for rows.Next() {
var user User
if err := db.ScanRows(rows, &user); err != nil {
panic(err)
}
// 处理用户数据
}
```
### 4. 使用 Row 处理单条记录
```go
var name string
row := db.Raw("SELECT name FROM users WHERE id = ?", 1).Row()
if err := row.Scan(&name); err != nil {
panic(err)
}
```
## 性能优化
### 1. 使用索引提示
```go
// MySQL 索引提示
db.Raw("SELECT * FROM users USE INDEX (idx_name) WHERE name = ?", "John").Scan(&users)
// PostgreSQL 索引提示
db.Raw("SELECT * FROM users WHERE name = ?", "John").Scan(&users)
```
### 2. 批量操作
```go
// 批量插入
values := []interface{}{"John", "john@example.com"}, {"Jane", "jane@example.com"}
query := "INSERT INTO users (name, email) VALUES "
placeholders := make([]string, len(values))
for i := range values {
placeholders[i] = "(?, ?)"
}
query += strings.Join(placeholders, ", ")
args := make([]interface{}, 0, len(values)*2)
for _, v := range values {
args = append(args, v.([]interface{})...)
}
db.Exec(query, args...)
```
## 注意事项
1. **SQL 注入**:始终使用参数化查询,不要拼接 SQL 字符串
2. **数据库兼容性**:不同数据库的 SQL 语法可能不同
3. **错误处理**:正确处理原生 SQL 执行的错误
4. **性能考虑**:复杂的原生 SQL 可能影响性能,需要优化
5. **可维护性**:原生 SQL 代码较难维护,尽量使用 ORM
6. **事务一致性**:在事务中使用原生 SQL 时要注意事务的一致性
## 常见问题
### Q: 什么时候应该使用原生 SQL?
A: 当 ORM 无法满足需求时,如复杂聚合查询、窗口函数、性能优化等场景。
### Q: 如何防止 SQL 注入?
A: 始终使用参数化查询(? 或命名参数),不要直接拼接 SQL 字符串。
### Q: 原生 SQL 和 ORM 混合使用会影响性能吗?
A: 不会,GORM 会正确处理混合查询,但要注意查询的复杂度。
### Q: 如何处理原生 SQL 的错误?
A: 检查 db.Error 或使用 Error() 方法获取错误信息。
服务端 · 3月6日 21:37
GORM 中常用的查询方法有哪些?GORM 提供了多种查询方法,以下是常用查询方法的详细说明:
## 基础查询方法
### 1. First() - 查询第一条记录
```go
var user User
db.First(&user) // SELECT * FROM users ORDER BY id LIMIT 1
db.First(&user, 10) // SELECT * FROM users WHERE id = 10
```
### 2. Last() - 查询最后一条记录
```go
var user User
db.Last(&user) // SELECT * FROM users ORDER BY id DESC LIMIT 1
```
### 3. Find() - 查询多条记录
```go
var users []User
db.Find(&users) // SELECT * FROM users
db.Find(&users, []int{1, 2, 3}) // SELECT * FROM users WHERE id IN (1, 2, 3)
```
### 4. Take() - 获取一条记录,不指定排序
```go
var user User
db.Take(&user) // SELECT * FROM users LIMIT 1
```
## 条件查询
### Where() - 添加查询条件
```go
db.Where("name = ?", "John").First(&user)
db.Where("name = ? AND age >= ?", "John", 18).Find(&users)
db.Where(map[string]interface{}{"name": "John", "age": 30}).First(&user)
db.Where(&User{Name: "John"}).First(&user)
```
## 高级查询
### 链式查询
```go
db.Where("age > ?", 18).
Order("age DESC").
Limit(10).
Offset(5).
Find(&users)
```
### Or 条件
```go
db.Where("name = ?", "John").Or("name = ?", "Jane").Find(&users)
```
### Not 条件
```go
db.Not("name = ?", "John").Find(&users)
```
### In 查询
```go
db.Where("id IN ?", []int{1, 2, 3}).Find(&users)
```
### Like 查询
```go
db.Where("name LIKE ?", "%John%").Find(&users)
```
### Between 查询
```go
db.Where("age BETWEEN ? AND ?", 18, 30).Find(&users)
```
## 排序和分页
### Order() - 排序
```go
db.Order("age DESC").Find(&users)
db.Order("age DESC, name ASC").Find(&users)
```
### Limit() - 限制记录数
```go
db.Limit(10).Find(&users)
```
### Offset() - 偏移量
```go
db.Offset(10).Limit(10).Find(&users) // 分页查询
```
## 选择特定字段
### Select() - 选择字段
```go
db.Select("name", "email").Find(&users)
db.Select("name, email").Find(&users)
```
## 聚合查询
### Count() - 计数
```go
var count int64
db.Model(&User{}).Where("age > ?", 18).Count(&count)
```
### Pluck() - 提取单列
```go
var names []string
db.Model(&User{}).Pluck("name", &names)
```
## 原生 SQL
### Raw() - 执行原生 SQL
```go
db.Raw("SELECT * FROM users WHERE age > ?", 18).Scan(&users)
```
### Exec() - 执行原生 SQL(不返回数据)
```go
db.Exec("UPDATE users SET age = age + 1 WHERE id = ?", 1)
```
## 注意事项
1. **First() vs Take()**: First() 会按主键排序,Take() 不排序
2. **查询条件**: 使用参数化查询(?)防止 SQL 注入
3. **性能优化**: 合理使用索引、限制查询字段、避免 N+1 查询
4. **错误处理**: 检查 `db.Error` 来处理查询错误
5. **预加载**: 使用 Preload() 避免关联查询的性能问题
服务端 · 3月6日 21:37
GORM 中如何处理错误?GORM 提供了完善的错误处理机制,正确处理错误对于构建稳定的应用程序至关重要。
## 错误处理基础
### 检查错误
GORM 的所有操作都会返回错误,需要检查 `db.Error`:
```go
// 创建记录
if err := db.Create(&user).Error; err != nil {
log.Printf("创建用户失败: %v", err)
return err
}
// 查询记录
if err := db.First(&user, 1).Error; err != nil {
log.Printf("查询用户失败: %v", err)
return err
}
// 更新记录
if err := db.Model(&user).Update("name", "John").Error; err != nil {
log.Printf("更新用户失败: %v", err)
return err
}
// 删除记录
if err := db.Delete(&user).Error; err != nil {
log.Printf("删除用户失败: %v", err)
return err
}
```
## 常见错误类型
### 1. 记录未找到错误
```go
var user User
result := db.First(&user, 999)
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
log.Println("用户不存在")
// 处理记录未找到的情况
} else if result.Error != nil {
log.Printf("查询失败: %v", result.Error)
return result.Error
}
```
### 2. 连接错误
```go
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Printf("数据库连接失败: %v", err)
panic(err)
}
// 测试连接
sqlDB, err := db.DB()
if err != nil {
log.Printf("获取数据库连接失败: %v", err)
panic(err)
}
if err := sqlDB.Ping(); err != nil {
log.Printf("数据库 ping 失败: %v", err)
panic(err)
}
```
### 3. 约束错误
```go
// 唯一约束冲突
user := User{Email: "existing@example.com"}
if err := db.Create(&user).Error; err != nil {
if strings.Contains(err.Error(), "Duplicate entry") {
log.Println("邮箱已存在")
return errors.New("邮箱已存在")
}
return err
}
// 外键约束错误
if err := db.Create(&order).Error; err != nil {
if strings.Contains(err.Error(), "foreign key constraint") {
log.Println("用户不存在")
return errors.New("用户不存在")
}
return err
}
```
### 4. 验证错误
```go
// 使用钩子进行验证
func (u *User) BeforeCreate(tx *gorm.DB) error {
if u.Name == "" {
return errors.New("用户名不能为空")
}
if !strings.Contains(u.Email, "@") {
return errors.New("邮箱格式不正确")
}
return nil
}
// 创建时处理验证错误
user := User{Name: "", Email: "invalid"}
if err := db.Create(&user).Error; err != nil {
log.Printf("验证失败: %v", err)
return err
}
```
## 错误处理最佳实践
### 1. 使用事务处理错误
```go
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&user).Error; err != nil {
return err // 自动回滚
}
if err := tx.Create(&profile).Error; err != nil {
return err // 自动回滚
}
return nil // 自动提交
})
if err != nil {
log.Printf("事务失败: %v", err)
return err
}
```
### 2. 自定义错误处理
```go
type DBError struct {
Code int
Message string
Err error
}
func (e *DBError) Error() string {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
}
func HandleDBError(err error) error {
if err == nil {
return nil
}
if errors.Is(err, gorm.ErrRecordNotFound) {
return &DBError{Code: 404, Message: "记录未找到", Err: err}
}
if strings.Contains(err.Error(), "Duplicate entry") {
return &DBError{Code: 409, Message: "记录已存在", Err: err}
}
if strings.Contains(err.Error(), "foreign key constraint") {
return &DBError{Code: 400, Message: "关联记录不存在", Err: err}
}
return &DBError{Code: 500, Message: "数据库错误", Err: err}
}
// 使用
if err := db.Create(&user).Error; err != nil {
return HandleDBError(err)
}
```
### 3. 错误日志记录
```go
// 配置日志
import "gorm.io/gorm/logger"
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: time.Second,
LogLevel: logger.Error,
IgnoreRecordNotFoundError: true,
Colorful: true,
},
)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: newLogger,
})
// 自定义错误处理
func logError(operation string, err error) {
if err != nil {
log.Printf("%s 失败: %v", operation, err)
// 可以发送到监控系统
// metrics.ErrorCounter.Inc()
}
}
// 使用
if err := db.Create(&user).Error; err != nil {
logError("创建用户", err)
}
```
### 4. 重试机制
```go
func withRetry(maxRetries int, fn func() error) error {
var lastErr error
for i := 0; i < maxRetries; i++ {
if err := fn(); err != nil {
lastErr = err
// 如果是连接错误,可以重试
if isConnectionError(err) {
time.Sleep(time.Second * time.Duration(i+1))
continue
}
// 其他错误不重试
return err
}
return nil
}
return fmt.Errorf("重试 %d 次后仍然失败: %v", maxRetries, lastErr)
}
func isConnectionError(err error) bool {
return strings.Contains(err.Error(), "connection") ||
strings.Contains(err.Error(), "timeout")
}
// 使用
err := withRetry(3, func() error {
return db.Create(&user).Error
})
```
## 错误恢复
### 使用 recover 处理 panic
```go
func safeDBOperation(db *gorm.DB, operation string, fn func(*gorm.DB) error) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("%s 发生 panic: %v", operation, r)
log.Printf("Panic recovered: %v", r)
}
}()
return fn(db)
}
// 使用
err := safeDBOperation(db, "创建用户", func(db *gorm.DB) error {
return db.Create(&user).Error
})
```
## 错误处理中间件
### 创建错误处理中间件
```go
type DBHandler struct {
db *gorm.DB
}
func (h *DBHandler) HandleError(err error) error {
if err == nil {
return nil
}
if errors.Is(err, gorm.ErrRecordNotFound) {
return &AppError{
Code: http.StatusNotFound,
Message: "资源不存在",
Err: err,
}
}
return &AppError{
Code: http.StatusInternalServerError,
Message: "数据库操作失败",
Err: err,
}
}
func (h *DBHandler) CreateUser(user *User) error {
if err := h.db.Create(user).Error; err != nil {
return h.HandleError(err)
}
return nil
}
```
## 注意事项
1. **始终检查错误**:不要忽略任何数据库操作的错误
2. **区分错误类型**:根据不同的错误类型采取不同的处理策略
3. **提供有意义的错误信息**:错误信息应该清晰、具体
4. **记录错误日志**:记录详细的错误信息便于调试
5. **避免暴露敏感信息**:不要将数据库错误直接暴露给用户
6. **使用事务**:对于多个操作,使用事务确保数据一致性
7. **重试机制**:对于临时性错误,可以实现重试机制
8. **监控告警**:设置错误监控和告警机制
## 常见问题
### Q: 如何区分记录未找到和其他错误?
A: 使用 `errors.Is(err, gorm.ErrRecordNotFound)` 来判断是否是记录未找到错误。
### Q: 错误信息应该记录到日志还是返回给用户?
A: 详细错误信息应该记录到日志,返回给用户的应该是简化的、友好的错误信息。
### Q: 如何处理数据库连接断开的情况?
A: 实现重试机制,或者使用连接池自动重连功能。
### Q: 事务中的错误如何处理?
A: 在事务回调中返回错误会自动回滚事务,不需要手动处理。
服务端 · 3月6日 21:37
GORM 中如何使用事务?GORM 提供了强大的事务支持,可以确保多个数据库操作的原子性和一致性。
## 基本事务操作
### 自动事务
GORM 默认会在单个操作中自动管理事务:
```go
// 单个操作自动使用事务
db.Create(&user)
db.Save(&user)
db.Delete(&user)
```
### 手动事务
对于需要多个操作的场景,需要手动管理事务:
```go
// 开始事务
tx := db.Begin()
// 执行操作
if err := tx.Create(&user).Error; err != nil {
// 发生错误,回滚事务
tx.Rollback()
return err
}
if err := tx.Create(&profile).Error; err != nil {
tx.Rollback()
return err
}
// 提交事务
tx.Commit()
```
## 事务方法
### Begin() - 开始事务
```go
tx := db.Begin()
```
### Commit() - 提交事务
```go
tx.Commit()
```
### Rollback() - 回滚事务
```go
tx.Rollback()
```
### RollbackTo() - 回滚到保存点
```go
// 创建保存点
tx.SavePoint("sp1")
// 回滚到保存点
tx.RollbackTo("sp1")
```
## 完整的事务示例
### 银行转账示例
```go
func TransferMoney(db *gorm.DB, fromID, toID uint, amount float64) error {
tx := db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 查询转出账户
var fromAccount Account
if err := tx.Where("id = ?", fromID).First(&fromAccount).Error; err != nil {
tx.Rollback()
return err
}
// 检查余额是否足够
if fromAccount.Balance < amount {
tx.Rollback()
return errors.New("余额不足")
}
// 扣除转出账户余额
if err := tx.Model(&fromAccount).Update("balance", fromAccount.Balance-amount).Error; err != nil {
tx.Rollback()
return err
}
// 查询转入账户
var toAccount Account
if err := tx.Where("id = ?", toID).First(&toAccount).Error; err != nil {
tx.Rollback()
return err
}
// 增加转入账户余额
if err := tx.Model(&toAccount).Update("balance", toAccount.Balance+amount).Error; err != nil {
tx.Rollback()
return err
}
// 记录交易日志
transaction := Transaction{
FromAccountID: fromID,
ToAccountID: toID,
Amount: amount,
Status: "completed",
}
if err := tx.Create(&transaction).Error; err != nil {
tx.Rollback()
return err
}
// 提交事务
return tx.Commit().Error
}
```
## 事务回调
GORM 提供了事务回调方法,可以简化事务处理:
### Transaction() 方法
```go
err := db.Transaction(func(tx *gorm.DB) error {
// 在事务中执行操作
if err := tx.Create(&user).Error; err != nil {
// 返回错误会自动回滚
return err
}
if err := tx.Create(&profile).Error; err != nil {
return err
}
// 返回 nil 会自动提交
return nil
})
if err != nil {
// 事务失败
}
```
### 嵌套事务
```go
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&user).Error; err != nil {
return err
}
// 嵌套事务
return tx.Transaction(func(tx2 *gorm.DB) error {
return tx2.Create(&profile).Error
})
})
```
## 事务选项
### 设置事务隔离级别
```go
tx := db.Begin(&sql.TxOptions{
Isolation: sql.LevelSerializable,
})
```
### 只读事务
```go
tx := db.Begin(&sql.TxOptions{
ReadOnly: true,
})
```
## 事务中的常见操作
### 创建记录
```go
tx.Create(&user)
```
### 更新记录
```go
tx.Model(&user).Update("name", "John")
```
### 删除记录
```go
tx.Delete(&user)
```
### 查询记录
```go
tx.First(&user, 1)
```
### 原生 SQL
```go
tx.Exec("UPDATE users SET name = ?", "John")
```
## 事务错误处理
### 检查事务状态
```go
tx := db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 执行操作
if err := tx.Create(&user).Error; err != nil {
tx.Rollback()
log.Printf("事务失败: %v", err)
return err
}
// 提交事务
if err := tx.Commit().Error; err != nil {
log.Printf("提交失败: %v", err)
return err
}
```
### 事务超时处理
```go
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
tx := db.BeginTx(ctx, nil)
// 执行操作...
```
## 嵌套事务和保存点
### 创建保存点
```go
tx.SavePoint("sp1")
```
### 回滚到保存点
```go
tx.RollbackTo("sp1")
```
### 释放保存点
```go
tx.Exec("RELEASE SAVEPOINT sp1")
```
## 注意事项
1. **事务范围**:事务应该在尽可能小的范围内使用,减少锁定时间
2. **错误处理**:必须正确处理事务中的错误,确保失败时回滚
3. **资源释放**:使用 defer 确保事务在函数退出时被正确处理
4. **隔离级别**:根据业务需求选择合适的事务隔离级别
5. **死锁预防**:避免长时间持有锁,按固定顺序访问资源
6. **性能考虑**:事务会增加数据库开销,避免不必要的事务
## 最佳实践
1. **使用 Transaction() 回调**:简化事务处理代码
2. **保持简短**:事务应该尽可能简短快速
3. **错误处理**:始终检查并处理错误
4. **日志记录**:记录事务的开始、提交和回滚
5. **测试覆盖**:为事务逻辑编写充分的测试用例
6. **避免嵌套**:尽量避免过深的嵌套事务
## 事务隔离级别
### Read Uncommitted(读未提交)
```go
tx := db.Begin(&sql.TxOptions{
Isolation: sql.LevelReadUncommitted,
})
```
### Read Committed(读已提交)
```go
tx := db.Begin(&sql.TxOptions{
Isolation: sql.LevelReadCommitted,
})
```
### Repeatable Read(可重复读)
```go
tx := db.Begin(&sql.TxOptions{
Isolation: sql.LevelRepeatableRead,
})
```
### Serializable(串行化)
```go
tx := db.Begin(&sql.TxOptions{
Isolation: sql.LevelSerializable,
})
```
服务端 · 3月6日 21:37
GORM 中的关联关系有哪些类型?GORM 支持四种主要的关联关系类型,每种关系都有其特定的使用场景和配置方式:
## 1. Belongs To(属于)
**定义**:一个模型属于另一个模型,外键在当前模型中。
**示例**:用户属于某个部门
```go
type Department struct {
ID uint
Name string
}
type User struct {
gorm.Model
Name string
DepartmentID uint
Department Department `gorm:"foreignKey:DepartmentID"`
}
// 使用
var user User
db.Preload("Department").First(&user, 1)
```
**特点**:
- 外键在子模型中
- 一对多关系的"多"方
- 使用 `foreignKey` 标签指定外键字段
## 2. Has One(有一个)
**定义**:一个模型拥有另一个模型,外键在关联模型中。
**示例**:用户有一个信用卡
```go
type CreditCard struct {
gorm.Model
Number string
UserID uint
User User `gorm:"foreignKey:UserID"`
}
type User struct {
gorm.Model
Name string
CreditCard CreditCard
}
// 使用
var user User
db.Preload("CreditCard").First(&user, 1)
```
**特点**:
- 外键在关联模型中
- 一对一关系
- 通常用于强关联的场景
## 3. Has Many(有多个)
**定义**:一个模型拥有多个关联模型,外键在关联模型中。
**示例**:用户有多个订单
```go
type Order struct {
gorm.Model
UserID uint
Amount float64
User User `gorm:"foreignKey:UserID"`
}
type User struct {
gorm.Model
Name string
Orders []Order
}
// 使用
var user User
db.Preload("Orders").First(&user, 1)
// 条件预加载
db.Preload("Orders", "amount > ?", 100).First(&user, 1)
```
**特点**:
- 外键在关联模型中
- 一对多关系
- 最常用的关联类型
## 4. Many To Many(多对多)
**定义**:两个模型之间存在多对多关系,需要通过中间表(连接表)来实现。
**示例**:用户和角色的多对多关系
```go
type User struct {
gorm.Model
Name string
Roles []Role `gorm:"many2many:user_roles;"`
}
type Role struct {
gorm.Model
Name string
Users []User `gorm:"many2many:user_roles;"`
}
// 使用
var user User
db.Preload("Roles").First(&user, 1)
// 添加关联
db.Model(&user).Association("Roles").Append(&Role{Name: "Admin"})
// 删除关联
db.Model(&user).Association("Roles").Delete(&Role{Name: "Admin"})
// 替换关联
db.Model(&user).Association("Roles").Replace(&Role{Name: "User"})
// 清空关联
db.Model(&user).Association("Roles").Clear()
// 计数
count := db.Model(&user).Association("Roles").Count()
```
**特点**:
- 需要中间表(连接表)
- 默认中间表名为:table1_table2
- 可以自定义中间表名和字段
## 自定义关联配置
### 自定义外键
```go
type User struct {
gorm.Model
CreditCards []CreditCard `gorm:"foreignKey:UserRefer"`
}
type CreditCard struct {
gorm.Model
Number string
UserRefer uint
}
```
### 自定义引用键
```go
type User struct {
gorm.Model
Name string `gorm:"index"`
CreditCard CreditCard `gorm:"foreignKey:UserName;references:Name"`
}
type CreditCard struct {
gorm.Model
Number string
UserName string
}
```
### 自定义多对多中间表
```go
type User struct {
gorm.Model
Roles []Role `gorm:"many2many:user_roles;joinForeignKey:UserID;joinReferences:RoleID"`
}
type Role struct {
gorm.Model
Name string
Users []User `gorm:"many2many:user_roles;"`
}
```
## 预加载(Preload)
预加载用于解决 N+1 查询问题:
```go
// 基础预加载
db.Preload("Orders").Find(&users)
// 嵌套预加载
db.Preload("Orders.Items").Find(&users)
// 条件预加载
db.Preload("Orders", "status = ?", "completed").Find(&users)
// 多个预加载
db.Preload("Orders").Preload("CreditCard").Find(&users)
```
## 关联操作
### Association API
```go
// 查找关联
var roles []Role
db.Model(&user).Association("Roles").Find(&roles)
// 添加关联
db.Model(&user).Association("Roles").Append(&Role{Name: "Admin"})
// 删除关联
db.Model(&user).Association("Roles").Delete(&Role{Name: "Admin"})
// 替换关联
db.Model(&user).Association("Roles").Replace([]Role{role1, role2})
// 清空关联
db.Model(&user).Association("Roles").Clear()
// 计数
count := db.Model(&user).Association("Roles").Count()
```
## 注意事项
1. **外键命名**:GORM 默认使用 `{关联模型名}ID` 作为外键名
2. **性能优化**:合理使用预加载避免 N+1 查询
3. **级联删除**:默认情况下,删除主记录不会删除关联记录,需要手动配置
4. **软删除**:GORM 支持软删除,关联查询时需要注意软删除的记录
5. **事务**:复杂的关联操作建议在事务中执行
服务端 · 3月6日 21:37
GORM 如何连接不同的数据库?GORM 支持多种数据库,包括 MySQL、PostgreSQL、SQLite、SQL Server 等。以下是连接不同数据库的方法和注意事项。
## 连接 MySQL
### 基本连接
```go
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
dsn := "user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
```
### 连接参数说明
- `user`: 数据库用户名
- `password`: 数据库密码
- `tcp(127.0.0.1:3306)`: 主机和端口
- `dbname`: 数据库名称
- `charset=utf8mb4`: 字符集
- `parseTime=True`: 解析时间
- `loc=Local`: 时区设置
### 高级配置
```go
dsn := "user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local&timeout=10s&readTimeout=30s&writeTimeout=30s"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
NowFunc: func() time.Time {
return time.Now().Local()
},
})
```
## 连接 PostgreSQL
### 基本连接
```go
import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
dsn := "host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
```
### 连接参数说明
- `host`: 数据库主机
- `user`: 数据库用户名
- `password`: 数据库密码
- `dbname`: 数据库名称
- `port`: 端口号
- `sslmode`: SSL 模式
- `TimeZone`: 时区设置
### 使用 URL 格式
```go
dsn := "postgres://gorm:gorm@localhost:9920/gorm?sslmode=disable&timezone=Asia/Shanghai"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
```
## 连接 SQLite
### 基本连接
```go
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
```
### 内存数据库
```go
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
```
## 连接 SQL Server
### 基本连接
```go
import (
"gorm.io/driver/sqlserver"
"gorm.io/gorm"
)
dsn := "sqlserver://gorm:LoremIpsum86@localhost:9930?database=gorm"
db, err := gorm.Open(sqlserver.Open(dsn), &gorm.Config{})
```
## 连接池配置
### 获取底层 SQL DB
```go
sqlDB, err := db.DB()
if err != nil {
panic("failed to get database connection")
}
```
### 配置连接池参数
```go
// 设置空闲连接池中的最大连接数
sqlDB.SetMaxIdleConns(10)
// 设置数据库的最大打开连接数
sqlDB.SetMaxOpenConns(100)
// 设置连接可复用的最长时间
sqlDB.SetConnMaxLifetime(time.Hour)
// 设置连接最大空闲时间
sqlDB.SetConnMaxIdleTime(10 * time.Minute)
```
## GORM 配置选项
### 基本配置
```go
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
// 跳过默认事务
SkipDefaultTransaction: true,
// 禁用外键约束
DisableForeignKeyConstraintWhenMigrating: true,
// 忽略数据错误
IgnoreRelationshipsWhenMigrating: true,
})
```
### Logger 配置
```go
import "gorm.io/gorm/logger"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
// 自定义 Logger
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: time.Second, // 慢查询阈值
LogLevel: logger.Info, // 日志级别
IgnoreRecordNotFoundError: true, // 忽略记录未找到错误
Colorful: true, // 彩色输出
},
)
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: newLogger,
})
```
### 命名策略配置
```go
import "gorm.io/gorm/schema"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
SingularTable: true, // 使用单数表名
NoLowerCase: true, // 不转换为小写
},
})
```
## 数据库特定配置
### MySQL 特定配置
```go
import "gorm.io/driver/mysql"
db, err := gorm.Open(mysql.New(mysql.Config{
DSN: dsn,
DefaultStringSize: 256, // 默认字符串长度
DisableDatetimePrecision: true, // 禁用 datetime 精度
DontSupportRenameIndex: true, // 不支持重命名索引
DontSupportRenameColumn: true, // 不支持重命名列
SkipInitializeWithVersion: false, // 根据版本自动配置
}), &gorm.Config{})
```
### PostgreSQL 特定配置
```go
import "gorm.io/driver/postgres"
db, err := gorm.Open(postgres.New(postgres.Config{
DSN: dsn,
PreferSimpleProtocol: false, // 禁用 prepared statement
}), &gorm.Config{})
```
## 连接管理最佳实践
### 1. 使用连接池
```go
sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
```
### 2. 测试连接
```go
sqlDB, _ := db.DB()
if err := sqlDB.Ping(); err != nil {
panic("failed to connect to database")
}
```
### 3. 优雅关闭
```go
sqlDB, _ := db.DB()
defer sqlDB.Close()
```
### 4. 连接重试
```go
func connectWithRetry(dsn string, maxRetries int) (*gorm.DB, error) {
var db *gorm.DB
var err error
for i := 0; i < maxRetries; i++ {
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err == nil {
return db, nil
}
time.Sleep(time.Second * time.Duration(i+1))
}
return nil, err
}
```
## 环境变量配置
### 使用环境变量
```go
import "os"
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
os.Getenv("DB_USER"),
os.Getenv("DB_PASSWORD"),
os.Getenv("DB_HOST"),
os.Getenv("DB_PORT"),
os.Getenv("DB_NAME"),
)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
```
## 多数据库连接
### 连接多个数据库
```go
// 主数据库
primaryDB, err := gorm.Open(mysql.Open(primaryDSN), &gorm.Config{})
// 从数据库
replicaDB, err := gorm.Open(mysql.Open(replicaDSN), &gorm.Config{})
// 使用不同的数据库
primaryDB.Create(&user)
replicaDB.First(&user, 1)
```
## 注意事项
1. **连接池大小**:根据应用并发量合理设置连接池大小
2. **超时设置**:设置合理的连接超时和读写超时
3. **错误处理**:正确处理连接错误和查询错误
4. **资源释放**:确保在应用退出时关闭数据库连接
5. **安全性**:不要在代码中硬编码数据库密码
6. **监控**:监控连接池的使用情况,及时发现问题
## 常见问题
### Q: 如何处理连接超时?
A: 在 DSN 中设置 timeout 参数,或使用 context 设置超时。
### Q: 连接池应该设置多大?
A: 根据应用并发量和数据库服务器性能调整,通常 MaxOpenConns 设置为 CPU 核心数的 2-4 倍。
### Q: 如何切换数据库?
A: 只需要更换对应的 driver 和 DSN,其他代码基本不需要修改。
### Q: 如何处理数据库连接泄漏?
A: 使用 defer 确保连接关闭,监控连接池状态,设置合理的连接生命周期。
服务端 · 3月6日 21:37
GORM 中的软删除(Soft Delete)是如何工作的?GORM 支持软删除(Soft Delete)功能,允许在逻辑上删除记录而不真正从数据库中删除它们。
## 软删除基本概念
软删除通过在模型中添加 `DeletedAt` 字段来实现,当执行删除操作时,GORM 不会真正删除记录,而是将 `DeletedAt` 字段设置为当前时间。
## 基本用法
### 启用软删除
```go
type User struct {
gorm.Model
Name string
Email string
}
// gorm.Model 包含了 DeletedAt gorm.DeletedAt 字段
```
### 手动定义软删除字段
```go
type User struct {
ID uint `gorm:"primaryKey"`
Name string
DeletedAt gorm.DeletedAt `gorm:"index"`
}
```
## 软删除操作
### 删除记录
```go
// 软删除
db.Delete(&user)
// 批量软删除
db.Where("age < ?", 18).Delete(&User{})
// 根据主键软删除
db.Delete(&User{}, 1)
```
### 查询记录
默认情况下,GORM 会自动过滤掉已软删除的记录:
```go
// 不会查询到已软删除的记录
var users []User
db.Find(&users)
// 只查询已软删除的记录
db.Unscoped().Find(&users)
// 查询所有记录(包括已软删除的)
db.Unscoped().Find(&users)
```
## 软删除的工作原理
### DeletedAt 字段
```go
type DeletedAt struct {
Time time.Time
Valid bool
}
```
- `Time`: 删除时间
- `Valid`: 是否已删除(true 表示已删除)
### SQL 生成
```go
// 删除操作生成的 SQL
// UPDATE users SET deleted_at = '2024-01-01 12:00:00' WHERE id = 1
// 查询操作生成的 SQL(自动添加条件)
// SELECT * FROM users WHERE deleted_at IS NULL
```
## 高级用法
### 查找已软删除的记录
```go
// 使用 Unscoped
var deletedUsers []User
db.Unscoped().Where("deleted_at IS NOT NULL").Find(&deletedUsers)
// 或者直接查询
db.Unscoped().Find(&deletedUsers)
```
### 恢复已软删除的记录
```go
// 恢复单个记录
var user User
db.Unscoped().First(&user, 1)
db.Model(&user).Update("DeletedAt", nil)
// 批量恢复
db.Unscoped().Model(&User{}).Where("deleted_at IS NOT NULL").Update("DeletedAt", nil)
```
### 永久删除记录
```go
// 永久删除(真正从数据库中删除)
db.Unscoped().Delete(&user)
// 批量永久删除
db.Unscoped().Where("age < ?", 18).Delete(&User{})
```
### 检查记录是否被软删除
```go
var user User
db.First(&user, 1)
if user.DeletedAt.Valid {
fmt.Println("记录已被软删除")
} else {
fmt.Println("记录未被删除")
}
```
## 软删除与关联关系
### 关联查询中的软删除
```go
type User struct {
gorm.Model
Name string
Posts []Post
}
type Post struct {
gorm.Model
Title string
UserID uint
}
// 查询用户时,不会包含已软删除的文章
var user User
db.Preload("Posts").First(&user, 1)
// 查询用户时,包含已软删除的文章
db.Preload("Posts", "deleted_at IS NOT NULL").Unscoped().First(&user, 1)
```
### 级联软删除
```go
type User struct {
gorm.Model
Name string
Posts []Post `gorm:"constraint:OnDelete:SET NULL"`
}
// 删除用户时,相关文章的 UserID 会被设置为 NULL
db.Delete(&user)
```
## 自定义软删除
### 自定义软删除字段名
```go
type User struct {
ID uint `gorm:"primaryKey"`
Name string
DeletedAt time.Time `gorm:"index"`
IsDeleted bool `gorm:"default:false"`
}
// 自定义软删除逻辑
func (u *User) BeforeDelete(tx *gorm.DB) error {
u.IsDeleted = true
return nil
}
```
### 使用不同的软删除策略
```go
type User struct {
ID uint `gorm:"primaryKey"`
Name string
Status string `gorm:"default:'active'"`
}
func (u *User) BeforeDelete(tx *gorm.DB) error {
u.Status = "deleted"
return tx.Save(u).Error
}
```
## 软删除的最佳实践
### 1. 数据审计
```go
type User struct {
gorm.Model
Name string
DeletedBy uint
}
func (u *User) BeforeDelete(tx *gorm.DB) error {
// 记录删除操作者
u.DeletedBy = getCurrentUserID()
return nil
}
```
### 2. 数据恢复
```go
func RestoreUser(db *gorm.DB, userID uint) error {
return db.Transaction(func(tx *gorm.DB) error {
var user User
if err := tx.Unscoped().First(&user, userID).Error; err != nil {
return err
}
return tx.Model(&user).Update("DeletedAt", nil).Error
})
}
```
### 3. 定期清理
```go
func CleanOldDeletedRecords(db *gorm.DB, days int) error {
threshold := time.Now().AddDate(0, 0, -days)
return db.Unscoped().
Where("deleted_at < ?", threshold).
Delete(&User{}).Error
}
```
## 注意事项
1. **唯一索引**:软删除的字段会影响唯一索引的约束
2. **性能影响**:软删除会增加查询条件,可能影响性能
3. **数据清理**:需要定期清理已软删除的旧数据
4. **关联查询**:注意关联查询中的软删除行为
5. **存储空间**:软删除会占用额外的存储空间
6. **业务逻辑**:确保业务逻辑正确处理软删除的记录
## 常见问题
### Q: 软删除会影响唯一索引吗?
A: 会,因为软删除的记录仍然存在于数据库中,可能会违反唯一索引约束。可以使用复合唯一索引来解决。
### Q: 如何批量恢复已软删除的记录?
A: 使用 `Unscoped()` 和 `Update()` 方法将 `DeletedAt` 设置为 `nil`。
### Q: 软删除和硬删除有什么区别?
A: 软删除只是标记记录为已删除,数据仍然存在;硬删除会真正从数据库中删除记录。
### Q: 如何查询特定时间范围内被删除的记录?
A: 使用 `Unscoped()` 和时间范围查询条件。
服务端 · 3月6日 21:37