乐闻世界logo
搜索文章和话题

Gorm

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