news 2026/10/16 10:38:10

SqlSugar更新语法全解:从单实体到批量更新与AOP审计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SqlSugar更新语法全解:从单实体到批量更新与AOP审计

我先把话说在前面:SqlSugar的Update相关语法,是很多人从LINQ转过来之后第一个被绕晕的地方。上一秒还在用db.Updateable(entity).ExecuteCommand(),下一秒发现不该改的列也被改了;想按条件更新某个字段,又不知道SetColumns和Where到底怎么组合;等到要批量更新几千条数据,又纠结要不要foreach。这篇文章就是把SqlSugar的更新语法按场景拆开,从最简单的单实体更新,到条件表达式更新、批量更新、子查询更新、AOP审计,一次讲透。适合刚接触SqlSugar的.NET开发者,也适合已经用了一段时间、想系统查漏补缺的朋友。

1. 为什么SqlSugar要把Update拆成这么多API

1.1 一条UPDATE语句背后的选择题

先回到SQL本身。任何一条更新语句,本质上就三件事:SET哪些列、赋什么值、WHERE哪一行。

UPDATE Student SET Name = N'张三' WHERE Id = 1

这段SQL看起来简单,但在ORM里做起来麻烦得多。因为ORM面对的是对象,而不是一条写好的SQL。一个实体类可能有三四十个属性,你调用Update的时候,框架必须回答几个问题:

  • 哪个列是主键?能不能当WHERE条件?
  • 这次的更新,是全部列都赋值,还是只更新其中几个?
  • 实体里某些字段是null,要不要把数据库里的值覆盖成NULL?
  • 如果实体没有主键,靠什么定位要改的行?
  • 能不能像写SQL那样,直接在SET里做自增、拼接字符串、取子查询结果?

同一个Update需求,因为这几个问题的答案不同,写法就完全不同。SqlSugar没有把这些细节藏在“智能默认”身后,而是提供了一组API,让你自己决定每一步。理解了这一点,再看Updateable、UpdateColumns、IgnoreColumns、WhereColumns、SetColumns这些方法,就不会觉得是语法堆砌了。

1.2 更新语法的四种基本范式

我把SqlSugar的Update用法归纳成四种范式,后面所有技巧基本都跑不出这四类:

范式代表性API适用场景
实体驱动Updateable(entity)手头有一个完整或部分实体对象,按主键更新
列控制驱动UpdateColumns/IgnoreColumns只想更新个别列,或想保护某些列不被覆盖
条件表达式驱动Updateable<T>()+SetColumns+Where不依赖实体对象,直接用Lambda描述“SET什么、WHERE什么”
批量驱动Updateable(List<T>)一次性更新一批实体,减少数据库往返

实际开发中,条件表达式驱动用得最多,也最安全。因为它的写法最接近SQL语义,可读性强,而且不会因为实体里多了个莫名其妙的字段就误更新。后面每一节都会围绕这四种范式展开。

2. 最常用的一套:按主键更新实体,以及列级控制

2.1 单实体更新的默认行为与最小写法

先看最常见的一段代码:

var student = new Student { Id = 1, Name = "张三", Age = 20 }; var rows = db.Updateable(student).ExecuteCommand();

这段代码生成的SQL大致是这样:

UPDATE Student SET Name = N'张三', Age = 20, Sex = NULL, CreateTime = NULL, IsDeleted = NULL WHERE Id = 1

注意看默认行为:实体里所有的非主键列,都会出现在SET里,包括值为null的属性。也就是说,如果Sex字段在实体里没赋值,那么更新之后数据库里的Sex就变成了NULL。

这个默认行为在日常开发中是个大坑。很多人的实体是从前端接过来的DTO,前端只传了Name和Age,其余字段都是默认值。这时候直接Updateable(entity).ExecuteCommand(),等于把这一行其他字段全部清空。我一开始用的时候也踩过这个坑,后来才养成习惯:要么在实体上提前把所有字段塞满,要么用下一节讲的列控制方法。

还有一点要提醒:ExecuteCommand()返回的是受影响行数。你可以用这个返回值判断更新是否真的命中了一行:

