ARTICLE DETAIL

资讯详情

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

SqlSugar更新数据语法全解:实体更新、批量更新与联表更新

SqlSugar更新数据语法全解:实体更新、批量更新与联表更新 做.NET开发的朋友应该都有过这种感受ORM的插入和查询用法都很好上手唯独更新Update这块越用到后面越别扭。单条更新好办批量更新就慢简单更新好写遇到“只更新某几列”“按条件批量改字段”“要跟另一张表的数据联动更新”的时候经常不知道API该怎么拼。这篇文章就把SqlSugar.NET开源ORM框架的Update更新数据语法整体梳理一遍把实体更新、指定列更新、条件更新、联表更新、批量更新、原生SQL更新这些场景一次讲透再附上我在实际项目里踩过的坑和性能优化结论。适合正在用或者准备用SqlSugar的朋友当一份“更新手册”来收藏。1. 为什么SqlSugar的Update值得单独写一篇三种更新模式的取舍SqlSugar的更新设计跟Insert不一样Insert基本就是Insertable一条路走到底而Update围绕Updateable扩展出了好几套语义。刚开始接触的人很容易被绕晕因为同一个“更新”动作可以有三种完全不同的写法而且它们背后的执行策略也不同。我把日常开发里最常用的更新路线归纳成三条。更新模式主入口典型场景性能特点实体整体更新Updateable(entity)表单回填、主表整行同步会把实体所有列都拼进SET简单但列粒度粗指定列/条件更新UpdateableT().SetColumns(...)只改状态、只改备注、数值加减列可控可做表达式计算适合批量业务操作原生SQL更新db.Ado.ExecuteCommand(...)跨表子查询、复杂CASE WHEN、数据库特有语法完全交给SQL性能上限最高但绕开了ORM映射先记住这三条线后面所有代码都是在这三根主线上变形。为什么选型这么重要因为更新语句是直接产生数据库行锁和IO的ORM更新粒度如果控制不好会把整行所有列都写进SET哪怕只有一个字段变了也会产生无意义的写入极端情况下还会覆盖掉别的请求刚改好的数据。比如我一个学生表实体页面只改了姓名实体里其他字段还是旧值如果直接把整个实体丢给Updateable执行年龄、班级、创建时间这些列全都会被重新写一遍等于用旧数据覆盖新数据。这是更新操作里最容易翻车的地方后面的实战里我会反复强调。选型逻辑上我个人的习惯很简单如果是从“表单提交”过来的数据直接实体更新如果是定时任务、批处理、接口里针对某个字段做业务变更一定用SetColumns指定列如果逻辑已经复杂到要在SQL层面做计算或者跨表联动不要硬用ORM的壳去套老老实实写原生SQL反而干净。2. 实体更新看似简单但默认行为和坑比你想的多2.1 基础写法与返回影响行数实体更新是最容易上手的写法一条语句就能完成更新。// 数据库表 student实体类 Student var student new Student { Id 1, Name 张三, Age 20, CreateTime DateTime.Now }; // 默认以实体主键作为更新条件 int rows db.Updateable(student).ExecuteCommand();ExecuteCommand()返回的是受影响行数这个返回值很有用。比如更新一条记录时如果rows 0通常意味着这条数据在数据库里已经不存在或者主键条件没匹配上并不一定代表更新失败。很多新手在这里会把0当成异常直接抛错实际上更好的做法是把它当作“预期内的情况”来处理。实体更新也可以手动指定条件写法是db.Updateable(student) .Where(it it.Id 1) .ExecuteCommand();这里要提醒一句如果实体本身有主键你又写了Where那么这两个条件是叠加关系不是替换关系最终SQL里会既有主键条件又有你写的条件。这个叠加行为在多数场景下是安全的但如果你写了一个跟主键矛盾的条件更新行数可能一直是0排查起来容易让人怀疑人生。2.2 更新时如何精确控制列集合IgnoreColumns与UpdateColumns实体更新最大的问题是“什么都更新”。一个实体十几个字段可能只有两三个需要变更剩下的全是旧值。这时候就要过滤列。忽略指定列db.Updateable(student) .IgnoreColumns(it new { it.CreateTime, it.Sort }) .ExecuteCommand();只更新指定列db.Updateable(student) .UpdateColumns(it new { it.Name, it.Age }) .ExecuteCommand();注意两条API的语义是反的IgnoreColumns是“除了这些列都更新”UpdateColumns是“只更新这些列”。二者别混用而且我不建议同时写一次更新里同时出现过滤和指定容易把自己绕进去维护代码的人也会骂。这条API特别适合那种“实体里带着全量数据但业务上只需要动两三个字段”的场景。比如修改用户资料只有昵称和头像变了用UpdateColumns指定这两列不仅能减少SQL长度还能避免并发情况下其他字段被无意义覆盖。2.3 实体更新里的空值陷阱null列到底会不会被更新这是我从EF和Dapper转过来的朋友问得最多的问题。潜意识里大家觉得“属性是null数据库就别更新这个字段了”但SqlSugar的默认行为和你想的不一样。默认情况下Updateable(student)会把实体所有列都拼进SET包括值为null的列——也就是说一个属性为null它会把数据库对应字段更新成NULL。如果你从页面拿到的实体里有些属性本来就是null比如用户没填的备注执行完之后数据库里原来的值就没了。想要过滤掉null列可以用这个参数db.Updateable(student) .IgnoreColumns(ignoreAllNullColumns: true) .ExecuteCommand();ignoreAllNullColumns: true的意思就是“值为null的列不参与更新”这样更新语句里只会包含有值的字段。这在做“部分字段更新”时非常实用等于用null来当过滤条件。但也要注意这个参数跟Where条件叠加时语义要理清楚Where管的是更新哪些行IgnoreColumns(ignoreAllNullColumns: true)管的是更新哪些列两者互不干扰。如果你有个字段本来就要置空靠null过滤就置不了空了那种情况需要单独用SetColumns显式赋值null。2.4 Where条件更新与并发安全不要全表更新实体更新时如果不写WhereSqlSugar会用主键作为更新条件。一旦实体里主键没有赋值或者实体本身没有主键危险就来了——轻则报错重则全表更新。我在项目中见过一次事故原因就是代码从EF框架迁移过来老代码里习惯把主键放在一个单独的“查询实体”里传更新实体上主键一直是默认值0。结果执行更新时条件变成了WHERE Id 0数据库自然匹配不到行但如果主键列缺失或者用了别的字段做条件情况就更糟了。所以我在团队里立了一个规矩写更新语句时先写Where再写更新内容。哪怕实体有主键也建议在关键更新里显式把主键条件写出来防止实体属性被改动后误伤数据。并发控制是另一个容易被忽略的点。后提交的更新如果只按主键做条件就很容易覆盖先提交的修改。常见做法是引入版本号和乐观锁db.UpdateableStudent() .SetColumns(it new Student { Name 新名字, Version it.Version 1 }) .Where(it it.Id 1 it.Version 10) .ExecuteCommand();这里Where里带上版本号条件如果影响行数为0说明版本号已经变了也就是数据被别人改过需要重新读取再做合并。这种方式比锁表优雅得多也是我处理更新并发时优先推荐的方式。3. 列更新与无实体更新只更新业务字段的标准姿势3.1 SetColumns细粒度列更新的核心API如果让我只挑一个更新API来推荐那一定是SetColumns。它是SqlSugar更新能力里最灵活的一个既能指定更新哪几列又能在赋值表达式里做计算。db.UpdateableStudent() .SetColumns(it new Student { LoginCount it.LoginCount 1, LastLoginTime DateTime.Now }) .Where(it it.Id 1) .ExecuteCommand();这段代码翻译成SQL大概就是UPDATE student SET LoginCount LoginCount 1, LastLoginTime GETDATE() WHERE Id 1注意LoginCount it.LoginCount 1这个写法它让“在原来字段值基础上叠加”变成了一件很自然的事。库存扣减、访问量加一、累计积分更新这些操作用SetColumns写起来比先查出旧值再算新值再更新要优雅得多而且还避免了“读后写”的并发问题因为计算是在数据库里完成的。SetColumns还支持把另一个字段的值赋给当前字段db.UpdateableStudent() .SetColumns(it new Student { Name it.Code - it.Id }) .Where(it it.Id 1) .ExecuteCommand();这种“同一条记录内部字段互相赋值”的SQL能力传统写法要先查出来再更新SetColumns直接一步到位。我在做数据订正脚本时经常用它。3.2 匿名对象更新当实体缺失或字段不全时怎么办有些表在项目里没有对应的实体类或者界面传来的数据只是一部分字段这时候可以借用现有实体类型配合匿名对象构造更新内容。db.UpdateableStudent() .SetColumns(new Student { Name 王五 }) .Where(it it.Id 2) .ExecuteCommand();也可以直接传匿名对象写法更轻db.UpdateableStudent() .SetColumns(new { Name 王五, Age 25 }) .Where(it it.Id 2) .ExecuteCommand();这种写法的优势是“根本没有完整实体只有要改的字段和值”代码意图一目了然。尤其是对接外部接口、写临时修复脚本的时候非常实用。3.3 通过字典更新动态字段有些项目会做动态字段设计列名都是运行时拼出来的比如扩展属性表、配置表、自定义字段表。这种情况下实体方案就不好用了可以用字典来更新。var updateColumns new Dictionarystring, object { [name] 赵六, [age] 30 }; db.UpdateableStudent() .SetColumns(updateColumns) .Where(it it.Id 3) .ExecuteCommand();这里要特别注意字典里的key是数据库真实列名不是实体属性名。比如实体属性叫UserName数据库列叫name那字典key就是name。如果实体上有SugarColumn映射也是以数据库列名为准。我之前在项目里把属性名直接当成key传进去结果生成的SQL里列名对不上一执行就报“列名无效”排查了半天才发现是大小写和映射的问题。SetColumns配合字典还有个好处可以完全绕开实体只对表的一部分列做更新。这套组合在处理“列名动态变化”的业务里几乎是唯一解。4. 复杂场景联表更新、原生SQL与数据库方言差异4.1 联表更新统计字段和冗余字段同步实际开发中经常遇到这种需求订单更新时要同步更新用户的累计消费金额或者学生换了班级要把班级表里的学生人数同步调整。这类跨表更新SqlSugar从5.x版本开始支持用JOIN的方式写联表更新。db.UpdateableStudent() .InnerJoinClassInfo((s, c) s.ClassId c.Id) .SetColumns(s new Student { ClassName c.Name, ClassYear c.Year }) .Where((s, c) c.Grade 3) .ExecuteCommand();这段代码生成的SQL类似UPDATE s SET s.ClassName c.Name, s.ClassYear c.Year FROM Student s INNER JOIN ClassInfo c ON s.ClassId c.Id WHERE c.Grade 3这个功能最适合做“冗余字段同步”。比如我在一个项目里订单表冗余了用户名称和用户手机号用户改昵称后需要把历史订单里的昵称同步更新用联表更新一条SQL就搞定了省去了“先查出一批订单再逐条改”的繁琐流程。这里我前面的写法在SqlSugar 5.1.x版本上可以正常跑如果你的版本比较旧建议先翻一下release notes确认是否支持不支持的情况下可以直接用原生SQL写联表更新。4.2 直接执行更新SQL的场景与参数化写法有些更新逻辑用ORM怎么拼都别扭比如要在更新时引用子查询结果、要对多行做复杂的条件分支判断。这时候我建议直接走Ado层执行SQL简单粗暴。int rows db.Ado.ExecuteCommand( UPDATE student SET age age 1 WHERE class_id classId, new { classId 2 } );注意参数化写法用classId占位符然后传入匿名对象这是防SQL注入的基本操作不要拼字符串。常见的一个场景是根据分数区间批量打标签UPDATE student SET status CASE WHEN score 90 THEN A WHEN score 80 THEN B ELSE C END WHERE class_id classId这种CASE WHEN批量更新逻辑用ORM实体更新写起来很笨因为不同行的更新值不同SqlSugar的SetColumns又没法写“每行根据自身字段动态判断”的逻辑。原生SQL一条语句全部搞定。批量更新数据订正时我基本都会想一下“这个逻辑用SQL写是不是更清晰”如果是就直接用ExecuteCommand。写原生SQL时要记住字段名必须用数据库真实列名不走实体映射。如果不确定列名先查一下表结构或者用db.Ado.GetDataTable(SELECT TOP 1 * FROM student)看返回的列名。4.3 数据库方言差异对Update写法的影响SqlSugar支持多种数据库但Update语法在不同数据库之间是有差异的。这一点团队里如果有人负责多个数据库版本的兼容最容易在这里踩坑。不同数据库的联表更新语法就不一样。MySQL的写法是UPDATE t1 JOIN t2 ON ... SET ...SqlServer则是UPDATE t1 SET ... FROM t1 JOIN t2。SqlSugar的Updateable InnerJoin会自动根据当前数据库方言生成对应语法这也是我优先推荐用它写联表更新的原因之一。再比如分页更新SqlServer有UPDATE TOP (100)MySQL有UPDATE ... LIMIT 100Oracle又是另一套。如果项目要在不同数据库之间切换这类数据库特有的更新写法尽量少直接写在ORM调用里——要么抽成按数据库类型判断的SQL要么用SqlSugar提供的统一API别让自己写的原生SQL成为迁移时的定时炸弹。还有一个很现实的点MySQL没有像Oracle那样自带“回闪”能力更新完了想还原旧值只能靠备份或事务。所以在大批量更新之前我习惯先开事务或者先备份数据。这点怎么强调都不为过因为更新语句一旦跑错很难恢复。5. 大批量更新时SqlSugar的性能表现与分批策略5.1 从循环Update到批量Update一次老项目改造实测先说我刚接手一个老项目时的真实情况。有个定时任务每天要把一万多条订单的冗余字段从用户表同步过来。老代码写得很“直观”foreach (var item in orderList) { db.Updateable(item).ExecuteCommand(); }一万条数据就是一万次数据库往返任务跑完基本要十几秒高峰期还会拖慢数据库。我把这段代码改成db.Updateable(orderList).ExecuteCommand();改成批量更新后同样的数据量耗时降到一秒左右具体耗时跟字段数量和数据库性能有关但量级上差距非常大代码行数还少了很多。为什么差别这么大因为循环单条更新每次都发起独立的数据库连接请求拼接SQL、发送、等待、解析绝大部分时间都花在网络往返上。而SqlSugar的批量Updateable(list)会把同一个实体列表合并成一组更新语句在一次连接内执行完可以理解为“把一万次小请求打包成了几次大请求”。当然它内部生成的仍然是参数化的多条UPDATE语句不是像BulkCopy那种数据库原生批量写入但省掉的往返开销已经是质变了。5.2 数据量与执行时间的实测趋势我在本地环境测过一个简单的三字段表更新5000条记录的耗时趋势循环单条更新大概需要6~10秒Updateable(list)批量更新在几百毫秒到1秒之间如果再把批量更新拆成每批1000条总耗时相差不大但内存和数据库锁的占用会更平稳。我整理了一个方便自己参考的口径更新方式1万条耗时量级特点循环单条Update10秒以上简单但性能极差坚决不要写在线程里Updateable(list)整体更新1秒左右推荐首选适合大多数批量场景Updateable(list)分批更新每个批次毫秒级适合超大数据量或需要事务包裹的场景需要说明的是具体数据在不同数据库、不同表结构和网络环境下会有差异但“循环单条更新比批量更新慢一个数量级以上”这个结论在大多数项目里都成立。性能敏感的业务建议自己建个表实测一下用数据说话。5.3 分批更新与事务的搭配以及锁的范围控制批量更新也有限制主要来自数据库参数数量。如果一条记录更新5个字段1000条数据就是5000个参数而SQL Server的参数上限是2100个。这种情况下把1万条数据一次性扔给Updateable(list)可能直接报参数太多。我的解决办法是分批每批数量按“字段数量”倒推。一般经验是字段少2~3个时每批可以放到1000条字段多8个以上时每批控制在300~500条比较稳。// .NET 6 的 Chunk 可以按数量分割集合 foreach (var batch in orderList.Chunk(500)) { int rows db.Updateable(batch) .UpdateColumns(it new { it.Status, it.Remark }) .ExecuteCommand(); }如果整个更新要求原子性比如“要么全部成功要么全部回滚”就把批处理包进事务里var result db.UseTran(() { foreach (var batch in orderList.Chunk(500)) { db.Updateable(batch).ExecuteCommand(); } }); if (!result.IsSuccess) { // 记录失败原因事务内部已经自动回滚 }用UseTran比手动BeginTran/CommitTran/RollbackTran省心因为它把异常和回滚封装好了。但要注意大事务会持锁时间变长在线业务高峰期容易导致别的请求阻塞。如果更新的是核心业务表我建议要么不包大事务要么把批大小控制得保守一点宁可多执行几次也别让锁范围扩大。6. 更新操作中容易踩的中招记录与最终建议6.1 我踩过的三个Update相关的坑第一个坑是时间字段被覆盖。实体更新时CreateTime这种“创建时写入、之后不该变”的字段也被带进了SET子句。我自己遇到过用户每次改昵称这条记录的创建时间就被刷新成当前时间排查了很久才发现是实体更新把所有列都更新了。解决方法很简单更新前把时间字段IgnoreColumns忽略掉或者直接用SetColumns只更新业务字段。第二个坑是自增主键没赋值导致更新不到行。从接口传过来的实体如果主键赋值为0或者根本没传更新条件就可能是WHERE Id 0SQL不报错但就是影响0行。这种问题在日志里很难发现因为你看到的是“更新成功了但数据没变”。我的习惯是更新前先校验关键条件字段如果Id小于等于0直接抛业务异常不让无效更新继续往下走。第三个坑是字典更新的列名问题。用Dictionarystring, object更新时我错把实体属性名当成列名传进去结果执行时报“列名无效”。SqlSugar的字典更新走的是数据库列名体系跟实体属性名不是一回事。如果你用了SugarColumn自定义列名这个坑更容易踩。建议在字典更新前先打印一下实际生成的SQL确认列名。6.2 更新前必做的几个检查项我总结了一套自己的更新前检查流程团队里也一直按这个执行确认更新条件有没有主键或者显式Where条件字段值是否正确会不会匹配到0行或全表确认更新列范围是整实体更新还是指定列更新有没有不该动的列混进去确认null处理方式值等于null的字段是应该忽略还是就是要置空确认并发策略多条更新可能同时命中同一行的时候是否加了版本号条件确认数据可回滚影响行数很多、涉及核心表之前是否包了事务或者有备份这五项看一遍用不了两分钟但能挡住绝大多数的更新事故。6.3 更新这套语法的整体学习顺序如果你刚开始接触SqlSugar的更新我建议的学习路径是先掌握实体更新和SetColumns这两种它们覆盖了日常80%以上的场景然后学批量Updateable(list)这是性能提升最明显的一步最后再看联表更新和原生SQL这些是复杂业务下用来兜底和提效的武器。没必要一开始就把所有API都背下来碰到对应场景再查就行。SqlSugar的Update语法相比纯手写SQL的舒服之处在于它把“更新意图”表达得非常清楚——要更新哪些行写在Where里要更新哪些列写在SetColumns里两者完全分离代码读起来就是人话。这也是为什么我在团队里推荐优先用ORM做更新而不是上来就拼SQL。我个人在实际项目里最深的体会是更新操作的安全边界比性能重要得多。性能不行可以加索引、可以分批但一次错误的全表更新或者并发覆盖可能需要好几个小时才能收拾残局。所以每次写更新语句之前我都会问自己一句这一条更新到底会动到哪些行、哪些列、影响多少人。能回答清楚这个问题再烂的API也能用得稳稳当当。
返回列表