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

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

相关文章:

数据库只更新特定字段的两种方式(先读后写 vs. 动态组织 SQL)-golang SQLx 实现代码(动态组织 SQL)

文章目录 数据库只更新特定字段的两种方式&#xff08;先读后写 vs. 动态组织 SQL&#xff09;go语言例子使用GORM的示例&#xff08;最常用的Go ORM库&#xff09;使用SQLx的两种更新方式实现golang SQLx 实现代码&#xff08;动态组织 SQL&#xff09; 数据库只更新特定字段的…...

从翻译后修饰角度解析人工合成途径与底盘细胞的适配性-文献精读136

Compatibility between synthetic pathway and chassis cells from the viewpoint of post-translational modifications 从翻译后修饰角度解析人工合成途径与底盘细胞的适配性 摘要 揭示工程化设计的人工合成途径与底盘细胞整体代谢网络的交互作用及适配性机制是合成生物学研…...

Cesium快速入门到精通系列教程一

一、打造第一个Cesium应用 1、官方渠道下载Cesium&#xff08;可选择历史版本&#xff09; ​​GitHub Releases页面​​ 访问 Cesium GitHub Releases&#xff0c;此处列出了所有正式发布的版本。 通过标签&#xff08;如 v1.95.0&#xff09;选择目标版本&#xff0c;下载…...

[Windows] 剪映 视频编辑处理

附链接&#xff1a;夸克网盘分享&#xff08;点击蓝色字体自行保存下载&#xff09;...

决策树 GBDT XGBoost LightGBM

一、决策树 1. 决策树有一个很强的假设&#xff1a; 信息是可分的&#xff0c;否则无法进行特征分支 2. 决策树的种类&#xff1a; 2. ID3决策树&#xff1a; ID3决策树的数划分标准是信息增益&#xff1a; 信息增益衡量的是通过某个特征进行数据划分前后熵的变化量。但是&…...

stm32 / arduino TPL0401A使用教程

这是在给英国的一个学生讲课时用到的一个芯片&#xff0c;做一个dcdc的反馈电路&#xff0c;刚开始用的不是这个&#xff0c;后来发现国内这个芯片用的挺成熟&#xff0c;就选择了这个。 芯片说明 首先我买的是TPL0401A,我发现淘宝上卖的都是A&#xff0c;其实想用C&#xff0…...

数据结构与算法之单链表面试题(新浪、百度、腾讯)

单链表面试题&#xff08;新浪、百度、腾讯&#xff09; 求单链表中的有效节点的个数 public int getCount(HeroNode head) {Hero1 cur head.getNext();int count 0;while(cur ! null) {count;cur cur.getNext();}return count;}查找单链表中的倒数第k个结点【新浪面试题】…...

单板机8088C语言计划

计划将原来用汇编写的小程序&#xff0c;用C语言重新写一遍 计划2个月能完成 然后再试试&#xff0c;能不能用C写一下固件BootLoad 和一个类似Dos时代的Debug调试器...

一周学会Pandas2之Python数据处理与分析-数据重塑与透视-pivot() - 透视 (长 -> 宽,有限制)

锋哥原创的Pandas2 Python数据处理与分析 视频教程&#xff1a; 2025版 Pandas2 Python数据处理与分析 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili pivot() 是 pandas 中用于数据重塑的核心方法&#xff0c;它将长格式数据转换为宽格式数据&#xff0c;与 melt() 方…...

机器学习中无监督学习方法的聚类:划分式聚类、层次聚类、密度聚类

1.定义和特点 2.划分式聚类&#xff1a;K-Means 、 K-Medoids 3.层次聚类&#xff1a;树状图 4.密度聚类&#xff1a;DBSCAN 5.聚类的应用 一、定义和特点 机器学习中的无监督学习聚类是一种通过数据内在结构将样本分组的技术&#xff0c;无需预先标注的类别标签。 它的核心目…...

【HW系列】—溯源与定位—Linux入侵排查

