☰
SqlSugar更新语法全解:从Update到Updateable的正确打开方式
2026/10/3 3:51:57 网站建设 项目流程

平时用SqlSugar做增删改查,我发现最容易让人犹豫不决的就是更新操作。新增就是一个Insertable,查询就是Queryable,都挺直白,一到了更新,官方文档里既能看到db.Update(entity),又有db.Updateable<T>().SetColumns(...).Where(...).ExecuteCommand(),写法五花八门,新手经常不知道该用哪套。

这篇是这个 SqlSugar 系列里的 update 更新数据语法合集,我会按使用频率把条件更新、批量更新、列级控制、异步更新、并发处理这些场景全部过一遍,每个写法都会配合实际项目里的意图来解释,最后再讲几个线上才容易踩到的坑。你可以把它当成一份带注释的语法手册,用到哪种更新场景直接来查。

1. 先做一道选择题:db.Update与db.Updateable到底该用哪个

1.1 db.Update的适用边界

SqlSugar 提供一个简洁入口db.Update(entity),它确实能完成最基本的更新:

public class Student { [SugarColumn(IsPrimaryKey = true, IsIdentity = true)] public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } public string ClassName { get; set; } public DateTime CreateTime { get; set; } public int Status { get; set; } } // 最简单的更新:按主键更新全部字段 var stu = new Student { Id = 1, Name = "李雷", Age = 19, ClassName = "三班", CreateTime = DateTime.Now, Status = 1 }; var count = db.Update(stu);

这段代码对应到数据库,大概就是一条UPDATE student SET Name='李雷', Age=19, ClassName='三班', ... WHERE Id=1。注意,db.Update传的是整个实体,意味着它会把实体上你写进去的每个属性都生成到 SET 子句里。

那它是怎么知道主键的?靠的是实体上的IsPrimaryKey=true这个标记。如果没有主键标记,db.Update会直接抛出异常,因为它不知道按哪个维度去定位数据。

1.2 Updateable链式写法的基本骨架

当你需要条件更新、只更新部分字段、批量更新、或者更新时做计算,db.Update(entity)这个糖就不够用了。此时应该使用Updateable<T>这个入口:

var count = db.Updateable<Student>() .SetColumns(it => new Student { ClassName = "一班" }) .Where(it => it.Id == 1) .ExecuteCommand();

这串链式调用的语义非常清晰:SetColumns指定要更新的字段和值,Where指定更新范围,ExecuteCommand真正执行并返回受影响行数。

对应 SQL:

UPDATE student SET ClassName = '一班' WHERE Id = 1

我这里多说一句为什么要区分两个入口。SqlSugar 内部把db.Update也实现成了Updateable的封装,所以db.Update(entity)本质上就是:

db.Updateable(entity).ExecuteCommand();

那为什么不直接统一用Updateable?因为简单场景用db.Update写起来少敲几个字,而复杂场景必须用链式能力把细节展开。所以我的习惯是:

  • 确认按主键更新、且整个实体字段都要覆盖,用db.Update(entity)。
  • 只要更新部分字段、按业务条件更新、批量更新、或者要做异步,一律Updateable。

这个选择做完,后面的事就顺了。

2. 覆盖90%场景的单表条件更新:SetColumns、Where和受影响行数

2.1 标准写法与直接SQL的对应

条件更新是实际项目里最常用的更新方式。举例,运营后台做“全班学生状态置为禁用”,传统 SQL 是:

UPDATE student SET Status = 0 WHERE ClassName = '三班'

用 SqlSugar 写就是:

var rows = db.Updateable<Student>() .SetColumns(it => new Student { Status = 0 }) .Where(it => it.ClassName == "三班") .ExecuteCommand();

这里有个关键理解:SetColumns里new Student { Status = 0 }并不是说框架要拿这个实体去匹配主键,它只是告诉你“我想把 Status 列设置成 0”。其他没写在SetColumns里的字段,这次更新一概不动。这个设计非常贴近实际需求,因为我们做条件更新时,往往只关心某一两个业务字段,其他列不该被碰。

再看一个更贴近业务的自增表达:把所有学生的年龄加 1 岁,也就是开学统一“长大一岁”:

