老读者应该知道,我写 SqlSugar 系列已经有一阵子了,从基本的查询语法到联表操作都聊过。这阵子在项目里频繁碰更新数据的各种场景,愈发觉得Update这块的写法相当有嚼头。很多人用 SqlSugar 做插入和查询很顺手,一到更新就开始复制粘贴旧代码,要么字段更新不受控,要么条件写不对导致全表被改。所以这篇专门把 Update 的语法整理成一个合集,把我实际踩过的坑和验证过的写法一并交代清楚。
这篇文章适合刚接触 SqlSugar 的 .NET 开发者,也适合已经在用但想系统梳理更新语法的朋友。我会从最基础的实体更新讲起,一路深入到批量更新、条件更新、表达式更新、子查询更新这些进阶玩法,最后附上我在生产环境里遇到的经典问题和排查思路。保证你看完能直接照着写,不用再翻文档。
1. 更新操作比你想的复杂:SqlSugar 的 Update 全景
很多人觉得更新不就是Updateable(entity).ExecuteCommand()嘛,有啥好讲的。但实际做业务系统时间长了就会明白,更新是所有 CRUD 里最容易出问题的环节。查询出了问题最多是性能慢或者数据不对,更新出了问题那可就是线上事故了——字段被覆盖、条件失效导致全表数据被改、并发下库存超卖,哪一个都够喝一壶的。
1.1 为什么 Update 是 ORM 里最难做好的部分
先说一个很反直觉的点:ORM 框架对查询的支持往往非常完善,各种子查询、聚合、导航属性,怎么写都行。但到了更新,框架反而会收着做。为啥?因为查询是无副作用的,而更新是有副作用的,框架不敢给你太多“自由发挥”的空间,否则出错成本太高。
拿原生 SQL 举例,一条UPDATE语句可以拆成三个关键部分:要更新哪张表、要改哪些字段、要过滤哪些行。字段部分和过滤部分稍微配合不好,就会出现“我只想改一条记录,结果把整张表都改了”的惨剧。SqlSugar 的 Update 系列设计,本质上就是围绕这三个核心要素做正交组合,Updateable<T>()指定表和实体,UpdateColumns()指定字段,Where()指定行条件。理解了这条主线,后面所有花哨的写法都能拆解成这三个要素的排列组合。
1.2 SqlSugar Update 能力图谱:一句话记住这些方法
我把常用的更新方法做了一个汇总表,方便后面展开时对照着看。
| 方法 | 作用 | 典型场景 |
|---|---|---|
Updateable(entity) | 按实体主键更新所有非空字段 | 常规单条更新 |
Updateable(entity).UpdateColumns(...) | 只更新指定字段 | 防误改、防覆盖 |
Updateable(entity).Where(...) | 按自定义条件更新 | 不依赖主键的更新 |
Updateable<T>().SetColumns(...) | 用表达式构造更新内容 | 动态赋值、列计算 |
Updateable(list).ExecuteCommand() | 批量更新 | 多条数据一次提交 |
UpdateColumns(...).Where(...) | 指定字段+条件组合 | 精准控制更新范围 |
另外还有异步版本ExecuteCommandAsync(),以及配合事务的用法。我个人习惯的划分是:单条简单更新用实体方式,批量更新用 List 方式,复杂赋值用SetColumns表达式。接下来逐个拆开讲。
2. 基础更新操作:一行代码背后的数据安全逻辑
先讲最基础也是最常用的实体更新。很多教程会告诉你“就是这么简单”,但没人告诉你这里面藏着多少坑。
2.1 实体更新:主键驱动的默认行为
最基本的写法是直接把实体对象丢给Updateable<T>():
var user = new User { Id = 1, Name = "张三", Age = 25, Email = null }; var result = db.Updateable(user).ExecuteCommand();这段代码执行后会生成什么样的 SQL?很多人以为是SET Name=@Name, Age=@Age, Email=@Email,但实际上 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 Name=@Name, Email=@Email,其中Email会被显式更新为 null。需要注意的是,UpdateColumns的表达式里,只要列出了这个字段,不管实体里这个属性的值是 null 还是默认值,都会无条件更新。
这个特性既是优点也是风险。优点是可以让一个属性为 null 的实体也能精确更新指定列,缺点是如果你在UpdateColumns里列了一个没有赋值的属性,数据库里原有的数据就被覆盖成默认值了。比如你更新一个Age属性忘了赋值(默认是 0),结果UpdateColumns(it => new { it.Age })跑完,所有人的年龄都变成 0 了。所以我个人的建议是:使用指定字段更新前,先把要更新的数据完整查出来,再修改目标字段后提交,不要直接拿一个半空的实体去做指定列更新。
2.3 条件更新:不依赖主键的更新姿势
实体更新无论如何都要走主键,这在某些业务场景下不够灵活。比如“把用户状态改为已禁用”这个操作,前端可能只知道用户名称,不想查一次再更新一次。这时候用Where指定条件就方便了:
var result = db.Updateable<User>() .UpdateColumns(user => new User { Status = 0, UpdatedAt = DateTime.Now }) .Where(user => user.Name == "张三") .ExecuteCommand();注意这里Updateable<User>()后面不跟实体对象,而是直接在UpdateColumns里用new User{}的方式指定要更新的列和值。这种写法的好处是字段和值在一处,条件和值完全分离,逻辑非常清晰。
有一点值得强调:Where可以多次调用,多次调用之间是AND关系。如果完全不写Where,那就变成更新全表了。SqlSugar 在这里倒是挺有原则,允许你更新全表(它认为那是你的自由),但会通过最终生成的 SQL 让你意识到自己在干什么。我建议在比较重要的生产表上进行无 WHERE 更新前,先注释掉条件跑一下,看看生成的 SQL 再决定要不要执行。
3. 批量更新与表达式更新:应对真实业务的高阶玩法
基础更新只是热身。真实业务里,你经常会遇到“一次要更新一千条数据”“要根据某列的现有值做计算后再更新”“更新时要把某个子查询的结果带进来”这类需求。这些就需要批量跟表达式的配合了。
3.1 批量更新 List:别被性能忽悠了
批量更新最直觉的写法就是把实体集合丢进去:
var users = new List<User> { 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 List<int> { 1, 2, 3, 4, 5 }; var result = db.Updateable<User>() .UpdateColumns(user => new User { Status = 10, UpdatedAt = DateTime.Now }) .Where(user => ids.Contains(user.Id)) .ExecuteCommand();这条 SQL 最终会被翻译成UPDATE User SET Status=@Status, UpdatedAt=@UpdatedAt WHERE Id IN (1,2,3,4,5),是一条真正的单条 SQL,性能和原子性都要比逐条更新靠谱得多。所以我的结论是:批量更新时先把数据分类——字段值相同也好,不同也好——然后分别选最优写法。
3.2 SetColumns 表达式:让更新拥有计算能力
有些更新不是单纯地把一个值写进去,而是要根据字段本身的现有值做计算。最典型的例子就是库存扣减和点击量累加。如果是常规写法,你得先把数据查出来,在内存里算好,再更新回去,两步之间还有并发风险。SqlSugar 的SetColumns表达式可以直接把计算逻辑写进更新语句里:
var result = db.Updateable<Article>() .SetColumns(article => article.ViewCount == article.ViewCount + 1) .Where(article => article.Id == 100) .ExecuteCommand();注意这里运算符用的是==,这是 SqlSugar 的表达式语法,最终会被翻译成 SQL 里的赋值符号= SET ViewCount = ViewCount + 1。我第一次看到这种写法也愣了一下,但实际用起来是真的方便,尤其是做统计类字段的原子累加。
这个方法最大的价值是规避了并发问题。如果你先查询、再计算、再更新,中间必然有间隙,两个请求同时操作就可能丢失更新。而把计算放进 SQL 里,更新操作是原子的,数据库的行锁保证了这个+1不会被其他事务覆盖。库存场景特别依赖这种写法:
var result = db.Updateable<Product>() .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.Updateable<Order>() .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.Updateable<User>() .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.Updateable<Order>() .SetColumns(order => new Order { CustomerName = SqlFunc.Subqueryable<Customer>() .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 can't specify target table for update in FROM clause"),SqlSugar 虽然帮你写生成逻辑,但底层数据库的限制它也不会替你规避。遇到这种情况,我的做法是把子查询结果先查出来映射成字典,再用SetColumns结合字典做批量更新,绕开这个限制。
4. 事务、异步和性能:生产环境里的 Update 实战心得
语法能跑通只是第一步,放到生产环境里,事务、异步、锁这些都是绕不开的问题。这一节我把自己在真实项目里的处理经验整理出来。
4.1 更新操作要不要手动开事务
单条 UPDATE 在数据库层面是原子的,不需要外部事务。但真实业务往往不止一条更新,比如“下单”这个操作,要扣库存、更新订单状态、写操作日志,涉及多张表多条语句,任意一条失败都需要回滚。SqlSugar 的事务用法和 ADO.NET 一脉相承:
try { db.BeginTran(); var updateResult = db.Updateable<Product>() .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.Updateable<Order>() .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.Updateable<User>() .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.Updateable<User>() .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 Server:MySQL 默认返回的是实际发生变化的行数,SQL Server 默认返回的是匹配到的行数。如果业务对“是否真的有变化”敏感,可以用UPDATE后通过@@ROWCOUNT这类机制去判断,而不是依赖 ORM 的返回值。
第三个原因:Where条件写在了错误的表达式里。我在代码评审里见过好几次这种写法:
// 错误示例:把 Where 条件放进了 UpdateColumns var result = db.Updateable<User>() .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 用起来是真的顺手。