☰
SqlSugar实战:从ORM基础到性能优化与架构设计
2026/10/15 7:26:44 网站建设 项目流程

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,不要一次性全部推翻。你可以先建一个独立数据访问层,把最复杂、最容易出错的几个查询先切过去,跑一段时间验证没问题后再逐步切开。我用这个思路迁移过两个上线中的系统,零事故、可回滚、团队压力也小。迁移不是技术赌注,而是越早开始的渐进式重构越安全。

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

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

立即咨询