ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SqlSugar Update更新语法全解析:实体更新、批量更新与避坑实践

SqlSugar Update更新语法全解析:实体更新、批量更新与避坑实践 老读者应该知道我写 SqlSugar 系列已经有一阵子了从基本的查询语法到联表操作都聊过。这阵子在项目里频繁碰更新数据的各种场景愈发觉得Update这块的写法相当有嚼头。很多人用 SqlSugar 做插入和查询很顺手一到更新就开始复制粘贴旧代码要么字段更新不受控要么条件写不对导致全表被改。所以这篇专门把 Update 的语法整理成一个合集把我实际踩过的坑和验证过的写法一并交代清楚。这篇文章适合刚接触 SqlSugar 的 .NET 开发者也适合已经在用但想系统梳理更新语法的朋友。我会从最基础的实体更新讲起一路深入到批量更新、条件更新、表达式更新、子查询更新这些进阶玩法最后附上我在生产环境里遇到的经典问题和排查思路。保证你看完能直接照着写不用再翻文档。1. 更新操作比你想的复杂SqlSugar 的 Update 全景很多人觉得更新不就是Updateable(entity).ExecuteCommand()嘛有啥好讲的。但实际做业务系统时间长了就会明白更新是所有 CRUD 里最容易出问题的环节。查询出了问题最多是性能慢或者数据不对更新出了问题那可就是线上事故了——字段被覆盖、条件失效导致全表数据被改、并发下库存超卖哪一个都够喝一壶的。1.1 为什么 Update 是 ORM 里最难做好的部分先说一个很反直觉的点ORM 框架对查询的支持往往非常完善各种子查询、聚合、导航属性怎么写都行。但到了更新框架反而会收着做。为啥因为查询是无副作用的而更新是有副作用的框架不敢给你太多“自由发挥”的空间否则出错成本太高。拿原生 SQL 举例一条UPDATE语句可以拆成三个关键部分要更新哪张表、要改哪些字段、要过滤哪些行。字段部分和过滤部分稍微配合不好就会出现“我只想改一条记录结果把整张表都改了”的惨剧。SqlSugar 的 Update 系列设计本质上就是围绕这三个核心要素做正交组合UpdateableT()指定表和实体UpdateColumns()指定字段Where()指定行条件。理解了这条主线后面所有花哨的写法都能拆解成这三个要素的排列组合。1.2 SqlSugar Update 能力图谱一句话记住这些方法我把常用的更新方法做了一个汇总表方便后面展开时对照着看。方法作用典型场景Updateable(entity)按实体主键更新所有非空字段常规单条更新Updateable(entity).UpdateColumns(...)只更新指定字段防误改、防覆盖Updateable(entity).Where(...)按自定义条件更新不依赖主键的更新UpdateableT().SetColumns(...)用表达式构造更新内容动态赋值、列计算Updateable(list).ExecuteCommand()批量更新多条数据一次提交UpdateColumns(...).Where(...)指定字段条件组合精准控制更新范围另外还有异步版本ExecuteCommandAsync()以及配合事务的用法。我个人习惯的划分是单条简单更新用实体方式批量更新用 List 方式复杂赋值用SetColumns表达式。接下来逐个拆开讲。2. 基础更新操作一行代码背后的数据安全逻辑先讲最基础也是最常用的实体更新。很多教程会告诉你“就是这么简单”但没人告诉你这里面藏着多少坑。2.1 实体更新主键驱动的默认行为最基本的写法是直接把实体对象丢给UpdateableT()var user new User { Id 1, Name 张三, Age 25, Email null }; var result db.Updateable(user).ExecuteCommand();这段代码执行后会生成什么样的 SQL很多人以为是SET NameName, AgeAge, EmailEmail但实际上 SqlSugar 的默认行为是只更新非空字段。也就是说Email为 null 时这条 SQL 根本不会包含Email字段只有Name和Age会被更新。这个默认行为的底层逻辑其实很务实实体对象往往是从前端传来的用户未必填了所有字段如果 null 也强行更新就把数据库里已有的数据清掉了。但这也意味着如果你确实想把某个字段置为 null用这种写法是做不到的需要配合UpdateColumns显式指定这个坑我后面细讲。还有一个关键限制默认按主键匹配。如果你的实体没有配置主键用[SugarColumn(IsPrimaryKey true)]或者数据库表本身没主键Updateable(entity)会直接报错。SqlSugar 是刻意这么设计的——没有主键定位宁可让你写不出来也不让你误更新全表。注意实体更新时主键字段的值不能为默认值0 或 空字符串。我曾经接过一个需求前端传过来的对象主键字段没赋值结果执行更新时直接抛Primary key is null的异常。排查了半天发现是前端漏传了 Id。建议在更新前对主键做一次显式校验。2.2 指定字段更新UpdateColumns 的精准控制接上面的话题如果我只想更新某几个字段甚至想把某字段更新为 null应该怎么写用UpdateColumns显式指定列var user new User { Id 1, Name 张三, Email null }; var result db.Updateable(user) .UpdateColumns(it new { it.Name, it.Email }) .ExecuteCommand();这样生成 SQL 就会包含SET NameName, EmailEmail其中Email会被显式更新为 null。需要注意的是UpdateColumns的表达式里只要列出了这个字段不管实体里这个属性的值是 null 还是默认值都会无条件更新。这个特性既是优点也是风险。优点是可以让一个属性为 null 的实体也能精确更新指定列缺点是如果你在UpdateColumns里列了一个没有赋值的属性数据库里原有的数据就被覆盖成默认值了。比如你更新一个Age属性忘了赋值默认是 0结果UpdateColumns(it new { it.Age })跑完所有人的年龄都变成 0 了。所以我个人的建议是使用指定字段更新前先把要更新的数据完整查出来再修改目标字段后提交不要直接拿一个半空的实体去做指定列更新。2.3 条件更新不依赖主键的更新姿势实体更新无论如何都要走主键这在某些业务场景下不够灵活。比如“把用户状态改为已禁用”这个操作前端可能只知道用户名称不想查一次再更新一次。这时候用Where指定条件就方便了var result db.UpdateableUser() .UpdateColumns(user new User { Status 0, UpdatedAt DateTime.Now }) .Where(user user.Name 张三) .ExecuteCommand();注意这里UpdateableUser()后面不跟实体对象而是直接在UpdateColumns里用new User{}的方式指定要更新的列和值。这种写法的好处是字段和值在一处条件和值完全分离逻辑非常清晰。有一点值得强调Where可以多次调用多次调用之间是AND关系。如果完全不写Where那就变成更新全表了。SqlSugar 在这里倒是挺有原则允许你更新全表它认为那是你的自由但会通过最终生成的 SQL 让你意识到自己在干什么。我建议在比较重要的生产表上进行无 WHERE 更新前先注释掉条件跑一下看看生成的 SQL 再决定要不要执行。3. 批量更新与表达式更新应对真实业务的高阶玩法基础更新只是热身。真实业务里你经常会遇到“一次要更新一千条数据”“要根据某列的现有值做计算后再更新”“更新时要把某个子查询的结果带进来”这类需求。这些就需要批量跟表达式的配合了。3.1 批量更新 List别被性能忽悠了批量更新最直觉的写法就是把实体集合丢进去var users new ListUser { new User { Id 1, Age 26 }, new User { Id 2, Age 27 }, new User { Id 3, Age 28 } }; var result db.Updateable(users).ExecuteCommand();SqlSugar 默认会怎么执行它会为每个实体生成一条单独的 UPDATE 语句然后在一个连接上顺序执行。少量数据没问题但如果你提交几百上千条这种逐条更新的效率就很感人。SqlSugar 提供了Updateable(list).ExecuteCommand()的重载以及一个偏门但实际很有用的属性db.Updateable(users).UpdateColumns(...)依然生效但底层仍然是逐条 update最多是在同一个连接里复用准备好的命令并没有从本质上变成真正的批量更新 SQL 语句。真正的高性能批量更新SqlSugar 支持 SQL Server 的Update With Join方式或者在 MySQL 上借助临时表来间接实现。但如果你的业务是纯单表更新我的经验是如果每次更新的字段值不同再优化的空间也不大建议直接在业务层把数据分批提交每批控制在 500 条以内。这个数量下即便逐条 update 也不会让数据库压力太大而且出错回滚的范围也可控。如果所有数据要更新的字段值完全一样只是 ID 列表不同这种场景用 SqlSugar 就非常舒服var ids new Listint { 1, 2, 3, 4, 5 }; var result db.UpdateableUser() .UpdateColumns(user new User { Status 10, UpdatedAt DateTime.Now }) .Where(user ids.Contains(user.Id)) .ExecuteCommand();这条 SQL 最终会被翻译成UPDATE User SET StatusStatus, UpdatedAtUpdatedAt WHERE Id IN (1,2,3,4,5)是一条真正的单条 SQL性能和原子性都要比逐条更新靠谱得多。所以我的结论是批量更新时先把数据分类——字段值相同也好不同也好——然后分别选最优写法。3.2 SetColumns 表达式让更新拥有计算能力有些更新不是单纯地把一个值写进去而是要根据字段本身的现有值做计算。最典型的例子就是库存扣减和点击量累加。如果是常规写法你得先把数据查出来在内存里算好再更新回去两步之间还有并发风险。SqlSugar 的SetColumns表达式可以直接把计算逻辑写进更新语句里var result db.UpdateableArticle() .SetColumns(article article.ViewCount article.ViewCount 1) .Where(article article.Id 100) .ExecuteCommand();注意这里运算符用的是这是 SqlSugar 的表达式语法最终会被翻译成 SQL 里的赋值符号 SET ViewCount ViewCount 1。我第一次看到这种写法也愣了一下但实际用起来是真的方便尤其是做统计类字段的原子累加。这个方法最大的价值是规避了并发问题。如果你先查询、再计算、再更新中间必然有间隙两个请求同时操作就可能丢失更新。而把计算放进 SQL 里更新操作是原子的数据库的行锁保证了这个1不会被其他事务覆盖。库存场景特别依赖这种写法var result db.UpdateableProduct() .SetColumns(product product.Stock product.Stock - 1) .Where(product product.Id 1001 product.Stock 1) .ExecuteCommand();这条语句翻译成 SQL 就是UPDATE Product SET Stock Stock - 1 WHERE Id 1001 AND Stock 1。执行后返回的结果数是多少如果库存不够条件不满足影响行数是 0你在代码里判断result 0就说明库存不足或者商品不存在。这个判断直接替代了“先查再改”的整个流程我在电商系统里是这么处理库存超卖问题的实测下来确实稳。SetColumns还有一个很实用的变体可以一次设置多个字段而且支持从 C# 变量取值var newStatus 2; var operatorName admin; var result db.UpdateableOrder() .SetColumns(order new Order { Status newStatus, ReviewedBy operatorName, ReviewTime DateTime.Now }) .Where(order order.Id 500) .ExecuteCommand();它会翻译成SET Status newStatus, ReviewedBy operatorName, ReviewTime ReviewTime写法上比单字段多次SetColumns简洁很多。这种写法的可读性也强——一眼就能看出来要改哪些字段、改成什么值。3.3 在更新中使用数据库函数和子查询有时候更新的值来自数据库函数比如给日期字段加一个时间或者把某个字符串拼上当前用户名。SqlSugar 支持直接在赋值表达式中调用数据库函数var result db.UpdateableUser() .SetColumns(user user.UpdateTime DateTime.Now) .Where(user user.Id 10) .ExecuteCommand();这里的DateTime.Now会被翻译成 SQL 里的GETDATE()或CURRENT_TIMESTAMP之类的数据库时间函数而不是在 C# 里取一个固定的时间戳。这个区别在分布式、多服务器场景下很重要——如果写成var now DateTime.Now; user.UpdateTime now;那所有请求都是从各自服务器取时间可能会不一致。而直接交给数据库函数取时时间全部由数据库服务端生成一致性更好。子查询更新更高级一点可以用SqlFunc.Subqueryable()实现“用另一张表的数据来更新当前表”的效果var result db.UpdateableOrder() .SetColumns(order new Order { CustomerName SqlFunc.SubqueryableCustomer() .Where(c c.Id order.CustomerId) .Select(c c.Name) }) .Where(order order.Status 1) .ExecuteCommand();这段代码最终生成的 SQL 就是类似UPDATE Order SET CustomerName (SELECT Name FROM Customer WHERE Id Order.CustomerId) WHERE Status 1的样子。SqlSugar 在这里帮你处理了子查询的表达和参数化不需要你去拼 SQL 字符串。不过有一点要注意子查询更新对数据库版本有要求比如 MySQL 更新时子查询不能直接引用被更新的目标表有些版本会报You cant specify target table for update in FROM clauseSqlSugar 虽然帮你写生成逻辑但底层数据库的限制它也不会替你规避。遇到这种情况我的做法是把子查询结果先查出来映射成字典再用SetColumns结合字典做批量更新绕开这个限制。4. 事务、异步和性能生产环境里的 Update 实战心得语法能跑通只是第一步放到生产环境里事务、异步、锁这些都是绕不开的问题。这一节我把自己在真实项目里的处理经验整理出来。4.1 更新操作要不要手动开事务单条 UPDATE 在数据库层面是原子的不需要外部事务。但真实业务往往不止一条更新比如“下单”这个操作要扣库存、更新订单状态、写操作日志涉及多张表多条语句任意一条失败都需要回滚。SqlSugar 的事务用法和 ADO.NET 一脉相承try { db.BeginTran(); var updateResult db.UpdateableProduct() .SetColumns(product product.Stock product.Stock - 1) .Where(product product.Id 1001 product.Stock 1) .ExecuteCommand(); if (updateResult 0) { throw new Exception(库存不足); } var orderResult db.UpdateableOrder() .UpdateColumns(order new Order { Status 5, PayTime DateTime.Now }) .Where(order order.Id 500) .ExecuteCommand(); db.CommitTran(); } catch (Exception ex) { db.RollbackTran(); // 记录日志 }BeginTran之后的所有ExecuteCommand都在同一个事务里任何一个环节抛异常都能整体回滚。这里有一个 SqlSugar 的使用细节事务必须在同一个 SqlSugarClient 实例上调用如果你用using (var db new SqlSugarClient(...))包裹整个逻辑期间不要Dispose这个实例。如果每次操作都新建 db 实例事务会断掉。关于事务的隔离级别默认情况下 SqlSugar 的事务遵循数据库的默认隔离级别通常是 READ COMMITTED。在上面的库存扣减场景里关键不是把隔离级别调高而是利用SET Stock Stock - 1 WHERE Stock 1这种原子条件更新。事务保证的是多条语句要么全部成功要么全部失败原子更新保证的是单条语句内部不会出现并发覆盖。两个层面配合起来才能把并发下的超卖问题彻底解决。4.2 ExecuteCommandAsync 的正确用法SqlSugar 的异步更新方法ExecuteCommandAsync()用起来很简单var result await db.UpdateableUser() .UpdateColumns(user new User { Status 0 }) .Where(user user.LastLoginTime DateTime.Now.AddMonths(-6)) .ExecuteCommandAsync();这种写法本质上是把同步的ExecuteCommand挂到异步上下文里执行。在这个场景下你不需要担心线程安全问题——SqlSugar 已经帮你处理好了连接和上下文。但有一点我实际踩过坑不要在多个异步方法里共享同一个 SqlSugarClient 实例做并行更新。SqlSugar 的实例不是完全线程安全的多个任务并发使用同一个实例进行Updateable操作偶尔会抛连接已被占用或者上下文错乱的异常。正确的做法是每个异步任务各自 new 一个实例或者使用依赖注入时给 DbContext 设置合适的 Scope 生命周期。我检查过 SqlSugar 的 GitHub Issues确实有人反馈过类似的并发问题。团队里的规矩是异步场景尽量用Async后缀的方法并且一个业务流程内共用一个实例如果是 Fan-out 式的并行任务每个子任务单独建实例。4.3 更新对性能影响最大的几个因素更新性能问题往往不是单条语句慢而是语句数量太多。最常见的就是循环里逐条更新// 强烈不推荐循环里逐条更新 foreach (var id in ids) { await db.UpdateableUser() .UpdateColumns(user new User { Status 10 }) .Where(user user.Id id) .ExecuteCommandAsync(); }这段代码如果有 1000 个 id就会执行 1000 条 UPDATE网络往返 1000 次。改成Where(ids.Contains(...))的写法一条 SQL 就能搞定。我用一个 5 万条数据的测试表对比过循环逐条更新耗时约 20 秒而 IN 条件批量更新不到 1 秒差距显著。此外更新语句里的索引使用也是容易被忽略的点。WHERE后面的条件如果没走索引数据库会做全表扫描更新时行锁的范围也会扩大。比如你按Status字段更新但Status没有索引数据库为了找到符合条件的行可能会锁住多条记录导致并发度下降。我的习惯是给更新语句常用的WHERE条件字段建立合适的索引这个策略对写多读少的表特别有效。还有一个细节是更新字段的冗余程度。把UPDATE语句中不需要修改的字段也塞进去会增加数据库的日志量和缓冲池压力。SqlSugar 默认的实体更新只覆盖非空字段已经算比较克制但在用UpdateColumns时还是要有意识地只列需要变化的字段特别是字段很多的宽表少更新几个大字段如Text、Description对性能的改善非常明显。5. 高频问题排查与避坑实录这一节算是整个系列的精华所在。我把自己和身边同事在项目里真正遇到过的更新相关问题做成了清单每个问题都会给出排查思路不是那种“请检查配置”的废话。5.1 Update 语句没有生效可能是这些原因第一个高频原因主键没传或者传了 0。实体更新默认按主键匹配如果你更新一个Id 0的实体执行后影响行数是 0但不会报错。因为 SqlSugar 确实生成了UPDATE ... WHERE Id 0这样的 SQL数据库里没有 Id 为 0 的记录自然一条都没更新。出现这种情况先打印生成的 SQL 看看WHERE条件到底是什么基本能立刻定位。第二个原因更新了UpdateColumns列表之外的字段但值相同。比如你只想改Name但代码里写成了UpdateColumns(it new { it.Name, it.StatusColor })而StatusColor在内存里跟数据库里是同一个值。数据库确实执行了更新但因为值没变化影响行数可能还是返回了 1有的数据库驱动会返回 1 表示匹配到了行有的返回 0。你拿result 0判断更新失败就会误判。这时候要看你用的是 MySQL 还是 SQL ServerMySQL 默认返回的是实际发生变化的行数SQL Server 默认返回的是匹配到的行数。如果业务对“是否真的有变化”敏感可以用UPDATE后通过ROWCOUNT这类机制去判断而不是依赖 ORM 的返回值。第三个原因Where条件写在了错误的表达式里。我在代码评审里见过好几次这种写法// 错误示例把 Where 条件放进了 UpdateColumns var result db.UpdateableUser() .UpdateColumns(user new User { Status user.Status 1 ? 10 : 20 }) .Where(user user.Id 100) .ExecuteCommand();这个写法的问题在于UpdateColumns表达式里写了条件最终生成的 SQL 会把条件当成更新值变成SET Status CASE WHEN Status 1 THEN 10 ELSE 20 END。虽然 MySQL 和 SQL Server 都支持这种写法但它跟“只更新符合条件的行”是两个不同的逻辑。SqlSugar 编译表达式时会按赋值表达式处理在SetColumns里是赋值符号在UpdateColumns里这种写法有时会让人混淆。我的建议是把条件一律放在Where把赋值逻辑一律放在SetColumns不要混着用。5.2 更新时遇到的并发冲突和锁问题并发导致数据被覆盖是我在生产环境处理过的最严重的更新问题。典型场景是编辑资料两个管理员同时打开同一条客户记录A 改了姓名B 改了手机号先后提交后提交的会把先提交的整个记录覆盖掉。这个问题的根源是实体更新默认更新所有非空字段B 提交时他内存里那条记录的姓名字段还是旧值于是把 A 改好的姓名覆盖回去了。解决思路有三层。第一层是字段级别控制只更新实际变化的字段这样 A 改姓名不影响手机号B 改手机号不影响姓名冲突范围缩小很多。第二层是乐观锁在表里加一个Version字段更新时SET Version Version 1 WHERE Version oldVersion影响行数为 0 就说明有冲突提示用户刷新重试。第三层是执行顺序上对关键资源加锁比如库存扣减用条件原子更新不依赖乐观锁也能保证不超卖。三层方案可以叠加使用具体要看业务的并发量和容忍度。更新时长时间锁等待的问题也遇到过。有一次线上批量更新任务跑起来后前台的查询全部变慢。排查后确认是批量更新的事务持有了大量行锁又没及时提交后来前台查询被阻塞。这个问题的根源在于批量更新在一个事务里执行了很长时间锁范围不断扩大。我的建议是大批量更新要控制事务粒度一个批次完成就提交如果更新涉及的行数特别多改成分批执行每批 500 条左右如果条件允许尽量安排在业务低峰期执行这类任务。另外SqlSugar 的Updateable默认情况下每条语句会自动提交不会长时间持有事务但如果手动BeginTran就要特别小心写完更新后立刻CommitTran别在事务里夹杂慢查询。5.3 一个最容易忽视的参数化问题SqlSugar 的参数化处理总体做得很到位生成的 SQL 一般都用参数而不是拼接字符串。但有一种情况值得注意在SetColumns里直接拼字符串拼接表达式的时候比如.SetColumns(user user.Remark user.Remark -verified)SqlSugar 会尝试把它编译成SET Remark Remark -verified。这个过程本身没问题但如果你把外部变量直接拼进字符串表达式里一定要确认 SqlSugar 有没有正确参数化。我见过有人写过user.Name user.Name inputName结果因为inputName是外部变量SqlSugar 有时候会根据上下文把它当成常量直接拼进 SQL。一旦拼进去就可能引入 SQL 注入风险。虽然 SqlSugar 在多数情况下会自动参数化但保险起见可以开启 SqlSugar 的 SQL 日志把生成的 SQL 打印出来检查一遍确认外部值有没有走参数。提示SqlSugar 提供了一个简单的 SQL 日志监听方式在创建实例时配置Aop.OnLogExecuting把生成的 SQL 和参数输出到控制台或日志文件。排查看生成的 SQL 是否合理这是我最推荐的调试手段。另外还有一个小坑当SetColumns表达式中同时用了 DateTime.Now 和外部变量时生成的 SQL 可能混有参数和常量导致执行计划缓存命中率下降。短期内不明显但在高频更新场景下各种不同的参数组合会让 SQL Server 的缓存计划膨胀。解决方案是尽量让 SQL 语句的结构保持固定只在参数层面变化。SqlSugar 生成的 SQL 大多满足这一点但当你手动拼一些表达式时就要注意保持结构统一。6. 写在最后的几点实在建议更新操作在 CRUD 里虽然看起来枯燥但它直接关系到数据的正确性和系统的稳定性。我这两年做 SqlSugar 项目的最大体会是写更新的时候多问自己一句“这条语句到底会改哪些行、哪些列”。很多线上问题不是语法不会写而是没想清楚更新范围就动手了。如果你还在用“查询出来再赋值再更新”的套路建议尽快尝试SetColumns表达式和条件更新这两种写法不但代码简洁还能在 SQL 层面规避并发问题。如果你正在处理批量更新记住那个优先级能用一条WHERE IN解决的就别用循环字段值不一样的再考虑逐条更新但一定要分批提交。再分享一个小技巧SqlSugar 可以开启EnableDiffLog来记录更新前后的字段差异这在审计场景下非常有用。我自己做订单系统的时候每次订单状态变更都靠它记录日志比手动比对实体属性省力多了。这算是 Update 玩法里比较进阶的一项功能有机会我再单独写一篇。建议你把这篇文章收藏起来下次写更新语句前翻一翻。尤其是第 5 节那几条避坑记录都是我拿真金白银的线上事故换来的经验。只要把更新范围控制住、并发问题想清楚SqlSugar 的 Update 用起来是真的顺手。
返回列表