当前位置: 首页 > ai >正文

数据库只更新特定字段的两种方式(先读后写 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的两种更新方式实现

  1. 先读后写方式
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")
}
  1. 动态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
}
  1. 安全校验阶段(关键防御层)
validFields := []string{"name", "email", "age"}
validFieldMap := make(map[string]bool)
for _, f := range validFields {validFieldMap[f] = true
}

白名单机制:防止SQL注入攻击,确保只更新预定义的字段
业务约束:避免更新非预期的敏感字段(如密码、权限等)
O(1)查询优化:将白名单数组转换为 map 实现 O(1) 时间复杂度校验

  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...)
  1. 使用fmt.Sprintf将setClauses用逗号连接起来,形成SET子句,并在后面加上WHERE条件(id=?)。
  2. 我们将userID添加到args切片的末尾,因为查询中有一个参数(id=?)需要对应。
  3. 最后,我们执行这个查询(db.Exec),传入查询字符串和参数切片。

参数化分离:SQL 指令与参数值分离存储,避免拼接攻击
参数顺序:SET字段值在前,WHERE条件值在后
实际执行等价于:

db.Exec("UPDATE users SET name = ?, age = ? WHERE id = ?", "张三", 25, 1001
)
http://www.xdnf.cn/news/10239.html

相关文章:

  • 索引的选择与Change Buffer
  • Linux进程信号
  • 车载诊断架构SOVD --- 车辆发现与建连
  • 项目:贪吃蛇实现
  • AI与智能驾驶的关系和原理:技术融合与未来展望-优雅草卓伊凡一、AI大模型基础原理与智能驾驶
  • 【Linux系列】Linux/Unix 系统中的 CPU 使用率
  • C++23 已移除特性解析
  • 电子电路:怎么理解时钟脉冲上升沿这句话?
  • ASP.NET Core SignalR的基本使用
  • 《深入解析SPI协议及其FPGA高效实现》-- 第一篇:SPI协议基础与工作机制
  • Python编程基础(一) | 变量和简单数据类型
  • git下载和安装(完整版)
  • 什么是特性阻抗
  • ArcPy错误处理与调试技巧(2)
  • Mybatis ORM SpringORM
  • 计算机视觉---GT(ground truth)
  • C# 成员函数中如何拿到当前所在类的名字?
  • 使用 HTML + JavaScript 实现一个日历任务管理系统
  • HackMyVM-First
  • vue3 基本语法 父子关系
  • odoo18 新特性
  • Webug4.0靶场通关笔记16- 第16关MySQL配置文件下载
  • 【前端】SPA v.s. MPA
  • C58-字符串拼接函数strcat
  • Unity3D仿星露谷物语开发56之保存角色位置到文件
  • 姜老师MBTI课程:4条轴线的总结
  • FactoryBean 接口
  • SOC-ESP32S3部分:21-非易失性存储库
  • Visual Stuido笔记:C++二进制兼容性之间的兼容性
  • C#中实现两个对象部分相同属性值的复制