var rows = db.Updateable<Student>() .SetColumns(it => new Student { Age = it.Age + 1 }) .ExecuteCommand();

这个写法会生成:

UPDATE student SET Age = Age + 1

重点在于it.Age + 1会被解析成数据库表达式,而不是在 C# 内存里先算一个固定值。这个特性在你做计数器、库存加减、发布时间刷新时非常实用。

2.2 Where条件下的组合技巧

Where是条件更新和“整表更新事故”之间的最后一道防线。我见过不少人写更新时漏了Where,直接导致全表被改,所以它的谨慎程度再怎么强调都不过分。

Where可以写多个表达式,框架会合并为AND:

var rows = db.Updateable<Student>() .SetColumns(it => new Student { Status = 1 }) .Where(it => it.ClassName == "三班") .Where(it => it.Age > 10) .ExecuteCommand();

等价于:

UPDATE student SET Status = 1 WHERE ClassName = '三班' AND Age > 10

除了用 lambda 表达条件,它也能接收原始 SQL 条件,这在做动态筛选时很顺手:

var rows = db.Updateable<Student>() .SetColumns(it => new Student { Status = 1 }) .Where("ClassName = @className AND Age > @minAge", new { className = "三班", minAge = 10 }) .ExecuteCommand();

这里我用的是带参数的写法,而不是拼字符串,可以有效避免注入问题。此外,ExecuteCommand()返回的rows就是数据库实际影响的行数,这个数字在业务上有很重要的用途。比如“只允许更新自己名下的数据”,你可以判断rows == 0就知道没有满足条件的记录,然后给出“数据已被删除或无权操作”的提示,而不是抛出异常让用户看到一堆看不懂的错误。

3. 列级控制才是更新的精髓:UpdateColumns、IgnoreColumns与实体忽略

3.1 指定列更新的常见姿势

实际开发里,更新的列往往是有讲究的。举例,用户改自己的昵称,不应该连CreateTime、LastLoginTime这类内部字段一起更新。但你如果用实体列表批量更新,一不小心就会把整个实体的字段全写进 SET。

解决办法之一是使用UpdateColumns明确指定要更新的列:

var stu = new Student { Id = 1, Name = "韩梅梅", Age = 18, CreateTime = DateTime.Now, Status = 1 }; var rows = db.Updateable(stu) .UpdateColumns(it => new { it.Name, it.Age }) .ExecuteCommand();

这时生成的 SQL 只包含Name和Age两列:

UPDATE student SET Name = '韩梅梅', Age = 18 WHERE Id = 1

即使stu对象的CreateTime和Status有值,也不会被更新。同样的语法也适用于按条件更新:

var rows = db.Updateable<Student>() .SetColumns(it => new Student { Name = "班长", Age = 20 }) .Where(it => it.ClassName == "三班") .ExecuteCommand();

如果你想反过来,只忽略少数几个列、其他列正常更新,那就用IgnoreColumns:

var rows = db.Updateable(stu) .IgnoreColumns(it => new { it.CreateTime, it.Status }) .ExecuteCommand();

它的意思是“别的字段我都要更新,但这两列你们别碰”。

3.2 为什么我不默认整行更新

很多初学者会踩一个经典坑:从数据库查出来一个实体,改了某个字段,然后直接db.Update(entity),结果发现整行数据都被“洗”了一遍,尤其是那些使用默认值或者为null的字段。

原因很简单,db.Update(entity)默认就是更新整个实体的所有字段。假设实体里Status原本是0,你查出来改成1,但Age在业务代码里某条路径没赋值,等于0,更新完数据库里 Age 就成0了。这种问题在排错时非常隐蔽,因为单看代码你很难发现哪里把 Age 重置了。

所以我在项目里形成了一条约定:只要不是“整个行整体被后端接口完整回写”,一律使用列级控制。列级控制不是多此一举,它是在防御脏写。如果你使用SqlSugar的实体特性,还可以在实体类上做永久忽略:

public class Student { [SugarColumn(IsOnlyIgnoreUpdate = true)] public DateTime CreateTime { get; set; } }

IsOnlyIgnoreUpdate = true的意思是:这个字段在新增时正常写入,但在更新时自动忽略。这类字段很适合放CreateTime这种“只允许创建时写一次”的列。用了特性之后,即使代码里不小心写了db.Update(entity),CreateTime也不会被覆盖。

