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

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

相关文章:

  • 【设计模式-4.6】行为型——状态模式
  • 电路学习(二)之电容
  • Win10 doccano pip安装笔记
  • 【深度学习】16. Deep Generative Models:生成对抗网络(GAN)
  • STM32CubeMX串口配置
  • LeetCode[257]二叉树的所有路径
  • 【图像处理入门】3. 几何变换基础:从平移旋转到插值魔法
  • 基于开源AI大模型AI智能名片S2B2C商城小程序源码的销售环节数字化实现路径研究
  • 接口性能优化
  • MySql(九)
  • 达梦的TEMP_SPACE_LIMIT参数
  • Vue-过滤器
  • 【PhysUnits】15.6 引入P1后的左移运算(shl.rs)
  • java8集合操作全集
  • linux 1.0.7
  • 《深入解析SPI协议及其FPGA高效实现》-- 第一篇:SPI协议基础与工作机制
  • MySQL索引与性能优化入门:让查询提速的秘密武器【MySQL系列】
  • AI 的早期萌芽?用 Swift 演绎约翰·康威的「生命游戏」
  • [蓝桥杯]机器人塔
  • Day 41
  • iis上做的网站外网怎么访问/营销推广内容
  • 上海网站建设沪icp备/商城推广软文范文
  • 来源门户网站源码/磁力狗在线引擎
  • vue做网站的好处是什么/永久免费自助建站系统
  • 怎样突破网站设计瓶颈/十大搜索引擎网站
  • 常德网站建设设计/黄金网站软件app大全下载