ARTICLE DETAIL

资讯详情

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

SqlSugar Update 语法全解析:从基础到批量更新与踩坑指南

SqlSugar Update 语法全解析:从基础到批量更新与踩坑指南 在 .NET 服务端开发里ORM 框架的选择一直是个热闹话题。从 EF Core 到 Dapper 再到 SqlSugar每家都有自己的忠实用户。SqlSugar 能在大量老项目和中小团队里扎根不是靠概念包装而是靠那套贴合业务直觉的更新语法——尤其 Update 系列写法简单、直接、一眼看懂又能覆盖绝大多数更新场景。如果你正在用 .NET 写增删改查或者正从别的 ORM 迁到 SqlSugar这篇把 Update 的常用语法、参数含义和踩坑点全部理一遍可以直接当字典查。我最早接触 SqlSugar 是做几个运营后台系统刚开始天天手拼 SQL一个“改用户资料”的需求写了快两百行条件分支后来换成db.Updateable(...)之后代码量至少减了一半。这篇文章把这些年用过的 update 场景整理成合集不保证覆盖所有偏门 API但日常开发里你能遇到的更新需求基本都能从这里面找到对应的姿势。老手可以直接跳到第三章新手建议从头看因为越前面的知识点越容易被忽略而忽略的代价往往是线上数据被批量改坏。1. 先把 Update 语法的全局逻辑讲清楚1.1 为什么 SqlSugar 值得单独整理一份更新合集很多开发会觉得ORM 不都是根据实体生成 SQL 吗Update 有什么好讲的实际上更新操作和 Insert、Select 有本质区别插入关心“插哪些字段”查询关心“筛哪些条件”而更新必须同时处理好“赋值”和“条件”两层逻辑。条件写宽了整张表被改赋值列写多了无关字段被覆盖。这两个问题在业务代码里一旦出现往往是事故级别。SqlSugar 的UpdateableAPI 恰恰就是围绕这两层逻辑设计的。它的链式方法把更新拆成三块表是谁、要给哪些列赋值、在什么条件下生效。理解了这套模型之后你不需要去背每个方法的签名而是自然知道去哪里查赋值相关的方法叫SetColumns或UpdateColumns条件相关的方法叫Where或WhereColumns执行方法只有一个叫ExecuteCommand。这套设计还有一个好处是贴近业务表达。我经常给团队里不太熟悉 SQL 的初级开发看代码他们读SetColumns(it it.Name 张三)的时候第一反应是“这是个比较表达式”其实是赋值敲过一次之后就能记住。这种“把 SQL 语义翻译成链式方法”的做法让更新逻辑变得可读、可拼装、可动态控制也是我愿意持续用它的原因。适合什么人看这篇合集一种是项目里已经在用 SqlSugar但老是记不住更新 API 的另一种是刚接触 .NET ORM想对比一下更新写法的。如果你是后者建议先打开 SqlSugar 官方文档的更新章节配合这篇一起看效果会好很多。1.2 从一条 SQL 到一行链式调用Updateable 封装了什么先回顾最原始的 SQL 更新长什么样UPDATE user SET name 张三, phone 13800138000, update_time NOW() WHERE id 10086;这条 SQL 一共干三件事指定表user设置三列的新值限定id 10086这一行。SqlSugar 的Updateable做的就是把这三件事分别映射到对应的链式方法上表由泛型UpdateableUser()决定也可以用.AS(表名)改表名。赋值由实体对象本身或者SetColumns、UpdateColumns决定。条件由Where、WhereColumns决定或者依赖实体主键自动生成。执行ExecuteCommand()发送 SQL返回受影响行数。我经常和一个比喻Updateable像一个装修队泛型是房子的地址赋值是“你要翻新哪些区域”条件就是“只翻新几号房其他房间别碰”最后ExecuteCommand是签字验收。ExecuteCommand不调用前面的链式配置再多也不会真正执行更新这个特性也让开发者可以像拼积木一样动态构造更新语句。还有一点容易忽略SqlSugar 生成更新语句时会使用参数化查询。比如刚才那条 SQL最终发给数据库的可能是name name这样的参数形式。这样做有两个好处一是能有效避免 SQL 注入二是数据库可以缓存执行计划。很多开发从手拼 SQL 转过来之后最不适应的就是这一点但实际上这恰恰是 ORM 框架帮你兜底的地方。1.3 主键SqlSugar 默认用什么决定更新范围更新操作最怕“不知道更新了谁”。SqlSugar 在实体更新时默认会尝试使用实体主键作为更新条件。也就是说如果你的User类里有一个标了IsPrimaryKey true的属性并且实体对象里的主键值有值那么db.Updateable(user).ExecuteCommand()会自动生成带主键条件的WHERE。但这里有一个必须警惕的点如果实体没有主键或者主键值没有赋值SqlSugar 的行为就不是那么“安全”了。不同版本的策略会有差异有些情况会尝试把所有列都拼到 WHERE 里做全列匹配导致一条都更新不中有些情况可能生成没有条件的更新语句直接全表覆盖。无论哪种结果都是业务灾难。所以我给自己的团队定了一条铁律没有配置主键的实体绝对不允许裸调ExecuteCommand()必须先写WhereColumns或Where显式指定更新条件。在下面的章节我会反复强调这一点因为它值得刻在脑门上。2. 最基本的实体更新与批量更新2.1 单实体更新确定主键之后直接调最基础的用法是传入一个实体对象。假设你的User类长这样public class User { [SqlSugar.SugarColumn(IsPrimaryKey true)] public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } public DateTime UpdateTime { get; set; } }那么更新单条记录只需要var user new User { Id 1, Name 张三, Age 18, UpdateTime DateTime.Now }; var rows db.Updateable(user).ExecuteCommand();这个操作生成的 SQL 大致是UPDATE user SET name name, age age, update_time update_time WHERE id id;这里有几个容易被坑的地方。第一Updateable(user)默认会把实体里所有非空字段都放进 SET 语句具体哪些列会更新取决于列是否允许为空以及版本行为。我见过很多人在实体更新时把不想动的字段也赋了旧值结果白白产生一次无意义的写操作。第二ExecuteCommand返回的rows是影响行数如果更新前后值一模一样很多数据库返回的影响行数是 0不代表更新失败。后面常见问题里再详细说。如果实在不放心主键会不会被识别可以显式指定更新条件列var rows db.Updateable(user) .WhereColumns(it it.Id) .ExecuteCommand();WhereColumns的意思是“用 Id 这一列的值作为每一行的定位条件”即使实体没有配置主键这个写法也能正常更新。2.2 批量更新别在 foreach 里逐条 Update新手最常见的错误是拿到一个ListUser之后在循环里调用db.Updateable(single).ExecuteCommand()。数据量小的时候确实能跑但几百上千条之后性能直线下降还容易把事务边界搞乱。SqlSugar 直接支持传集合var list new ListUser { new User { Id 1, Name 张三, Age 18 }, new User { Id 2, Name 李四, Age 22 }, new User { Id 3, Name 王五, Age 25 } }; var rows db.Updateable(list).ExecuteCommand();这个操作并不是生成一条“一个 UPDATE 更新多行”的 SQL而是框架内部逐条生成更新语句并在一个事务包裹下执行。好处是具备基本的原子性中间只要有一条失败整个批量更新都会回滚。坏处是数据量大了之后性能依然不理想所以 SqlSugar 提供了PageSize方法来分批执行这个我放到性能优化小节细讲。批量更新还有一个注意点如果列表里的实体带有主键默认按主键更新如果主键缺失或者你想按照业务字段去定位就要使用WhereColumns。比如按照 用户编号区域编号 来更新var rows db.Updateable(list) .WhereColumns(it new { it.RegionId, it.UserId }) .ExecuteCommand();这段代码的意思是同一个RegionId和UserId对应的记录会被更新为该实体中携带的其他列值。这个写法在处理 Excel 导入后修正数据时尤其常用。2.3 异步方法与受影响行数ASP.NET Core 项目里我基本都会用异步版本。SqlSugar 的更新方法基本都有对应异步版本var rows await db.Updateable(user).ExecuteCommandAsync();为什么要强调异步Web 应用在高并发下如果线程池里的线程都阻塞在数据库操作上吞吐量会明显下降。而且 SqlSugar 的异步 API 不只是包一层 Task内部会正确处理连接和异步 IO所以能用 async 就用 async。rows这个返回值也别把它当摆设。我习惯在更新用户状态这类关键操作后判断返回值if (rows 0) { // 不能直接认定失败但可以单独处理 }为什么不能直接认定失败因为“影响行数为 0”可能有三种情况记录不存在、条件不满足、数据值没有变化。如果业务要求严格应该再查一次数据或者用更高阶的乐观锁方案来判断到底有没有冲突。3. 按条件更新与局部列控制3.1 SetColumns只更新你指定的字段而不是整个实体实体更新虽然方便但最大的问题是“不可控”。比如编辑用户资料时前端只会传过来姓名和年龄但实体里还有手机号、状态、积分等字段。如果直接用实体更新就必须先把旧实体查出来整个赋一遍否则会把自己的字段覆盖掉。更优雅的方案是SetColumns它可以做到只更新指定字段db.UpdateableUser() .SetColumns(it it.Name 张三) .SetColumns(it it.Age 18) .SetColumns(it it.UpdateTime DateTime.Now) .Where(it it.Id 10086) .ExecuteCommand();这里的不是比较而是“赋值的意思”。it.Name 张三被解析成SET name 张三。第一次看到的人很容易懵这个语法是 SqlSugar 独有的习惯写多了反而觉得很顺因为它把“给哪个列赋值”写得很显式。我实际项目里最喜欢用这个 API 做“动态更新”。接口入参可能只有部分字段有值传统做法是写一大堆 if 去拼接 SQL用 SqlSugar 可以这样var upd db.UpdateableUser().Where(it it.Id request.Id); if (!string.IsNullOrEmpty(request.Name)) { upd upd.SetColumns(it it.Name request.Name); } if (request.Age.HasValue) { upd upd.SetColumns(it it.Age request.Age.Value); } var rows await upd.ExecuteCommandAsync();这样既避免了把 null 更新进数据库也避免了覆盖前端没有提交的字段。而且SetColumns方法支持链式调用像拼积木一样条件满足就多拼一块不满足就不拼非常灵活。3.2 UpdateColumns 与 IgnoreColumns局部列控制的速查对比除了SetColumnsSqlSugar 还提供了两个从实体更新出发的列控制方法。一个是UpdateColumns指定“只更新这些列”另一个是IgnoreColumns指定“除了这些列都要更新”。先说UpdateColumns它适合实体已经完整赋值但不想更新某些无关字段的场景var user new User { Id 1, Name 张三, Age 18, UpdateTime DateTime.Now, CreateTime DateTime.Now // 这个字段不该被更新 }; db.Updateable(user) .UpdateColumns(it new { it.Name, it.Age, it.UpdateTime }) .ExecuteCommand();生成的 SQL 只会包含name、age、update_time三个 SET 字段不管实体里CreateTime有没有值都不会碰它。再看IgnoreColumns它是反着来默认全字段更新但把指定的列排除在外db.Updateable(user) .IgnoreColumns(it new { it.CreateTime, it.RowVersion }) .ExecuteCommand();这在处理“某些列只能由数据库维护”的场景时非常好用比如自增主键、创建时间、行版本号等。我建议把这两个方法区分开来记UpdateColumns 是白名单IgnoreColumns 是黑名单。不要把两者混用否则容易造成理解混乱。另外如果用SetColumns已经明确指定了赋值列就不要再叠加UpdateColumns了那样会让代码逻辑变得难以维护。3.3 Where 与 WhereColumns更新条件到底怎么写Where是大家最熟悉的条件方法它生成的是整个更新的全局过滤条件和普通查询里的Where几乎一样db.UpdateableUser() .SetColumns(it it.Status -1) .Where(it it.Id 10086 it.OrganizationId 5) .ExecuteCommand();对应的 SQL 就是UPDATE user SET status -1 WHERE id 10086 AND organization_id 5;Where适合“所有行共用同一个条件”的批量更新比如把某个组织下的所有用户停用。而WhereColumns之前已经出现过它的作用相对特殊批量更新时用它指定“每一行要携带哪些列作为更新条件”。它的语义更像“把实体里这些列的值作为 SQL 参数放进 WHERE”。看一个例子var list new ListUser { new User { Id 1, Name 张三, RegionId 10 }, new User { Id 2, Name 李四, RegionId 20 } }; db.Updateable(list) .WhereColumns(it new { it.Id, it.RegionId }) .ExecuteCommand();这条会生成两条更新语句第一条是WHERE id 1 AND region_id 10第二条是WHERE id 2 AND region_id 20。Where和WhereColumns可以组合使用但绝大多数业务场景只需要其中一种。记住Where 是静态全局条件WhereColumns 是逐行动态条件。这个区别搞清楚了批量更新基本不会写错。4. 复杂更新场景与 SqlSugar 高阶玩法4.1 用 SQL 表达式做自增、减库存、字符串拼接业务里经常遇到“浏览量加 1”“库存减 1”这样的需求。新手容易写出这种代码先查出来再内存里加一再更新回去。这个写法在并发下一定会丢更新因为中间有“查出来”和“写回去”的间隙。正确做法是让数据库自己在 SQL 里做加减。SqlSugar 的SetColumns支持表达式右侧引用数据库字段本身db.UpdateableProduct() .SetColumns(it it.Stock it.Stock - 1) .Where(it it.Sku A10086 it.Stock 1) .ExecuteCommand();生成的 SQL 类似UPDATE product SET stock stock - 1 WHERE sku A10086 AND stock 1;这么做有两个好处第一整个过程在数据库内部完成不存在查询再回写的并发窗口第二Where 里加stock 1天然防止了库存扣成负数比在业务代码里判断可靠得多。同理字符串拼接、时间追加都可以用表达式db.UpdateableUser() .SetColumns(it it.Name it.Name _新后缀) .SetColumns(it it.LoginCount it.LoginCount 1) .SetColumns(it it.UpdateTime DateTime.Now) .Where(it it.Id 1) .ExecuteCommand();SetColumns右侧如果出现it.某字段它会被翻译成字段引用而不是参数值。这一点非常重要因为它是“数据库内部计算”还是“外部参数覆盖”的分水岭。写之前想清楚右侧引用的是不是列名就不会出错了。4.2 更新时加 WhereExists避免关联空值覆盖有一个很容易被忽视的更新场景更新一个子表但子表数据关联的主表记录可能已经被删了或者关联值本身不在主表里。如果直接更新就会把一批“孤儿数据”的状态改掉等到后续关联查询时才发现数据对不上。SqlSugar 的子查询表达式可以解决这个问题。需求场景为更新所有帖子状态为“已删除”但只更新作者还存在的帖子。官方用法是借助SqlFunc.Subqueryabledb.UpdateablePost() .SetColumns(it it.Status -1) .Where(it SqlFunc.SubqueryableUser() .Where(u u.Id it.UserId) .Any()) .ExecuteCommand();Any()会被解析成EXISTS生成的 SQL 类似UPDATE post SET status -1 WHERE EXISTS (SELECT 1 FROM user u WHERE u.id post.user_id);这个写法把“过滤脏数据”下推到了数据库不用先查 id 列表再更新。我在做历史数据清洗时经常这么用因为历史数据里外键关系经常不完整这个条件能一次性筛掉大量脏数据避免把原本就无主的记录误更新。不同版本的 SqlSugar 对SqlFunc.Subqueryable的支持略有差异用之前建议先在本地跑一个最小示例看下生成的 SQL。4.3 无主键实体、复合主键与乐观锁处理无主键实体更新是前面反复强调的高危场景。一张业务表如果本身没有主键那么实体类里就不能定义IsPrimaryKey属性。此时直接执行var row new NoKeyTable { Name 测试, Code C001 }; db.Updateable(row).ExecuteCommand();很可能不会达到预期。所以无主键表更新时必须先定位条件var row new NoKeyTable { Code C001, Name 测试 }; db.Updateable(row) .WhereColumns(it new { it.Code }) .ExecuteCommand();这样生成的 WHERE 只有code C001而其他列作为SET赋值定位和更新逻辑非常清晰。复合主键表的处理跟单主键类似只要实体上把多个字段标记为主键默认更新就会带上全部主键条件。批量更新时可以显式指定多个条件列db.Updateable(list) .WhereColumns(it new { it.OrderId, it.SeqNo }) .ExecuteCommand();再来说乐观锁。SqlSugar 本身没有内置强一致的乐观锁拦截但完全可以自己用“版本号条件”实现。表里加一个Version字段更新时在Where里带上旧版本号var rows db.UpdateableUser() .SetColumns(it it.Name 新名字) .SetColumns(it it.Version it.Version 1) .Where(it it.Id request.Id it.Version request.Version) .ExecuteCommand(); if (rows 0) { // 说明版本号已经变化应该提示用户重新加载 }这里SetColumns负责把版本号自增Where负责校验版本号一致两条一起生成一条原子 SQL在并发场景下不会出现“两个人同时改同一行”的问题。这个模式我在订单、配置表等关键数据上用了很久稳。4.4 更新配合事务、缓存与分页批量更新操作经常要跟其他写操作放在同一个事务里。比如“改订单状态”和“扣库存”必须一起成功或一起失败。SqlSugar 的写法很直接var result db.Ado.UseTran(() { db.UpdateableOrder() .SetColumns(it it.Status 2) .Where(it it.OrderId 10001) .ExecuteCommand(); db.UpdateableProduct() .SetColumns(it it.Stock it.Stock - 1) .Where(it it.Sku A10086) .ExecuteCommand(); }); if (result.HasError) { // 事务回滚记录 result.ErrorMessage }UseTran会自动在传入委托执行发生异常时回滚不用自己手动管理Commit/Rollback代码结构也清晰。如果是异步方法可以用UseTranAsync。如果项目里开了二级缓存更新后记得清缓存否则老数据会被缓存住。SqlSugar 提供了RemoveDataCache方法db.UpdateableUser() .SetColumns(it it.Name 张三) .Where(it it.Id 1) .RemoveDataCache() .ExecuteCommand();最后说大数据量批量更新的分页控制。当List里有几千甚至上万条数据时一次性执行会有两个问题单次事务时间过长、数据库锁范围过大。SqlSugar 的PageSize可以把大集合拆成多个批次db.Updateable(userList) .WhereColumns(it it.Id) .PageSize(500) .ExecuteCommand();这个 API 会在内部按 500 条一批执行每一批有自己的事务边界整体性能比循环调用好得多。我在清理历史数据时用过一次 2 万条左右的批量更新配合PageSize(1000)几分钟跑完没有出现锁等待超时。5. Update 踩坑与排查实录5.1 影响行数为 0但数据其实没变更新接口返回rows 0第一反应“更新失败了”实际上不一定。比如用户把昵称从“张三”改成“张三”数据库会认为没有任何字段发生实际变化某些数据库驱动返回的影响行数就是 0。这在 MySQL 里非常容易碰到。排查方法很简单先看条件是否真的匹配。把 SqlSugar 生成的 SQL 打出来手动在数据库客户端执行一遍看看是否命中记录。打印 SQL 有两种方式一种是在代码里拿到更新语句SqlSugar 提供了类似ToSql()的方法var sql db.UpdateableUser() .SetColumns(it it.Name 张三) .Where(it it.Id 1) .ToSql(); Console.WriteLine(sql);另一种是开启 SqlSugar 的 SQL 日志输出。看到 SQL 之后再判断如果条件字段类型不匹配导致索引失效或者实体字段名跟列名对不上都可能导致更新命中不了。如果确认是“值没变化导致的 0 行”业务上不要把它当作失败去提示用户。可以把需求拆成“只要有提交就提示成功”或者用ExecuteCommand之前先判断值是否变了。5.2 更新全表的灾难WHERE 被吞了怎么办见过太多线上事故是“一条 update 语句忘了写 where”。ORM 虽然帮你生成了 SQL但它不会替你检查业务完整性。尤其是这种写法db.UpdateableUser() .SetColumns(it it.Status -1) .ExecuteCommand();没写WhereSqlSugar 不会报错它会老老实实生成UPDATE user SET status -1然后整个表的状态就没了。这也是为什么我在前面反复强调写 Update 之前先写条件列养成“条件优先”的习惯。有一个保险做法在开发环境配置 SqlSugar 的 SQL 执行监控把所有更新语句记录到日志里人工巡检或者定时检查。另一个更保险的做法是给所有核心更新语句用ToSql()先看一眼确认翻译出来的 SQL 带上了WHERE。不要嫌麻烦这个动作十秒钟能省掉的烂摊子可能是几小时甚至一整天。如果已经误更新了全表第一时间先备份并停止写入然后根据业务日志尝试恢复。老实讲预防的成本远低于事后处理ORM 本身不背这个锅写代码的人要心里有数。5.3 并发下的脏写版本号、条件值、数据库锁并发更新问题在运营后台尤其突出。两个管理员同时编辑同一个用户后保存的人会覆盖先保存的人。解决办法就是用乐观锁也就是我前面提到的“版本号 条件”。这里再给一个更通用的做法更新时在Where里带上旧值条件不一定要版本号字段。比如表单页提交时前端带了用户当前年龄更新语句要求数据库里的年龄必须还是这个值才更新var rows db.UpdateableUser() .SetColumns(it it.Name request.Name) .Where(it it.Id request.Id it.Age request.OldAge) .ExecuteCommand();如果rows 0说明这行数据在你打开表单之后已经被别人改过这时再决定是重试还是报冲突。这个方案的优点是表结构不用加字段缺点是每个条件都得预先赋值字段多的时候写起来繁琐。两者选一种即可。对于“读出来、改一下、写回去”的场景尽量改造成 SQL 表达式直接更新比如库存、次数这一类千万不要在应用层做“读改写”。如果一个字段经常被并发修改数据库表设计时就该考虑加版本号或者使用原子更新表达式。5.4 大批量更新性能调优经验最后集中说性能。最常见的性能问题不是Updateable本身慢而是写法没发挥出框架和数据库的优势。第一条铁律绝对不要循环里面调单条更新。哪怕只有十次循环每次都要重新连数据库、开事务浪费巨大。能用批量Updateable(list)就不用单条。第二条经验是PageSize配合合理的批大小。我试过 500、1000、2000 三档大多数情况下 500~1000 效果比较稳。批太大事务时间太长锁竞争激烈批太小又体现不出批量优势。不同数据库表现有差异SQLServer 和 MySQL 建议都实际压一下。第三条经验是如果要更新几万行且逻辑和现有表数据强相关别死磕 ORM。SqlSugar 的Ado对象允许直接执行原生 SQLvar rows db.Ado.ExecuteCommand( UPDATE user SET status status WHERE department_id deptId, new SqlSugar.SugarParameter(status, -1), new SqlSugar.SugarParameter(deptId, 100) );原生 SQL 配合临时表、join 更新往往比逐条 ORM 更新快一个数量级。框架不是万能的该用 SQL 的时候大胆用。更新操作还依赖索引。批量更新时如果Where后面的字段没索引数据库会全表扫描然后锁住大量行性能瞬间崩掉。我习惯在开发环境用EXPLAIN看一下更新语句的执行计划尤其是那种“按业务编号更新整张表”的脚本。最后再分享一个小习惯任何更新方法在交付前我都会用ToSql()或ToSqlString()看一眼生成的 SQL重点检查两件事——SET 里有没有混进不该更新的列WHERE 里条件是不是完整。这个习惯帮我躲过了至少三次潜在的线上事故。SqlSugar 的 Update 语法本身不难难的是每次动手前都保持“更新是有风险的”这根弦。把这根弦绷住了更新操作就能变成一件既简单又安全的事。
返回列表