3.3 关于 NULL 字段的行为

再补充一个容易让人困惑的点:Updateable(entity)更新整个实体时,如果某个属性值是null,生成的 SQL 里很可能会有字段 = null这半句。这跟 EF Core 的部分更新行为不完全一样,SqlSugar 默认不会因为你传了 null 就自动跳过这一列。

所以当你面对一个没有完全赋值的实体时,最安全的写法仍然是UpdateColumns或者IgnoreColumns,把 null 字段排除在外。我在代码 review 时看到db.Update(entity)这种写法都会停下来确认一下:这个实体到底是不是完整、可信任地从外部传入的?如果不是,就必须改成列级更新。

4. 批量更新不该只有一种写法:实体集合、列裁剪与分批

4.1 列表Updateable的基本法

批量更新最常见的就是传一个实体列表,让框架按主键逐行更新:

var list = new List<Student> { new Student { Id = 1, Age = 10, ClassName = "三班", Status = 1 }, new Student { Id = 2, Age = 12, ClassName = "四班", Status = 1 }, new Student { Id = 3, Age = 11, ClassName = "三班", Status = 1 } }; var rows = db.Updateable(list).ExecuteCommand();

这个写法会生成类似这样的执行逻辑:

UPDATE student SET Age = 10, ClassName = '三班', Status = 1 WHERE Id = 1 UPDATE student SET Age = 12, ClassName = '四班', Status = 1 WHERE Id = 2 UPDATE student SET Age = 11, ClassName = '三班', Status = 1 WHERE Id = 3

SqlSugar 一般会把这些语句放在一个事务里执行,保证要么全部成功,要么全部回滚。但与此同时,如果你不想更新某些字段,批量更新同样支持列裁剪:

var rows = db.Updateable(list) .UpdateColumns(it => new { it.Age, it.ClassName }) .ExecuteCommand();

这样Status和CreateTime就不会被写入。

4.2 大批量场景下的分批思路

批量更新虽然方便,但并非万能。当列表数量到几千甚至几万条时,一条 UPDATE 一条事务的开销也是实打实的。SqlSugar 内部会帮你处理事务,但数据库连接、网络往返依然存在。

我常用的做法是手动分批。比如每 500 条一批:

for (int i = 0; i < list.Count; i += 500) { var batch = list.GetRange(i, Math.Min(500, list.Count - i)); db.Updateable(batch) .UpdateColumns(it => new { it.Age, it.ClassName }) .ExecuteCommand(); }

分批的好处不只是让单次更新不至于过大,更重要的是出错时影响范围可控,也方便做进度记录。如果你有几十万行需要刷数据,我更建议直接在数据库里用临时表或者关联更新完成,ORM 做超大批量更新多少有些吃力,这不是 SqlSugar 的问题,这是所有 ORM 的通性。

4.3 特殊的“每行值不一样”更新

有些业务,列表里每行更新的是不同的字段逻辑,例如:

  • ID 为 1 的学生,要把年龄改成Age + 1。
  • ID 为 2 的学生,要把班级改成“五班”。

这种情况用统一的SetColumns做不到“一行一套规则”。如果非要用框架能力来做,可以用Storageable的按主键匹配更新:

var students = new List<Student> { new Student { Id = 1, Age = 10, Status = 0 }, new Student { Id = 2, Age = 12, Status = 0 } }; var storage = db.Storageable(students) .WhereColumns(it => it.Id) .ToStorage(); var updateRows = storage.AsUpdateable.ExecuteCommand();

这里WhereColumns(it => it.Id)的意思是使用Id列作为每行更新时的定位条件。Storageable会先对这批数据进行存在性判断,再执行更新,适合“半新增半更新”的复杂导入场景。如果你只是标准批量更新,用前面的Updateable(list)就够了,不需要为了炫技把复杂度拉高。

5. 表达式更新与数据库函数:让语法离数据库更近

5.1 自增、计算字段与局部变量

更新里经常要用到数据库侧的计算,比如阅读数自增:

var rows = db.Updateable<Article>() .SetColumns(it => new Article { ViewCount = it.ViewCount + 1 }) .Where(it => it.Id == 100) .ExecuteCommand();

