数据库只更新特定字段的两种方式(先读后写 vs. 动态组织 SQL)-golang SQLx 实现代码(动态组织 SQL)
文章目录
- 数据库只更新特定字段的两种方式(先读后写 vs. 动态组织 SQL)
- go语言例子
- 使用GORM的示例(最常用的Go ORM库)
- 使用SQLx的两种更新方式实现
- golang SQLx 实现代码(动态组织 SQL)
数据库只更新特定字段的两种方式(先读后写 vs. 动态组织 SQL)
方法 | 先读后写 (Fetch-Then-Update) | 动态组织 SQL (Dynamic SQL) |
---|---|---|
原理 | 1. 读取整行数据 2. 修改内存对象 3. 全字段写回 | 动态生成 UPDATE 语句,只更新传入字段 |
网络开销 | 高 (2次DB操作) | 低 (1次DB操作) |
并发安全 | 需额外处理乐观锁 | 天然避免写冲突 |
适用场景 | 强事务一致性需求 | 高频局部更新 |
性能 | 较低 (更新全字段) | 更高 (仅更新变动字段) |
对比项 | 先读后写 | 动态组织 SQL |
---|---|---|
代码简单清晰 | 逻辑直观,易于理解和维护 | 需要动态构建 SQL,代码复杂度较高 |
避免 SQL 拼接 | 无需拼接 SQL,降低 SQL 注入风险 | 需要拼接 SQL,存在 SQL 注入风险 |
保证数据完整性 | 不会意外丢失其他字段的值 | 只更新特定字段,其他字段不受影响 |
便于添加业务逻辑 | 可以在更新前对数据进行复杂处理或验证 | 难以在更新前基于现有数据做复杂处理 |
性能开销 | 需要两次数据库操作(读 + 写),性能较低 | 只需一次数据库操作,性能较高 |
并发问题 | 读取后数据可能被其他进程修改,导致覆盖新数据 | 只锁定和修改需要更新的字段,减少并发冲突 |
资源消耗 | 需要在内存中处理完整记录,资源消耗较高 | 不需要获取和处理完整记录,资源消耗较低 |
适合批量操作 | 不适合批量操作,性能较差 | 适合批量操作,可以一次性更新多条记录的特定字段 |
性能开销 | 需要两次数据库操作(读 + 写),性能较低 | 只需一次数据库操作,性能较高 |
并发问题 | 读取后数据可能被其他进程修改,导致覆盖新数据 | 只锁定和修改需要更新的字段,减少并发冲突 |
资源消耗 | 需要在内存中处理完整记录,资源消耗较高 | 不需要获取和处理完整记录,资源消耗较低 |
SQL 拼接复杂 | 无需拼接 SQL,降低 SQL 注入风险 | 需要拼接 SQL,存在 SQL 注入风险 |
业务逻辑受限 | 可以在更新前对数据进行复杂处理或验证 | 难以在更新前基于现有数据做复杂处理 |
-
方法一:先读后写(先查询出当前完整记录,然后合并用户提交的字段,最后用完整的结构体更新整个记录)
-
优点
代码简单清晰:逻辑直观,易于理解和维护,直接使用结构体更新,不易出错。 -
缺点
性能开销:需要两次数据库操作(读+写)
并发问题:如果读取后数据被其他进程修改,可能导致覆盖新数据
-
-
方法二:动态组织 SQL(根据用户提交的字段,动态生成只更新这些字段的 SQL 语句)
-
优点
性能更高:只需一次数据库操作
减少并发冲突:只锁定和修改需要更新的字段
资源消耗低:不需要获取和处理完整记录
适合批量操作:可以一次性更新多条记录的特定字段 -
缺点
SQL拼接复杂:需要动态构建SQL语句,容易出错。代码复杂,需要构建动态 SQL 和对应的参数列表,容易出错。
安全风险:如不正确处理,可能导致SQL注入
业务逻辑受限:难以在更新前基于现有数据做复杂处理
-
推荐方案:动态组织 SQL
尤其适合大表单的单字段更新,减少网络开销和数据库负载。
go语言例子
使用GORM的示例(最常用的Go ORM库)
package mainimport ("fmt""log""gorm.io/driver/mysql""gorm.io/gorm"
)type User struct {ID uintName stringEmail stringAge intAddress string
}func main() {// 连接数据库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{})if err != nil {log.Fatal(err)}// 方式1: 动态更新 - GORM自动生成仅更新指定字段的SQLresult := db.Model(&User{ID: 1}).Updates(map[string]interface{}{"name": "New Name","age": 30,})if result.Error != nil {log.Fatal(result.Error)}fmt.Printf("Updated %d records
", result.RowsAffected)// 方式2: 先读后写 - 适合复杂业务逻辑var user Userif err := db.First(&user, 1).Error; err != nil {log.Fatal(err)}// 更新字段user.Name = "Another Name"user.Age = 35// 业务逻辑验证if user.Age < 18 {log.Fatal("User must be at least 18 years old")}// 保存更新if err := db.Save(&user).Error; err != nil {log.Fatal(err)}fmt.Println("User updated successfully")
}
在现代Go应用中,使用ORM框架(如GORM)是最常见的数据库操作方式,它同时支持这两种更新模式,让开发者可以根据具体场景灵活选择。
使用SQLx的两种更新方式实现
- 先读后写方式
package mainimport ("fmt""log""github.com/jmoiron/sqlx"_ "github.com/go-sql-driver/mysql"
)type User struct {ID int64 `db:"id"`Name string `db:"name"`Email string `db:"email"`Age int `db:"age"`Address string `db:"address"`
}func updateUserReadFirst(db *sqlx.DB, userID int64, updates map[string]interface{}) error {// 先读取当前用户数据user := User{}err := db.Get(&user, "SELECT id, name, email, age, address FROM users WHERE id = ?", userID)if err != nil {return fmt.Errorf("failed to fetch user: %w", err)}// 根据需要更新字段if name, ok := updates["name"].(string); ok {user.Name = name}if email, ok := updates["email"].(string); ok {user.Email = email}if age, ok := updates["age"].(int); ok {user.Age = age}if address, ok := updates["address"].(string); ok {user.Address = address}// 执行业务逻辑验证(示例)if user.Age < 18 {return fmt.Errorf("user must be at least 18 years old")}// 将完整记录写回数据库 - 使用命名参数query := `UPDATE users SET name=:name, email=:email, age=:age, address=:address WHERE id=:id`_, err = db.NamedExec(query, user)return err
}func main() {// 连接数据库db, err := sqlx.Connect("mysql", "user:password@tcp(127.0.0.1:3306)/dbname")if err != nil {log.Fatal(err)}defer db.Close()// 需要更新的字段updates := map[string]interface{}{"name": "New Name","age": 30,}// 更新用户信息err = updateUserReadFirst(db, 1, updates)if err != nil {log.Fatal(err)}fmt.Println("User updated successfully")
}
- 动态SQL方式
package mainimport ("fmt""log""strings""github.com/jmoiron/sqlx"_ "github.com/go-sql-driver/mysql"
)type User struct {ID int64 `db:"id"`Name string `db:"name"`Email string `db:"email"`Age int `db:"age"`Address string `db:"address"`
}func updateUserDynamicSQL(db *sqlx.DB, userID int64, updates map[string]interface{}) error {// 构建动态SQLvar setStatements []stringvar args []interface{}for field, value := range updates {setStatements = append(setStatements, fmt.Sprintf("%s = ?", field))args = append(args, value)}if len(setStatements) == 0 {return fmt.Errorf("no fields to update")}// 构建完整SQL语句query := fmt.Sprintf("UPDATE users SET %s WHERE id = ?", strings.Join(setStatements, ", "))// 添加WHERE条件参数args = append(args, userID)// 执行更新_, err := db.Exec(query, args...)return err
}// 利用SQLx的命名参数特性的替代实现
func updateUserDynamicSQLNamed(db *sqlx.DB, userID int64, updates map[string]interface{}) error {// 构建动态SQL (使用命名参数)var setStatements []stringnamedArgs := map[string]interface{}{}for field, value := range updates {paramName := "param_" + fieldsetStatements = append(setStatements, fmt.Sprintf("%s = :%s", field, paramName))namedArgs[paramName] = value}if len(setStatements) == 0 {return fmt.Errorf("no fields to update")}// 添加ID条件namedArgs["id"] = userID// 构建完整SQL语句query := fmt.Sprintf("UPDATE users SET %s WHERE id = :id", strings.Join(setStatements, ", "))// 使用NamedExec执行更新_, err := db.NamedExec(query, namedArgs)return err
}func main() {// 连接数据库db, err := sqlx.Connect("mysql", "user:password@tcp(127.0.0.1:3306)/dbname")if err != nil {log.Fatal(err)}defer db.Close()// 需要更新的字段updates := map[string]interface{}{"name": "New Name","age": 30,}// 方法1: 使用常规参数err = updateUserDynamicSQL(db, 1, updates)if err != nil {log.Fatal(err)}// 方法2: 使用命名参数err = updateUserDynamicSQLNamed(db, 1, updates)if err != nil {log.Fatal(err)}fmt.Println("User updated successfully")
}
golang SQLx 实现代码(动态组织 SQL)
假设有一个用户表(users),有字段:id, name, email, age。 用户可能只提交了 age 字段,我们要更新这个用户的年龄。
动态构建 SQL 的例子:
func UpdateUser(db *sqlx.DB, userID int, updates map[string]interface{}) error {validFields := []string{"name", "email", "age"}validFieldMap := make(map[string]bool)for _, f := range validFields {validFieldMap[f] = true}setClauses := []string{}args := []interface{}{}for field, value := range updates {if validFieldMap[field] {setClauses = append(setClauses, fmt.Sprintf("%s = ?", field))args = append(args, value)}}if len(setClauses) == 0 {return nil // 没有有效字段需要更新}query := fmt.Sprintf("UPDATE users SET %s WHERE id = ?",strings.Join(setClauses, ", "),)args = append(args, userID)_, err := db.Exec(query, args...)return err
}
- 安全校验阶段(关键防御层)
validFields := []string{"name", "email", "age"}
validFieldMap := make(map[string]bool)
for _, f := range validFields {validFieldMap[f] = true
}
白名单机制:防止SQL注入攻击,确保只更新预定义的字段
业务约束:避免更新非预期的敏感字段(如密码、权限等)
O(1)查询优化:将白名单数组转换为 map 实现 O(1) 时间复杂度校验
- SQL语句组装
// 1. 准备容器:建造SQL的“零件仓库”
setClauses := []string{} // 存放 SQL 的 SET 部分,比如 ["name = ?", "age = ?"]
args := []interface{}{} // 存放参数值的容器,比如 ["张三", 25]// 2. 筛选有效更新字段:工厂质检流程
for field, value := range updates { // 遍历用户提交的更新数据if validFieldMap[field] { // 检查字段是否在白名单内(name/email/age)// 生产SQL片段:相当于准备"name = ?"这样的零件setClauses = append(setClauses, fmt.Sprintf("%s = ?", field))// 收集参数值:把实际值(如"张三")放进容器args = append(args, value)}
}// 3. 安全检查:检查是否有合格零件
if len(setClauses) == 0 { // 如果没有有效字段return nil // 直接退出,相当于“无需更新就不操作”
}// 4. 组装完整SQL:搭建完整的更新语句
// 例子:若更新name和age,则生成 "UPDATE users SET name = ?, age = ? WHERE id = ?"
query := fmt.Sprintf("UPDATE users SET %s WHERE id = ?", // 模板strings.Join(setClauses, ", "), // 用逗号连接零件:name=?, age=?
)// 5. 添加用户ID参数:补上最后一块零件
args = append(args, userID) // 现在args = ["张三", 25, 1001]// 6. 执行SQL:启动数据库操作机器
_, err := db.Exec(query, args...)
- 使用fmt.Sprintf将setClauses用逗号连接起来,形成SET子句,并在后面加上WHERE条件(id=?)。
- 我们将userID添加到args切片的末尾,因为查询中有一个参数(id=?)需要对应。
- 最后,我们执行这个查询(db.Exec),传入查询字符串和参数切片。
参数化分离:SQL 指令与参数值分离存储,避免拼接攻击
参数顺序:SET字段值在前,WHERE条件值在后
实际执行等价于:
db.Exec("UPDATE users SET name = ?, age = ? WHERE id = ?", "张三", 25, 1001
)