SqlSugar 这些年在国内 .NET 圈子里几乎是“社区顶流”般的存在。我最早是类 Dapper 拼接 Sql 的忠实党,后来被组长强行安利了 SqlSugar,抱着怀疑的态度试了一周之后,直接改了项目里大部分数据访问代码。这个库真正打动我的地方,不是它宣称每秒几百万行的性能,而是它把“数据库操作”这件事简化到了接近业务思考的层级:你只需要写实体、写 Lambda 表达式,剩下的 SQL 生成、参数化、实体跟踪、主从库切换都由框架处理。今天这篇就好好拆一拆 SqlSugar 的常用类库设计、核心 API 用法以及我在真实项目里踩过的坑。
如果你正准备学 .NET 的 ORM,或者你在 EF Core 和 SqlSugar 之间反复横跳,又或者你已经被复杂 SQL 搞得头大,这篇文章非常适合你。我会从安装开始,一路讲到事务、多租户、SQL 拦截、性能调优,最后一节专门整理常见问题,基本是按照团队内新人培训的深度来写的。
1. 先弄清楚:SqlSugar 到底解决了什么问题
1.1 从程序员的数据库操作痛点讲起
大多数业务系统的数据访问代码,表面上是表结构操作,实际上充满了重复劳动:拼接参数、处理数据库差异、控制事务边界、处理主键生成、维护软删除条件。早期我们用 ADO.NET 写数据层,每一行代码都在和 Connection、Command、DataAdapter 打架。后来 Dapper 缓解了一半问题,另一半问题——复杂的实体映射、多表查询、数据迁移——依然要自己手写。
SqlSugar 解决的正是“开发效率和执行效率要同时在线”这个核心矛盾。它轻量到可以在一个小工具里直接引用,但又提供了你期望从大型 ORM 身上找到的几乎所有能力:CodeFirst 建表、差异更新、Lambda 查询、分页、多租户、读写分离、生成实体、全局过滤器和 AOP 拦截。一句话总结:它把数据库访问的“标准动作”全都自动化了,把“非常规动作”又以原生 SQL 的方式留了口子,爱情和面包都要有。
1.2 与 EF Core、Dapper 的定位差异
很多人习惯把 SqlSugar 和 EF Core 放在天平两端。EF Core 是重量级完整解决方案,模型设计、迁移、上下文追踪那是相当完善,但学习曲线比较陡,团队里如果新手多,很容易乱用懒加载导致 N+1 查询直接击穿数据库。SqlSugar 在概念上更简单,没有 ChangeTracker 那一套强追踪机制,写起来更接近 SQL 思维,性能上也因为少了追踪开销而更有优势。
那么和 Dapper 相比呢?Dapper 更像一把手术刀,快到你怀疑人生,但你得自己缝线、自己止血。写 SQL、映射、多表拼接、事务包裹都得手工来。SqlSugar 是那个把常用手术工具集成工具箱的产品,它底层可以基于 Dapper 风格的高性能 DataReader 实现映射,但对外却提供链式 Lambda API。我见过不少项目从 Dapper 迁移到 SqlSugar,迁移后的代码量直接降了一半多。
所以这本质上是三种策略:你追求极端可控选 Dapper,追求生态完整、模型复杂度极高选 EF Core,而你要在主流业务系统的“快速开发+性能兼顾+多种数据库兼容”之间找平衡,SqlSugar 是我实际体验下来最接地气的选择。
2. 十行代码跑通第一步:环境准备与首个 Demo
2.1 NuGet 安装与初始化
不管你的项目是 NET 6、NET 8 还是 .NET Framework 4.6.1,SqlSugar 都有对应的版本。最近的拆分让核心包变成了 SqlSugarCore,它支持 .NET Standard 2.1 和后续的 .NET 版本,所以跨平台完全没问题。在 NuGet 包管理器里搜 SqlSugarCore,选择官方作者发布的最新稳定版,而不是那些看起来一九年再没更新过的野包。
安装完成之后,第一步是创建一个唯一的数据库访问入口。推荐的做法是封装一个名为 DbContext 的静态类或单例注入类,别在到处 new SqlSugarClient,否则你会在连接管理和事务控制上吃大亏。
public class DbContext { public static SqlSugarClient GetInstance() { return new SqlSugarClient(new ConnectionConfig() { ConnectionString = "Server=localhost;Database=DemoDb;Uid=root;Pwd=123456;", DbType = DbType.MySql, IsAutoCloseConnection = true, InitKeyType = InitKeyType.Attribute }); } }这里两个参数要重点解释:IsAutoCloseConnection 设为 true 时,每次操作结束会自动关闭连接并归还连接池,避免你忘了 Dispose 导致连接耗尽;InitKeyType.Attribute 则表示主键信息从实体特性里去读取,而不是通过数据库结构反推,这让实体文件在团队迁移时可以把数据库依赖降到最低。
2.2 最简单的增删改查
假如我们有一张商户表,实体定义大概是这样:
[SugarTable("Business.Shop")] public class Shop { [SugarColumn(IsPrimaryKey = true, IsIdentity = false)] public long Id { get; set; } [SugarColumn(Length = 50)] public string ShopName { get; set; } [SugarColumn(IsNullable = true)] public string Owner { get; set; } public DateTime CreateTime { get; set; } }数据库操作可以直接写在一个方法里,非常直观:
var db = DbContext.GetInstance(); // 新增 var shop = new Shop { ShopName = "城东店", Owner = "老张", CreateTime = DateTime.Now }; var id = db.Insertable(shop).ExecuteReturnSnowflakeId(); // 查询 var one = db.Queryable<Shop>().Where(s => s.Id == id).First(); // 更新 one.ShopName = "城东新店"; db.Updateable(one).ExecuteCommand(); // 删除 db.Deleteable<Shop>().Where(s => s.Id == id).ExecuteCommand();注意,这里我用了 ExecuteReturnSnowflakeId,不是 ExecuteReturnIdentity。雪花 ID 是分布式架构的好朋友,它避免了自增主键在分库分表、数据迁移时的各种屁股问题。SqlSugar 内置了一套对象映射器,当 IsAutoCloseConnection 为 true 时,你甚至不用写 using,操作完数据库连接自己回家,非常省心。
2.3 认识 SqlSugarClient 的两个实例模式
SqlSugarClient 并不是轻量对象,每一个实例内部都包含连接配置、AOP 事件、全局过滤器、语言模板等一堆状态。官方文档区分了单例和 scoped 模式。
在 Web 应用里,我推荐把 SqlSugarClient 注册成作用域单例,每个请求一个实例,既不会跨请求共享状态造成并发混乱,也能避免反复创建实例带来的性能浪费。比如在 ASP.NET Core 里:
builder.Services.AddScoped<ISqlSugarClient>(x => { var config = new ConnectionConfig() { ConnectionString = builder.Configuration.GetConnectionString("Default"), DbType = DbType.SqlServer, IsAutoCloseConnection = true, MoreSettings = new ConnMoreSettings { IsWithNoLockQuery = true } }; return new SqlSugarClient(config); });这里有个细节我想多说一句:IsWithNoLockQuery 开启后,查询语句默认带上 WITH (NOLOCK),对高并发读多写少系统很有帮助,但前提是你清楚这可能会读到未提交的数据。不要在财务类强一致场景里乱开这个开关。
3. 高频功能的核心细节(重点中的重点)
3.1 实体映射与主键策略
SqlSugar 通过特性驱动实体到数据库的映射,SugarColumn 特性里参数很多,日常最常用的就这么几个:
- IsPrimaryKey: 标记主键字段
- IsIdentity: 标记数据库自增列
- ColumnDataType: 自定义数据库列类型,比如 varchar(100)、datetime2
- IsNullable: 是否允许为空
- Length: 配合 ColumnDataType 自动建表时使用,限制长度
- IsIgnore: 这个字段在数据库里不存在,比如临时计算属性
[SugarColumn(IsPrimaryKey = true, ColumnDataType = "bigint")] public long Id { get; set; } [SugarColumn(ColumnName = "shop_name", Length = 50)] public string ShopName { get; set; } [SugarColumn(IsIgnore = true)] public string TempRemark { get; set; }ColumnName 在统一使用下划线命名法的大团队里特别重要,你可以让 C# 属性保持标准驼峰命名,而数据库用 snake_case,在实体特性里做一次映射,干净利落。
主键选择上,我强烈建议新项目直接用雪花 ID。自增主键在几十万行数据时看起来没什么,一旦涉及数据迁移、按 ID 区间拆分归档、多库汇总,就会变成灾难。SqlSugar 生成雪花 ID 的方法在后面细说,你只需要知道它能保证在分布式环境下不重复且时间有序就行。
3.2 CodeFirst 建表:从零生成数据库结构
传统开发流程是先画数据库表,再反向往代码里写实体。SqlSugar 支持反过来:先写好实体类,启动时自动比较实体与数据库的差异,然后自动建表、加列。这叫 CodeFirst,对项目冷启动和快速迭代来说,真香。
用起来很简单:
db.CodeFirst.InitTables<Shop, Category, Order>();它会读取实体上所有特性,生成规范的表结构。如果你后续给 Shop 增加了一个字段,再次执行 InitTables 并不会重建表,而是自动执行 ALTER TABLE 添加列。这个差异比较算法对日常开发完全够用,但要注意一点:它不会自动帮你删列。你要删除字段,得手动去数据库或通过迁移脚本处理,否则容易丢数据。
还有个经验值:如果你用 SqlSugar 做正式产品多环境发布,别把 InitTables 放在生产环境每次启动都执行。我一般只在开发环境或者集成测试环境自动建表,生产环境还是走严格的发布脚本,更稳妥。
3.3 常用查询语法:Lambda、条件组与语法糖
查询是 SqlSugar 用得最频繁的部分,我把常用套路贴出来给新手抄:
// 等值、模糊、区间 var list = db.Queryable<Shop>() .Where(s => s.Owner == "老张") .Where(s => s.ShopName.Contains("城东")) .Where(s => s.CreateTime > DateTime.Now.AddDays(-30)) .OrderBy(s => s.CreateTime, OrderByType.Desc) .ToPageList(1, 10, ref total, ref totalPage);Contains 转成 SQL 是 LIKE '%城东%',StartWith 和 EndWith 也有专门语法,但要注意 SqlSugar 在部分数据库(比如 MySQL)上对大小写敏感度差异,建议在字段排序规则层面提前统一。
动态条件组合是业务系统绕不开的需求。前端传一堆筛选条件,你不能一个个拼 if。SqlSugar 提供的方法很优雅:
var exp = Expressionable.Create<Shop>() .AndIF(!string.IsNullOrEmpty(name), s => s.ShopName.Contains(name)) .AndIF(status.HasValue, s => s.Status == status.Value) .ToExpression(); var list = db.Queryable<Shop>().Where(exp).ToList();AndIF 内部做了表达式树的拼接,比手动构造字符串安全一百倍。至少我们就遇到过同事把前端值直接拼进 Where 字符串引发 SQL 注入的事故,所以尽量用 Lambda 方式,永远不要直接传原始字符串条件。
3.4 仓储模式:避免客户端到处 new 引擎
SqlSugar 的流畅 API 很容易让人形成到处调用 db 的习惯,但项目复杂到一定程度,数据访问逻辑应该收敛到仓储层。本身 SqlSugar 库里就提供了 ISugarRepository 作为泛型基础仓储,你也可以根据自己的业务抽象一层。
我项目里的做法是定义一个基础仓储接口,把通用方法再包一层:
public interface IShopRepo { Task<Shop> GetByIdAsync(long id); Task<PageModel<Shop>> Page(int pageIndex, int pageSize, string keyword); }底层实现内注入一个 ISqlSugarClient,然后执行增删改查。业务层直接依赖仓储接口,永远不直接感知 SqlSugarClient 的存在。这样如果哪一天你决定整个分布式事务方案或者换成别的 ORM,业务层代码不需要跟着地震。
注意,大部分时候不需要过度抽象。小项目直接用 SqlSugar 的扩展方法就完事了,超过十张表且业务规则复杂时再上仓储模式。过早抽象会让简单查询变成套娃现场,反而毁了 SqlSugar 本来“短平快”的体验。
4. 事务、多库、多租户等高级能力的正确打开方式
4.1 SqlSugar 的事务与工作单元
数据库操作很少只有一条 SQL 就收工的。经典场景是创建订单,需要扣库存、写支付流水、更新用户积分,任何一步失败都应该回滚。SqlSugar 的事务使用方式非常直观:
var result = db.Ado.UseTran(() => { db.Insertable(order).ExecuteCommand(); db.Updateable(stock).Where(s => s.Id == stock.Id).ExecuteCommand(); // 其他业务 }); if (result.IsSuccess) { // 提交成功 } else { var error = result.ErrorMessage; // 记录日志 }内部它是基于事务连接上下文实现的。嵌套事务时要注意 UseTran 并非真的 SQL Server 嵌套事务,它更像事务计数器,内层异常会标记到外层事务上,最终整体回滚。我建议在方法层级统一使用一个事务入口,不要在服务层内部到处开事务,那样会降低并发性能,还会让undo日志复杂。
更推荐的方案是把工作单元封装在请求级别:
public class UnitOfWork { private readonly ISqlSugarClient _db; public UnitOfWork(ISqlSugarClient db) { _db = db; } public async Task ExecuteAsync(Func<Task> action) { var result = await _db.Ado.UseTranAsync(async () => { await action(); }); if (!result.IsSuccess) throw new Exception(result.ErrorMessage); } }业务服务里直接注入工作单元,把多个仓储调用包在一个事务里,干净又利索。
4.2 多库切换与跨库查询
典型的分库场景是主库存订单,从库存日志,或者按客户片区拆库。SqlSugar 对多库支持做得挺完善,你可以用同一个实体模型访问多个数据库。
一种做法是动态切换连接字符串:
db.ChangeDatabase(DbType.SqlServer, "Server=other;Database=OtherDb;..."); var team = db.Queryable<Team>().First();另一种做法是在一个配置里配置多个连接:
var db = new SqlSugarClient(new ConnectionConfig() { ConnectionString = "主库连接", DbType = DbType.MySql, ConfigureExternalServices = new ConfigureExternalServices() { // 配置 slave 连接 } });我用的最多的是按业务线创建多个 DbClient 单例,然后通过接口注入,而不是在一个 Client 中间反复切换数据库。原因很简单,切换时如果刚好有事务未提交,很容易搞乱上下文。
跨库查询这种操作,SqlSugar 并不直接支持跨数据库实例的 JOIN,但它支持跨表同库查询,或者你去查询端把数据拉回来内存再合并。真遇到要跨库连表查询的场景,大部分人用的是数据库层的链接服务器或同步数据仓库,ORM 不是万能的。
4.3 全局过滤器搞定软删除与多租户
业务开发中,软删除几乎是标配。每条数据带一个 IsDeleted 标记,所有查询默认只查未删除的数据。如果不做封装,每个查询里都要加 .Where(x => !x.IsDeleted),特别容易漏。SqlSugar 的全局过滤器直接把这个逻辑固化到了 DbClient 层:
db.QueryFilter.AddTableFilter<IDeleteFilter>(it => it.IsDeleted == false);这行代码一旦执行,之后所有查询都会自动追加 AND IsDeleted = 0。前提是实体实现了 IDeleteFilter 接口。这个机制内部看是表达式树的动态合并,对开发者几乎是透明的。
多租户也能用类似的思路。假设系统里有多家公司,每张业务表都有 TenantId,你只需要配置一个全局过滤:
db.QueryFilter.AddTableFilter<ITenantFilter>(it => it.TenantId == _currentTenantContext.TenantId);这样业务层的代码完全不用感知租户 ID,写起来跟单租户应用没什么区别,极大降低泄露数据给其他租户的风险。多租户数据隔离说白了就是链路层完成过滤,省心。
4.4 一个模拟项目的后端架构实战
我们把上面这些能力串起来,搭一个模拟业务系统的数据访问层。领域是“进销存系统”,后端 .NET 8 Web API,数据库 MySQL。
项目结构大概是:
Solution/ ├─ Demo.Api/ # WebAPI 层 ├─ Demo.Domain/ # 实体类、接口 ├─ Demo.Application/ # 业务服务 ├─ Demo.Infrastructure/ # 仓储实现、DbContext实体定义示例:
[SugarTable("wms_product")] public class Product { [SugarColumn(IsPrimaryKey = true)] public long Id { get; set; } [SugarColumn(Length = 100)] public string ProductName { get; set; } public decimal Price { get; set; } public int Stock { get; set; } public bool IsDeleted { get; set; } public long TenantId { get; set; } }初始化配置:
var db = new SqlSugarClient(new ConnectionConfig { ConnectionString = "...", DbType = DbType.MySql, IsAutoCloseConnection = true, InitKeyType = InitKeyType.Attribute }); // CodeFirst 建表 db.CodeFirst.InitTables<Product, Order, OrderItem>(); // 全局软删除 + 多租户过滤器 db.QueryFilter.AddTableFilter<IDeleteFilter>(it => it.IsDeleted == false); db.QueryFilter.AddTableFilter<ITenantFilter>(it => it.TenantId == 1001);业务服务示例:
public class OrderService { private readonly ISqlSugarClient _db; public OrderService(ISqlSugarClient db) => _db = db; public async Task<long> CreateOrderAsync(OrderDto dto) { var id = 0L; var result = await _db.Ado.UseTranAsync(async () => { // 扣库存 await _db.Updateable<Product>() .SetColumns(p => p.Stock == p.Stock - dto.Quantity) .Where(p => p.Id == dto.ProductId) .ExecuteCommandAsync(); // 创建订单 var order = new Order { ProductId = dto.ProductId, Quantity = dto.Quantity, CreateTime = DateTime.Now }; id = await _db.Insertable(order).ExecuteReturnSnowflakeIdAsync(); }); if (!result.IsSuccess) throw new Exception(result.ErrorMessage); return id; } }这套架构下,业务层关注点就是“怎么处理业务”,数据库访问统一由基础设施层承载,换数据库、切分库、上线多租户都相对无痛。我们在团队里把这种模式叫作“半个 DDD”:既没有过度设计领域事件和聚合根,也避免了把数据访问细节撒得到处都是。
5. 性能调优:我用着顺手但容易被忽略的细节
5.1 高效分页、导航和查询优化
分页这块 SqlSugar 提供了几个重载。我推荐直接用 ToPageListAsync:
var page = await db.Queryable<Order>() .Where(o => o.Status == 1) .OrderBy(o => o.CreateTime, OrderByType.Desc) .ToPageListAsync(pageIndex, pageSize, ref total, ref totalPage);它生成的分页 SQL 在 MySQL 下是 LIMIT @offset, @size,在 SQL Server 下是 OFFSET/FETCH,优化程度不错。注意别在 OrderBy 里写复杂函数,比如 ORDER BY CONVERT(varchar, CreateTime) 这种,会导致索引失效。
如果你要联表查询,SqlSugar 提供三种方式:导航属性、Mapper 映射和 Join 查询。性能排序是:单表查询批量映射 > Join 查询 > Include 导航属性。我先说结论:能拆成多次简单查询的,不要迷信一个大 JOIN。
比如查询订单列表,顺带要带出用户昵称和商品名。你可以一次查完订单,再根据订单里的 UserId 集合查用户表,然后用字典在内存里组装。这种方法本质上就是把这个 ORM 的单表性能和对象映射发挥到极致。SqlSugar 提供了 Mapper 属性专门干这个:
var orders = await db.Queryable<Order>() .Mapper(o => o.UserName, o => o.UserId, (o, name) => o.UserName = name) .ToListAsync();这只是演示,实际开发我更建议直接用一对多映射接口。核心思想不变:避免在数据库层做不必要的笛卡尔积膨胀。几万行的 Join 查询,拆成两次索引查询加内存拼接,快一个数量级不夸张。
5.2 AOP 拦截 SQL,慢 SQL 排查
SqlSugar 有一个非常棒的特性:AOP 事件。你可以在每个操作前后挂上日志、性能统计、SQL 篡改等钩子。我在生产系统里长期开启这个功能,用来做慢 SQL 分析和安全审计。
db.Aop.OnLogExecuting = (sql, pars) => { Console.WriteLine($"SQL: {sql}"); Console.WriteLine($"参数: {string.Join(", ", pars.Select(p => $"{p.ParameterName}={p.Value}"))}"); // 可以写入日志文件或 ES };严重推荐在生产环境也打开 OnLogExecuting,但要注意别把敏感数据打到日志里。更温和的方案是只在阈值超时后记录。你可以在 OnLogExecuted 里拿到耗时:
db.Aop.OnLogExecuted = (sql, pars) => { // 这里怎么拿耗时?官方提供了 ViaServerTime 或者你自己包裹 Stopwatch };我自己的偏门技巧:结合 Stopwatch 和 OnLogExecuting 构造一个全局慢查询记录器,耗时超过 500ms 的操作就写入告警表。上线后的第一个月,我靠这个就抓到了三四个索引缺失场景,效果拔群。
AOP 还有一个用途是统一加密字段。比如手机号的读取自动解密、写入自动加密,这一套在 OnExecuting 回调里做值的变换,完全不用改业务代码。做医美、金融、电商系统对敏感字段合规要求高的,建议研究下这个玩法。
5.3 连接管理、异步和一键 CRUD 预热
SqlSugar 的异步方法并不像某些框架只是包了一层假异步,它的命令行执行是异步真跑,IO 完成的回调再继续。所以能用 async 就用 async:
await db.Queryable<Order>().ToListAsync(); await db.Insertable(order).ExecuteCommandAsync();但要注意,并不是所有方法都异步,比如 CodeFirst 建表大多数是同步的,在启动配置里没必要包装异步,反正是一次性操作,影响不大。
连接池这块依赖你所用的数据库驱动,MySql 的连接池默认开启。记住几个参数:Max Pool Size、Min Pool Size、Connection Lifetime 都要根据业务压测结果去调整。SqlSugar 的 IsAutoCloseConnection 保证了每个操作结束后连接立即归还池子,这比长期持有一个 Connection 更符合 Web 应用的高并发需求。
还有一个我后来才发现的细节:SqlSugar 有 DbMaintenance 类,可以一键判断表是否存在、清空表、重新建表之类的维护操作。尤其在 CI/CD 环境做一键初始化和数据库快照恢复非常方便。
bool exists = db.DbMaintenance.IsAnyTable("Order", false); if (!exists) { db.CodeFirst.InitTables<Order>(); }6. 常见问题排查与避坑实录
6.1 那些年我们一起踩过的映射坑
团队里新人经常遇到的一个问题是:实体定义的是 decimal 类型,插到数据库变成字符串?大概率是因为你用了 SQLite 或者数据库列类型不匹配。SqlSugar 默认映射有一套对应关系表,MySql 的 decimal 映射、SqlServer 的 Money 映射各有不同,当你不显式指定 ColumnDataType 时,可能建出来的表列不是你想要的那个类型。
我记得有一次在 MySQL 5.7 下建表,所有 decimal 都变成了 decimal(18,2),精度一旦不够,金额就直接四舍五入成整数了,查了很久才发现。解决方法是显式指定:
[SugarColumn(ColumnDataType = "decimal(18,4)")] public decimal Price { get; set; }另外大小写不敏感问题很坑。Windows 下 SqlServer 不区分表名大小写,但 MySQL 在 Linux 下默认区分大小写。实体类 Shop 对应的表名被拼成 shop,然后报表不存在。我建议在实体特性里全用统一的表名格式,规范化大团队直接用全小写下划线。
6.2 ORM 混用:什么时候需要原生 SQL
无论 ORM 多强大,总有它搞不定的场景,比如非常复杂的报表统计存储过程、临时表之间的操作、特定数据库的 JSON 查询扩展。SqlSugar 保证了 Ado 对象提供原生能力,你依然可以简单粗暴:
var dt = db.Ado.GetDataTable("select * from v_order_stats where create_date > @d", new SugarParameter("@d", DateTime.Now.AddDays(-1)));这跟直接使用 ADO.NET 没什么两样,但它跟 SqlSugar 的上下文事务是共享的,天然保持在一个事务里。对复杂查询,我的原则是:能用 SqlSugar 优雅表达的,绝不用原生 SQL;原生 SQL 写在清晰的位置并且包一层 Service 接口,禁止散落在 Controller 里。
混用 Dapper 也是可行的,SqlSugar 提供了 SqlQueryable 这种类 Dapper 的用户接口,但意义不大。真要在同一个项目中用一个执行层,我建议把 Dapper 封装进仓储实现里,对外暴露的还是业务接口,底层是谁根本不重要。
6.3 快速排查手册
我把实际项目里出现频率最高的五个问题整理成一个表格,方便你对照处理。
| 现象 | 原因 | 解决方式 |
|---|---|---|
| 查询出来字段全部为 null | 实体 ColumnName 和数据库列名不一致 | 加 SugarColumn(ColumnName) 或开启全局下划线映射 |
| 插入返回主键 0 | 主键没有标记 IsPrimaryKey 或者用了自增列但 IsIdentity 没标 | 给实体加上正确的特性,用 ExecuteReturnSnowflakeId |
| 一直报超时 Timeout | 数据库连接池耗尽或某条 SQL 阻塞 | 检查 IsAutoCloseConnection,查看 AOP 慢 SQL 记录 |
| 分页查询总条数与实际不符 | 使用了 Contains 之类的复杂筛选条件导致语句变化 | 用 SqlSugar 的 ToPageList 内置统计,不要自己封装 |
| 多表 Join 查询字段重复 | 写位置遗漏了对列名加别名 | 使用 SqlSugar 的 table 别名 on 语法,避免 select * |
关于第一点,我再补充一个技巧:如果你团队所有表都是下划线命名,不用每个类都写 ColumnName 映射。可以使用全局配置:
ConfigureExternalServices = new ConfigureExternalServices { EntityService = (type, entityInfo) => { /* 统一处理 */ } }在 EntityService 里解析实体的属性名并自动映射为下划线风格,一劳永逸。代码可以搜一下官方文档里的 MappingColumn 全局配置,照着抄就行。
6.4 实战中的几个独家心得
SqlSugar 的单库性能上限其实很高,但很多人的用法限制了它。我自己的经验是,凡是能在查询前减少数据量的条件,比如时间范围、状态过滤,永远放在最前面。不管你用的是 EF、Dapper 还是原生 SQL,数据库优化第一步永远是缩小结果集。
其次,批量插入用 Insertable(list).ExecuteCommand() 默认还会自动调用 PreparedStatement,同样可以走事务。加一个 .UseParameter() 确保所有值参数化,一则预防注入,二则提高执行计划重用率。
并发控制我一般用版本号字段,在实体上加一个 Version 列的更新语句:
var success = await db.Updateable<Order>() .SetColumns(o => new Order { Status = 2 }) .Where(o => o.Id == id && o.Version == version) .ExecuteCommandAsync(); if (success == 0) { // 并发冲突处理,提示用户重新刷新 }配合 SqlSugar 的乐观锁,不用数据库悲观锁也能解决大部分超卖、重复提交问题。
还有一个容易被忽略的性能杀手是 N+1 查询。SqlSugar 的 Mapper 可以避免它。例如订单列表要显示商品信息,如果你在循环里调用 db.Queryable () 每个订单一次,就是经典的 N+1。正确姿势是查完订单后收集所有商品 ID,再查商品列表,内存 Dictionary 关联。SqlSugar 提供这种能力用起来毫不费力,但前提是你在写代码时就意识到这个问题。
7. 最后分享一个我认为最实用的技巧
也许你已经注意到,全文到处提到 AOP、过滤器、雪花 ID、事务工作单元,但真正让 SqlSugar 与团队开发生态对齐的,其实就是它的“统一入口配置”。不管你是静态类、单例还是 scoped 注入,SqlSugarClient 的初始化配置是根,过滤器、AOP、全局映射都在这一层完成。我们内部有一条铁律:数据库 Client 的创建只能出现在依赖注入容器里,业务代码只能通过仓储接口访问数据,这样出了问题,只需要查一个配置文件和仓储实现就能定位。
文章写到这里,也不算一个完整的项目总结吧,顶多算一次代码级的拆解。SqlSugar 能做到的远不止这些,比如它还有读写分离、数据归档、租户与多数据库自由切换、JsonToModel 自动转换、类型处理扩展等,有兴趣的完全可以照着官方文档继续挖。但不管你要用多少高级功能,先把上面这些基础和实战细节走一遍,再谈优化才靠谱。
最后再分享一个小技巧:如果你已经有一个老项目是用原生 Ado 或者 Dapper 写的,想迁移到 SqlSugar,不要一次性全部推翻。你可以先建一个独立数据访问层,把最复杂、最容易出错的几个查询先切过去,跑一段时间验证没问题后再逐步切开。我用这个思路迁移过两个上线中的系统,零事故、可回滚、团队压力也小。迁移不是技术赌注,而是越早开始的渐进式重构越安全。