这个写法的价值在于并发安全。如果你先SELECT ViewCount出来,在 C# 里加一,再 UPDATE 回去,两次请求之间很容易互相覆盖。直接让数据库执行ViewCount = ViewCount + 1,才是原子操作。

表达式里也可以使用局部变量:

var newClassName = "重点班"; var rows = db.Updateable<Student>() .SetColumns(it => new Student { ClassName = newClassName }) .Where(it => it.Id == 1) .ExecuteCommand();

SqlSugar 会把newClassName作为参数传入 SQL,而不是直接拼字符串,这一点非常安全。不过要注意,SetColumns的表达式里尽量不要放复杂的方法调用,例如it.Name.Trim()这种能否翻译成 SQL 取决于具体版本和数据库。如果你需要大写转换、截断字符串,建议先用SqlFunc封装好的方法,或者直接使用下面要说的原始 SQL。

5.2 子查询在更新条件中的使用

更新不是只能根据当前表字段做条件,它也可以配合子查询。比如我只想更新“存在有效班级记录”的学生状态:

var rows = db.Updateable<Student>() .SetColumns(it => new Student { Status = 1 }) .Where(it => SqlFunc.Subqueryable<Class>() .Where(c => c.Id == it.ClassId) .Any()) .ExecuteCommand();

生成到数据库侧大致是:

UPDATE student SET Status = 1 WHERE EXISTS ( SELECT 1 FROM class WHERE class.Id = student.ClassId )

这种写法的好处是:不用先把符合条件的 ID 集合查出来再Contains,也不需要先查内存再逐一更新,整个条件在数据库里一次性完成。数据量大时,性能差异会很明显。

5.3 手工SQL也不是什么让步

虽然 SqlSugar 提供了丰富的表达式,但有时候业务场景实在太绕,比如我要把一个字段拼上另一个字段、还要调数据库函数,这时候老老实实写 SQL 反而更清晰:

var rows = db.Ado.ExecuteCommand( "UPDATE student SET Name = UPPER(Name) WHERE Id = @id", new { id = 1 } );

Ado.ExecuteCommand是 SqlSugar 保留给“原生 SQL 执行”的后门。它不丢人,反而是懂取舍的表现。表达式能写清楚就用表达式,写不清楚就直说,别硬把一段复杂 SQL 塞进 lambda 里,那样后续维护的人会非常痛苦。

6. 乐观并发与异步:上线之后才会关注的两件事

6.1 用Version字段锁住并发更新

先说并发。很多业务场景里,两个人同时打开同一个编辑页,各自改完保存,后保存的人会覆盖先保存的人。这个问题用“先查后改”是防不住的,必须在更新语句里加版本条件。

常规做法是给表加一个Version字段,每次更新把它加 1,同时在WHERE里带上客户端拿到的旧版本号:

