文章目录
数据库只更新特定字段的两种方式(先读后写 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 main
import (
"fmt"
"log"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type User struct {
ID uint
Name string
Email string
Age int
Address 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自动生成仅更新指定字段的SQL
result := 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 User
if 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 main
import (
"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 main
import (
"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 {
// 构建动态SQL
var setStatements []string
var 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 []string
namedArgs := map[string]interface{}{}
for field, value := range updates {
paramName := "param_" + field
setStatements = 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
)