文章目录 一、Linux入侵排查1.账户安全2.特权用户排查&#xff08;UID0&#xff09;3.查看历史命令4.异常端口与进程端口排查进程排查 二、溯源分析1. 威胁情报&#xff08;Threat Intelligence&#xff09;2. IP定位&#xff08;IP Geolocation&#xff09;3. 端口扫描&#x…...

CPO-BP+MOPSO,冠豪猪优化BP神经网络+多目标粒子群算法!(Matlab源码)

目录 效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.CPO-BPNSGA&#xff0c;冠豪猪优化BP神经网络粒子群算法&#xff01;&#xff08;Matlab完整源码和数据&#xff09;&#xff0c;冠豪猪算法优化BP神经网络的权值和阈值&#xff0c;运行环境Matlab2020b及以上。 多…...

模块化设计,static和extern(面试题常见)

文章目录 一、函数的声明和定义1.1 单个文件1.2 多个文件1.3 static和extern1.3.1 static修饰局部变量1.3.2 static修饰全局变量1.3.3 static修饰函数 总结 一、函数的声明和定义 1.1 单个文件 一般我们在使用函数的时候&#xff0c;直接将函数写出来就使用了 题目:写一个函数…...

【快速解决】数据库快速导出成sql文件

1、cmd直接打开 输入命令 mysqldump -u用户名 -p密码 数据库名 > 导出文件名.sql修改成自己mysql的用户名和密码&#xff0c;和要导出的数据库名称&#xff0c;给导出的文件起一个名字。 如图所示 这样就成功了。...

使用 Syncfusion 在 .NET 8 中生成 PDF/DOC/XLS/PPT

Syncfusion 是一个功能强大的控件库&#xff0c;提供了多种工具来生成和处理 PDF、Word、Excel 和 PowerPoint 文档。在 .NET 8 中&#xff0c;使用 Syncfusion 可以简化生成这些文档的流程&#xff0c;并确保生成的文件高效、准确。本文将介绍如何在 .NET 8 中使用 Syncfusion…...

LearnOpenGL-笔记-其十二

今天我们来将LearnOpenGL的高级光照部分彻底完结&#xff1a; Bloom 泛光是一个非常常见的用于改善图像质量的手段&#xff0c;其主要做法就是将某个高亮度区域的亮度向四周发善以实现该区域更亮的视觉效果&#xff08;因为显示器的亮度范围有限&#xff0c;需要通过泛光来体…...

【C++】C++面向对象设计的核心思想之一: 接口抽象、解耦和可扩展性

1. 什么是虚函数&#xff1f; 虚函数&#xff08;virtual&#xff09;是C里实现“多态”的关键机制。 在基类中声明虚函数&#xff0c;在子类中可以**覆盖&#xff08;override&#xff09;**它们。通过基类指针/引用操作时&#xff0c;自动调用实际对象&#xff08;子类&…...

Namespace 命名空间的使用

名字空间&#xff1a;划分更多的逻辑空间&#xff0c;有效避免名字冲突的问题 1.什么是命名空间 名字命名空间 namespace 名字空间名 {...} // 名字空间 n1 域 namespace n1 {// 全局变量int g_money 0;void save(int money){g_money money;}void pay(int money){g_money - m…...

mac 下安装Rust Toolchain(Nightly)

你可以用 Homebrew 安装 rustup&#xff0c;这是推荐的管理 Rust toolchain的 brew install rustup-init安装 Rust&#xff08;包含 rustup&#xff09; rustup-init安装过程中会让你选择安装那个&#xff0c;直接回车选择默认的即可 安装完成后&#xff0c;cargo, rustc, r…...

PHP中文网文章内容提取免费API接口教程

接口简介&#xff1a; 提取PHP中文网指定文章内容。本接口仅做内容提取&#xff0c;未经作者授权请勿转载。 请求地址&#xff1a; https://cn.apihz.cn/api/caiji/phpzww.php 请求方式&#xff1a; POST或GET。 请求参数&#xff1a; 【名称】【参数】【必填】【说明】 【用…...

【Java笔记】Spring IoC DI