var updateRows = db.Updateable<Student>() .SetColumns(it => new Student { Name = "李雷", Version = it.Version + 1 }) .Where(it => it.Id == 1 && it.Version == oldVersion) .ExecuteCommand(); if (updateRows == 0) { // 版本没对上,说明数据已经被别人改过 throw new Exception("数据已被其他用户修改,请刷新后重试"); }

这个模式非常经典,核心原理就是乐观锁:先假设不会冲突,只在最后更新时用版本号校验。因为UPDATE ... WHERE Version = oldVersion的原子性由数据库保证,所以它能有效避免“丢失更新”。

如果你想把版本号自动维护,不想在每个更新里手动加,也可以在实体上配合SqlSugar的更新拦截能力统一处理,但我个人倾向于把版本控制写显式,因为显式逻辑更容易排查。

6.2 ExecuteCommandAsync与作用域管理

异步更新在 I/O 密集场景下很有价值,尤其是 Web API 接口里,一个异步更新可以让当前线程提前释放,提升服务器吞吐量。

var rows = await db.Updateable<Student>() .SetColumns(it => new Student { ClassName = "五班" }) .Where(it => it.Id == 10) .ExecuteCommandAsync();

ExecuteCommandAsync和ExecuteCommand的语法完全一样,只是返回Task<int>。批量更新同样有异步版本:

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

这里我要提醒一个作用域问题:如果你在 ASP.NET Core 里把SqlSugarClient注册成了单例,然后让多个请求并发访问同一个实例做异步更新,很容易出现连接状态混乱。SqlSugar 官方推荐使用SqlSugarScope作为单例 DB 入口,它内部按上下文做了隔离。如果你是简单项目,更稳妥的做法是每个请求一个SqlSugarClient实例,用完即释放。

SqlSugarScope的好处是它本身线程安全,内部会帮你处理并发时的连接分配,所以线上项目我基本都用它。我见过有人为了性能把SqlSugarClient注册成单例,结果并发一高就报“连接已打开”之类的错,定位了半天才发现是实例生命周期的问题。

7. 定位更新SQL异常与实战中反复遇到的坑

7.1 先看SQL,再猜框架:ToSql与Aop输出

更新出问题,第一反应不是改代码,而是看框架到底生成了什么 SQL。SqlSugar 里有个我很喜欢的方法ToSql(),它可以在不执行的情况下把 SQL 和参数输出出来:

var sqlBuilder = db.Updateable<Student>() .SetColumns(it => new Student { Status = 0 }) .Where(it => it.Id == 1); var sql = sqlBuilder.ToSql(); Console.WriteLine(sql);

这样你就能直接看到UPDATE student SET Status = 0 WHERE Id = 1到底长什么样。如果 SQL 里莫名其妙多了一个你没想更新的字段,或者 WHERE 条件少了一段,一眼就能发现。

如果你想知道整个应用都执行了哪些 SQL,可以用 Aop:

db.Aop.OnLogExecuting = (sql, pars) => { Console.WriteLine(sql); };

这套日志在开发和排错阶段几乎是必须开的。更新操作不像查询,你很难从返回结果里倒推它做了什么,SQL 日志就是事故现场的唯一目击者。

7.2 主键、NULL和大文本的坑

先说主键。Updateable(entity)和db.Update(entity)都必须知道哪里有主键。如果实体类没有标记主键列,更新时会报“请设置主键”一类的错误。有些表没有严格的主键,只有业务唯一键,这时候你得记得用[SugarColumn(IsPrimaryKey = true)]去指定定位列。

再说 NULL。在前面列级更新部分已经提过,Updateable(entity)全字段更新时,NULL 字段可能变成SET 字段 = NULL。我遇到过不止一次,一个接口只想改昵称,结果状态字段被置空,因为前端传参里没有带状态。解决方式就是显式UpdateColumns,不给“顺手覆盖”留机会。

还有一个大文本的坑:更新varchar(max)、text、ntext这类大字段时,一些老数据库驱动对参数长度比较敏感。如果你在同一批更新里混着大文本和普通字段,可能出现参数类型转换异常。这时候我会把大文本更新单独拎出来,或者直接用原生 SQL 控制参数类型。

7.3 一个线上更新的经验清单

最后分享一份我每次写更新代码都会遵循的清单,看起来简单,但每条都是踩过坑换来的:

  • 明确更新范围:Where里有没有可能匹配到多行?如果业务上只允许一行,要在条件里包含主键或唯一键。
  • 明确更新列:写的SetColumns或UpdateColumns是不是只有本次业务要改的字段?有没有CreateTime、CreateBy这类不该动的列。
  • 检查返回行数:ExecuteCommand返回 0 不代表异常,但代表没有数据被修改,要有对应处理。
  • 先看生成 SQL:上线前用ToSql()或 Aop 日志确认一次真实语句。
  • 大事务控制:批量更新数量大就分批,单批控制在合理范围,别一把梭。

这些检查点看起来有点教条,但在我参与的很多项目里,线上更新事故大概率就发生在上面某一项。比如漏了Where导致整表状态被改,比如没有UpdateColumns导致创建时间被覆盖成当前时间,这类问题一旦发生,恢复成本通常比写代码成本高得多。

SqlSugar 的更新能力很完整,从简单实体更新到复杂条件更新、批量更新、异步更新都有覆盖。关键是你要在动手之前想清楚:这次更新的定位条件是哪些列,需要更新的字段是哪些列,这个 SQL 是不是我预期的那一条。把这三件事想清楚,再复杂的业务也就是一行链式调用的事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询