if (rows == 0) { // 说明没有找到 Id = 1 的数据,或者数据本身没变化 }

2.2 UpdateColumns只动指定列,IgnoreColumns锁住不碰的列

当你只需要更新一个实体的某几个字段时,用UpdateColumns:

db.Updateable(student) .UpdateColumns(it => new { it.Name, it.Age }) .ExecuteCommand();

生成的SQL就只有两列:

UPDATE Student SET Name = N'张三', Age = 20 WHERE Id = 1

注意,UpdateColumns里的Lambda返回的是一个匿名对象,这个匿名对象的属性名就是你要更新的列名。如果你不喜欢表达式写法,也可以传字符串数组:

db.Updateable(student) .UpdateColumns("Name", "Age") .ExecuteCommand();

字符串写法适合动态拼列名,比如权限系统里根据用户可编辑字段动态生成更新列表。

与UpdateColumns互补的是IgnoreColumns,意思是“除了这些列,其他都更新”:

db.Updateable(student) .IgnoreColumns(it => new { it.CreateTime, it.IsDeleted }) .ExecuteCommand();

这个写法适合更新一个“几乎完整”的实体,但想保护CreateTime、IsDeleted这种不该被业务操作覆盖的字段。两个方法的选择标准很简单:

  • 更新字段少 -> 用UpdateColumns,明确列出要动的列。
  • 更新字段多、只有一两个字段需要保护 -> 用IgnoreColumns,写起来更省。

2.3 没有主键的表怎么安全更新:WhereColumns与Where兜底

实体有没有主键,SqlSugar的更新行为差别很大。有主键时,默认就用主键当WHERE条件,这个没什么好说的。麻烦的是某些表没有主键,比如历史流水表、日志表、或者早期设计不规范的表。

这时候直接用Updateable(entity).ExecuteCommand(),SqlSugar很可能不知道拿哪一列去定位记录。按我的经验,最关键的是绝不能指望它“聪明地猜”,必须显式指定条件列:

// 假设业务唯一键是 Code,表上没有主键 db.Updateable(new Student { Code = "S001", Name = "新名字" }) .WhereColumns(it => it.Code) .ExecuteCommand();

WhereColumns的作用是告诉SqlSugar:用Code列作为定位条件,而不是主键。生成的SQL是:

UPDATE Student SET Name = N'新名字' WHERE Code = N'S001'

类似的,如果你连WhereColumns都不想用,还可以直接用Where:

db.Updateable(new Student { Name = "新名字" }) .Where(it => it.Code == "S001") .ExecuteCommand();

这两种写法都能在没有主键的情况下精确定位数据。但这里我要特别强调:在没有主键、也不指定任何条件的情况下,某些数据库上SqlSugar会直接更新整表。这属于典型的生产事故级别操作,千万不能靠侥幸。凡是操作没有主键的表,更新前必须先确认条件列,或者先在测试库里跑一遍生成的SQL看有没有漏掉WHERE。

3. 条件更新与表达式更新:SetColumns才是主力语法

3.1 基本条件更新怎么组合

实体驱动式更新有一个天然的局限:你得先有一个实体对象。但很多时候,你只是想把某个字段更新成指定值,比如“把所有状态为0的订单改成已取消”。这时候用Updateable<T>()加SetColumns加Where才是正解:

db.Updateable<Order>() .SetColumns(it => it.Status == 2) .Where(it => it.Status == 0) .ExecuteCommand();

这段代码的意思是:把Status等于0的行的Status设置为2。看清这里的赋值语义,SetColumns(it => it.Status == 2)里的==不是判断相等,而是“将Status赋值为2”。第一眼容易懵,用习惯之后反而觉得顺,因为一眼就能看出“要改哪列、改成什么值”。

多个条件可以直接用&&或||组合:

db.Updateable<Order>() .SetColumns(it => new Order { Status = 2, UpdateTime = DateTime.Now }) .Where(it => it.Status == 0 && it.CreateTime < DateTime.Now.AddDays(-30)) .ExecuteCommand();

如果你更喜欢画线式的写法,也可以把SetColumns换成传入对象:

db.Updateable<Order>() .SetColumns(it => it.Status == 2) .Where(it => it.Id == 1) .ExecuteCommand();

然后注意一个非常重要的坑:不建议连续多次调用.Where()来叠加条件。我印象中某些版本里,多次调.Where()不是把条件累加,而是后面的覆盖前面的。动态条件多的时候,最好用表达式拼接,或者让调用方传一个合并好的表达式树。你可以在本地用LINQPad之类工具打印生成的SQL验证一下,反正我每次看到有人写.Where(a).Where(b)都会多留个心眼。

3.2 让数据库算而不是让程序算:自增、日期、字符串拼接

SetColumns真正强大的地方在于,它不只是支持“赋一个固定值”,还能写表达式,把计算交给数据库执行。最典型的是自增场景:

// 浏览数加1 db.Updateable<Article>() .SetColumns(it => it.ViewCount == it.ViewCount + 1) .Where(it => it.Id == 10) .ExecuteCommand();

这条生成的SQL是:

UPDATE Article SET ViewCount = ViewCount + 1 WHERE Id = 10

注意,自增操作在并发下如果用“先查出来再+1再更新”的写法,很容易丢更新。而直接在数据库里执行ViewCount + 1是原子性的,这才是自增的正确姿势。

类似的用法还有:

// 更新时间 db.Updateable<User>() .SetColumns(it => it.LastLoginTime == DateTime.Now) .Where(it => it.Id == userId) .ExecuteCommand(); // 字符串拼接:给所有VIP用户的名字加后缀 db.Updateable<User>() .SetColumns(it => it.Name == it.Name + "VIP") .Where(it => it.IsVip == true) .ExecuteCommand();

用这一组写法,你就可以少写很多“先Select再Update”的冗余代码。记住一个原则:能够在数据库里一步完成的赋值,不要拉回内存做一遍再写回去。

3.3 关联子查询更新:一个SqlFunc.Subqueryable解决的问题

还有一个高频需求,我单独拿出来说:把A表某个字段更新成B表查出来的值。比如学生表里存了TeacherName冗余字段,老师改名了,学生表的冗余字段也得跟着改。

SqlSugar里可以用SqlFunc.Subqueryable做子查询赋值:

db.Updateable<Student>() .Where(it => it.TeacherId > 0) .SetColumns(it => it.TeacherName == SqlFunc.Subqueryable<Teacher>() .Where(t => t.Id == it.TeacherId) .Select(t => t.Name)) .ExecuteCommand();

生成的SQL大致是:

UPDATE Student SET TeacherName = (SELECT TOP 1 Name FROM Teacher WHERE Id = Student.TeacherId) WHERE TeacherId > 0

这个写法的好处是不用先查出所有学生再逐条更新,一条SQL就把关联更新做完了。如果你觉得子查询可读性差,也可以回退到原生SQL:

db.Ado.ExecuteCommand(@" UPDATE s SET s.TeacherName = t.Name FROM Student s INNER JOIN Teacher t ON t.Id = s.TeacherId WHERE s.TeacherId > 0 ");

原生SQL在复杂关联更新时更直观,但跨数据库移植性差。SqlSugar支持多数据库,只要一个写法能在不同库上翻译出正确SQL,我就优先用ORM。子查询更新就是这种场景。

4. 批量更新:从foreach到一条命令,性能差的根源与解法

4.1 批量更新的正确姿势与参数分片

批量更新最常见的错误姿势是foreach逐条更新:

foreach (var item in list) { db.Updateable(item).ExecuteCommand(); }

这么写功能上没问题,但性能非常差。每一条UPDATE都要经历一次网络往返、SQL解析、事务提交,1000条数据就要打1000次数据库。实测下来,这种写法在数据量过千之后会明显变慢。

正确做法是直接传List:

var studentList = new List<Student> { new Student { Id = 1, Name = "张三", Age = 20 }, new Student { Id = 2, Name = "李四", Age = 21 } }; var rows = db.Updateable(studentList).ExecuteCommand();

SqlSugar底层会把List里所有实体的UPDATE语句合并成一个批处理,一次性发给数据库执行,减少大量往返。在我自己的测试环境里,1000条数据逐条更新可能要十几秒,用批量更新后基本一两秒内完成。当然不同数据库、不同网络环境数据会有差异,但数量级差距是真实的。

如果数据量特别大,比如上万条,我建议手动分片,每500条一批,配合事务保证原子性:

db.Ado.BeginTran(); try { var pageSize = 500; for (var i = 0; i < list.Count; i += pageSize) { var batch = list.Skip(i).Take(pageSize).ToList(); db.Updateable(batch).ExecuteCommand(); } db.Ado.CommitTran(); } catch { db.Ado.RollbackTran(); throw; }

分片的原因主要有两个:一是某些数据库对单条SQL的参数数量有限制,比如SQL Server默认是2100个参数左右;二是单次批处理太大,数据库端的重放日志也会膨胀。分片之后参数数量可控,失败回滚也清晰。

4.2 批量更新最容易忽略的字段覆盖问题

Updateable(List<T>)和单实体更新一样,默认也是把每行对象的所有列都更新一遍。批量更新场景里最危险的画面是:

你先查出一些数据,在内存里改了其中两个字段,然后把这个List交给Updateable执行。看起来只改了两列,实际上每行所有列都被重新SET了一遍,包括你没动过的字段。

解决方案是给批量更新指定列:

db.Updateable(studentList) .UpdateColumns(it => new { it.Name, it.Age }) .ExecuteCommand();

或者配合IgnoreColumns:

db.Updateable(studentList) .IgnoreColumns(it => new { it.CreateTime, it.UpdateTime }) .ExecuteCommand();

这里我再说一个判断经验:如果你批量更新的数据原封不动来自上一次查询结果,那么全列更新问题不大;如果你的数据来自接口入参,或者只组装了部分字段,那必须用列控制。千万不要写“这次侥幸没出事”的代码。

4.3 批量更新失败怎么处理

批量更新不是“执行完就完事”的。注意ExecuteCommand()返回的是受影响的总行数,你可以用它验证批量更新是否完整:

var expected = studentList.Count; var affected = db.Updateable(studentList).ExecuteCommand(); if (affected != expected) { // 说明有一部分行没有被更新,需要进一步排查 }

有的数据库对UPDATE语句的“受影响行数”统计方式不太一样,但大部分场景下,这个数字可以当做一个粗粒度的校验指标。

如果批量更新中途抛异常,数据库通常不会自动回滚所有行,所以我在批量更新大数据量时习惯显式开启事务,像上面那段分片代码一样。注意事务范围内不要做太多无关操作,尽量只把更新逻辑包进去,缩短事务持有时间。

5. 更新过程中的数据保护与审计:AOP、返回值和版本控制

5.1 用AOP统一处理审计字段和SQL日志

SqlSugar的AOP功能在更新场景里特别好用。OnUpdateExecuting会在每次更新执行前触发,你可以在这里拿到即将执行的SQL和参数,做统一处理:

db.Aop.OnUpdateExecuting = (sql, pars) => { // 打印更新SQL,排查问题时非常有用 Console.WriteLine(sql); }; db.Aop.OnUpdateExecuted = (sql, pars) => { // 更新执行成功后,可以记录审计日志 };

我实际项目里经常用这个钩子统一处理审计字段。比如要求所有更新操作都必须把UpdateTime设为当前时间、UpdateUserId设为当前登录人。如果在每个业务方法里手动赋值,很容易漏。统一在AOP里根据参数名修改,就能保证一致性:

db.Aop.OnUpdateExecuting = (sql, pars) => { foreach (var p in pars) { if (p.ParameterName == "UpdateTime") { p.Value = DateTime.Now; } } };

注意参数名的格式可能跟列名不完全一致,用之前最好先打印一遍SQL和参数名确认。AOP是一个很容易被人忽略但非常值得投入的功能,更新逻辑越分散,它越能帮你兜底。

5.2 ExecuteCommand返回值、ExecuteReturnEntity的场景

更新之后要不要拿返回值,分几种情况。

普通更新,只需ExecuteCommand(),拿一个int受影响行数就够了。前面已经说过,用这个数字判断更新是否命中。

有些场景更新后需要拿到完整实体。比如数据库里有触发器、计算列、默认值,你更新完想拿回数据库里的最新值,就可以用Updateable(entity).ExecuteReturnEntity():

var updatedStudent = db.Updateable(student) .UpdateColumns(it => new { it.Name }) .ExecuteReturnEntity();

这个方法更新完成后会返回这一行的最新实体,包含数据库计算出来的字段。不过要注意,它内部会多一次查询,性能上比ExecuteCommand()稍重,非必要不滥用。

异步场景对应的是ExecuteCommandAsync():

var rows = await db.Updateable(student) .UpdateColumns(it => new { it.Name }) .ExecuteCommandAsync();

5.3 版本控制(乐观锁)让并发更新不丢数据

并发更新的经典问题:两个人同时读取同一条数据,A把状态改成1,B把状态改成2,最后谁后提交谁覆盖谁,丢失更新。SqlSugar提供了一套版本控制机制,本质是乐观锁。

使用前先在实体里加一个版本号字段:

public class Student { [SqlSugar.SugarColumn(IsPrimaryKey = true)] public int Id { get; set; } public string Name { get; set; } public int Version { get; set; } }

更新时调用VersionCode:

var rows = db.Updateable(student) .VersionCode(it => it.Version) .ExecuteCommand(); if (rows == 0) { // 版本号已经不匹配,说明数据被其他人改过了 }

SqlSugar会在生成的SQL里带上版本号条件,类似:

UPDATE Student SET Name = N'张三', Version = Version + 1 WHERE Id = 1 AND Version = 0

如果执行时数据库里的Version已经不是0,说明这行数据已经被其他人更新过,本次更新影响行数为0。你在业务里拿到这个0,就可以提示用户“数据已变化,请刷新后重试”。这套机制建议在所有需要编辑保存的场景里用起来,成本低,防丢更新效果很直接。

6. 更新语法速查表与避坑备忘

6.1 常见更新场景的写法速查

需求推荐写法关键点
按主键更新实体所有非主键列Updateable(entity).ExecuteCommand()空值也会覆盖到数据库
只更新指定列Updateable(entity).UpdateColumns(x => new { x.Name }).ExecuteCommand()适合部分字段更新
忽略指定列Updateable(entity).IgnoreColumns(x => new { x.CreateTime }).ExecuteCommand()适合保护敏感字段
按业务条件更新Updateable<T>().SetColumns(x => x.Status == 2).Where(x => x.Id == 1).ExecuteCommand()不依赖完整实体
字段自增SetColumns(x => x.ViewCount == x.ViewCount + 1)原子操作,并发安全
批量更新Updateable(list).UpdateColumns(...).ExecuteCommand()避免foreach逐条更新
更新值来自子查询SetColumns(x => x.Name == SqlFunc.Subqueryable<T>()...)可做关联表同步
更新后返回完整实体Updateable(entity).ExecuteReturnEntity()多一次查询
异步更新ExecuteCommandAsync()配合async/await
乐观锁更新Updateable(entity).VersionCode(x => x.Version).ExecuteCommand()防并发丢更新

这张表基本覆盖了日常工作里九成以上的更新需求。剩下的个性化场景,基本都是这些API的组合。

6.2 我踩过的三个Update坑

第一个坑,就是实体里的null字段把数据库值覆盖成NULL。那次是给一套老系统加批量修改功能,前端只传了主键和两个要改的字段,我用Updateable(entity)直接执行,结果一条更新把所有客户资料的备注、联系电话全清空了。当时吓得赶紧回滚,后来统一改成UpdateColumns明确指定列,再也没出过事。

第二个坑,是没有主键的表直接调用Updateable(entity)。代码没报错,执行完才发现整表都被改了一遍。从此我给自己定了一条死规矩:凡是实体上没标主键的,更新之前必须显式指定WhereColumns或Where,不放行默认更新。

第三个坑是动态条件拼接。多个来源的条件需要同时生效时,向Where里传入动态组合的表达式树,不要靠多次调用Where。我有一次就因为这个写出来的SQL条件只剩了最后一个,排查了很久才定位到是链式调用覆盖的问题。

6.3 关于更新语法的使用建议

最后说点个人体会。SqlSugar的更新API看着多,实际记住一条主线就行:默认实体更新是“全列覆盖”,不想全列覆盖就用UpdateColumns或IgnoreColumns控制;更精细的更新直接走Updateable<T>().SetColumns(...).Where(...)表达式路线。我现在的项目里,超过八成的更新都是SetColumns + Where写的,实体驱动的更新只保留在“整行保存”的场景。

如果你还在困惑某条更新为什么没生效,先别急着怀疑框架。把db.Aop.OnUpdateExecuting里的SQL打印出来看一遍,八成问题出在条件没拼对,或者有字段被意外置空。ORM只是把你的意图翻译成SQL,翻译出来的SQL好不好、该不该执行,最终还是得开发者自己心里有数。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 10:35:13

净现值查询SQL题解:LEFT JOIN与空值兜底的经典实践

1. 题目拆解&#xff1a;净现值查询到底在考什么 1.1 题目原意与表结构 力扣1421这道题&#xff0c;我愿称之为"数据库入门到进阶的分水岭"。题目本身描述得非常简洁&#xff1a;给定一张 NPV 表&#xff08;净现值表&#xff09;和一张 Queries 表&#xff08;…

作者头像 李华
网站建设 2026/10/16 10:33:55

Outlook阅读窗格跑偏怎么办?从视图设置到DPI兼容性全面解决

周一早晨的固定节目&#xff1a;打开Outlook&#xff0c;屏幕右侧冒出熟悉的邮件预览窗格&#xff0c;而我一向习惯把它放在底部。这个问题在联想笔记本上尤其高频——外接显示器拔插、系统更新重启、或是后台驱动工具刷新了一轮显卡驱动之后&#xff0c;Outlook 的窗格布局就会…

作者头像 李华
网站建设 2026/10/16 10:33:46

Docker Compose部署SpringBoot+MySQL前后端分离系统

我先把这套系统放到一台全新的 Ubuntu 服务器上跑通了一遍&#xff0c;过程比预想的曲折&#xff0c;但最后用 Docker Compose 一键拉起的那一刻&#xff0c;整个前后端分离物业管理系统从数据库到静态页面全部就绪。这篇把整个部署过程掰开揉碎&#xff0c;从环境准备、项目改…

作者头像 李华
网站建设 2026/10/16 10:33:16

硬件设计的本质:系统性工程决策与物理约束闭环

1. 这不是画电路图&#xff0c;而是给机器“定骨骼”和“塑神经”“什么是硬件设计&#xff08;Hardware Design&#xff09;&#xff1f;”——这个问题我被问过不下两百次&#xff0c;提问者身份跨度极大&#xff1a;刚进电子系大一的学生、转行做嵌入式开发的程序员、想自研…

作者头像 李华
网站建设 2026/10/16 10:33:10

OpenClaw生产级高可用部署:性能调优与稳定性实战指南

这个系列的读者应该都有体会&#xff1a;OpenClaw在本地或者单机环境跑起来非常简单&#xff0c;装好依赖、配好模型Key、启动服务&#xff0c;一个能帮你查资料、写周报、管日程的AI助理就能用了。但真要让它7x24小时在线&#xff0c;随时响应&#xff0c;不卡死、不丢任务、不…

作者头像 李华
网站建设 2026/10/16 10:32:38

Spring Boot 整合 ElasticSearch 7.6.x 实战:版本选型、环境搭建与性能调优

1. 为什么在 Spring Boot 项目里选 ElasticSearch 7.6.x 而不是别的版本很多人在技术选型时容易陷入一个误区&#xff1a;看到 ElasticSearch 出了新版本就无脑升级&#xff0c;结果项目跑到一半发现客户端不兼容、API 大改、文档对不上。我自己在这个问题上反复折腾过好几轮&a…

作者头像 李华