目录 Spring IoC & DI1. IoC1.1 Bean的存储1.1.1 类注解1.1.2 方法注解 Bean1.1.3 重命名1.1.4 Spring扫描路径 2. DI Spring IoC & DI Spring两个核心思想&#xff1a;IoC & AOP Spring相当于一个容器&#xff0c;IoC就是把对象存放在Spring容器中&#xff0c;让…...

学习STC51单片机22(芯片为STC89C52RCRC)

记住这个AT指令千万不要去脑子记&#xff0c;要用手册查 每日一言 努力不是为了感动谁&#xff0c;而是为了不辜负自己的野心。 硬件&#xff1a;ESP8266 wife模块 蓝牙&#xff0c;ESP-01s&#xff0c;Zigbee&#xff0c;NB-lot等通信模块都是基于AT指令的设计 老样子 我们用…...

ubuntu20.04.5--arm64版上使用node集成java

ubuntu20.04.5arm上使用node集成java #ssh&#xff0c;可选 sudo apt update sudo apt install openssh-server sudo systemctl status ssh sudo systemctl enable ssh sudo systemctl enable --now ssh #防火墙相关&#xff0c;可选 sudo ufw allow ssh sudo ufw allow 22…...

Linux --UDP套接字实现简单的网络聊天室

一、Server端的实现 1.1、服务端的初始化 ①、创建套接字&#xff1a; 创建套接字接口&#xff1a; #include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int socket(int domain, int type, int protocol); //1. 这是一个创建套接字的接…...

嵌入式学习笔记 - keil安装目录下的头文件自动包含问题

Keil MDK/MDK-ARM&#xff08;ARM编译器&#xff09;默认情况下会自动包含其安装目录下的标准头文件路径&#xff08;如CMSIS库、设备头文件等&#xff09;。具体机制如下&#xff1a; ‌默认自动包含‌&#xff1a; 新建工程或使用设备数据库选择芯片型号后&#xff0c;Keil会…...

word批量导出visio图

具体步骤 修改word格式打开VBA窗口插入代码运行代码 修改word格式 将word文档修改为docm格式 打开VBA窗口 打开开发工具VisualBasic项&#xff0c;如果没有右键在自定义功能区添加 插入代码 插入 -> 模块&#xff0c;代码如下&#xff1a; Sub ExportAllVisioDiagrams()D…...

把数据库做得能扩展:Aurora DSQL 的故事

把数据库做得能扩展&#xff1a;Aurora DSQL 的故事 我们在 AWS re:Invent 上发布了 Aurora DSQL&#xff0c;这是一个全新方式构建关系型数据库的尝试。它不是单纯的技术升级&#xff0c;而是一段从零开始、反复试错、不断学习的工程旅程。 我们为什么做 Aurora DSQL&#x…...

全面解析:npm 命令、package.json 结构与 Vite 详解

全面解析&#xff1a;npm 命令、package.json 结构与 Vite 详解 一、npm run dev 和 npm run build 命令解析 1. npm run dev 作用&#xff1a;启动开发服务器&#xff0c;用于本地开发原理&#xff1a; 启动 Vite 开发服务器提供实时热更新&#xff08;HMR&#xff09;功能…...

【本地部署】 Deepseek+Dify创建工作流

文章目录 DeepseekDify 简介流程1、下载Docker2、Dify下载3、使用浏览器打开 Deepseek Deepseek 是一款功能强大的 AI 语言模型工具&#xff0c;具备出色的理解与生成能力。它可以处理各种自然语言任务&#xff0c;无论是文本创作、问答&#xff0c;还是数据分析与解释&#x…...

Rust 配置解析`serde` + `toml`

&#x1f980; Rust 配置解析&#xff1a;彻底搞懂 TOML、Option、Vec、derive 背后的原理 &#x1f4cc; 目录 什么是 TOML 文件&#xff1f;为什么要用 serde toml crate&#xff1f;结构体上 #[derive(...)] 是什么&#xff1f;配置中数组 [] 和表数组 [[...]] 怎么用&…...