☰
.NET Core数据访问层实战:深入解析SqlSugar ORM高效用法
2026/10/19 8:01:03 网站建设 项目流程

最近在忙一个 .NET Core 老项目的重构,数据访问层从手工 ADO.NET 迁到了 SqlSugar,整个过程比预期顺利很多,也让我彻底确定了一个观点:中小型系统里,SqlSugar 这种开箱即用的 ORM 比 EF Core 和 Dapper 都更省心。EF Core 功能全但太重,随便一个查询都要过一遍状态追踪;Dapper 够快但业务一复杂,SQL 和映射全得自己写,维护成本很高。SqlSugar 正好在两者之间,API 直白、性能不差、事务分页这些高频能力全都内置。这篇文章把 .NET Core 集成 SqlSugar 的最简实战拆开讲透,从接入配置到日常开发,代码可以直接抄,坑全是真实踩过的。适合刚接触这个 ORM 的 .NET 开发同学,也适合正在做技术选型的团队。

1. 选型思考:.NET Core 项目里为什么值得给 SqlSugar 一个机会

1.1 主流 ORM 的现状与 SqlSugar 的生态位

先聊实际的。你在 .NET Core 里做数据访问,绕不开三选一:EF Core、Dapper、或者 SqlSugar 这类第三方 ORM。EF Core 有微软官方维护,功能和生态都强,团队里如果有偏爱全托管式开发的人,选它没错;但 EF Core 的启动和迁移成本不低,尤其你只想快速改表结构、不想走 Migration 那套流程的时候,会感觉被框架绑住了。Dapper 则是另一个方向,SQL 全自己写,性能拉满,但实体赋值、批量插入、分页这些都要自己搭轮子,项目一复杂,重复代码多了也烦人。

SqlSugar 正好填了中间这块。第一,它的 API 设计更贴近 SQL 思维。你用 EF Core 写一个多表查询,心里要把 LINQ 翻译成 SQL;用 SqlSugar 写,方法名基本都是 Where、OrderBy、GroupBy、LeftJoin 这种单词,几行代码读下来,脑子里能直接想象出生成的 SQL 长什么样。第二,它把项目里常见的活儿直接内置了,不用自己写工具类。分页、事务、批量插入、缓存、AOP 日志,都给你留好了入口。第三,性能上它并不吃亏,内部做了表达式缓存,同一条查询表达式第二次执行时不用重新解析。我拿一个几百万行数据的订单表做过简单测试,简单条件查询和分页的场景下,SqlSugar 的延迟表现和 Dapper 差不了太多,但书写效率高一大截。

1.2 "开箱即用"具体指什么:你省下的样板代码

"开箱即用"这个词容易被当成营销话术,但在我看来它是可以量化的。一个清爽的数据访问层要解决四件事:连接管理、实体映射、增删改查模板、事务处理。用 EF Core,你得建 DbContext、注册 DbSet、配实体配置类、迁移还要走命令行;用 Dapper,除了写 SQL,你还得自己处理连接开关、参数化、结果映射。而 SqlSugar 的做法是:一个 ConnectionConfig 告诉它数据库连哪、哪种类型,一个 SqlSugarClient 实例搞定后面所有请求。实体映射靠约定加特性,约定覆盖大多数场景;连接自动开关,不需要担心忘记释放;事务用 db.Ado.UseTran 包一层,回滚和提交都是自动的。

我印象最深的是一个带三十多张表的小型后台管理系统,从零开始用 SqlSugar,第一天就把所有表的 CodeFirst 建表脚本跑通了。它不是没有学习曲线,但曲线很短:看完这篇文的配置部分,再去翻一下官方文档的 API 列表,就能开始写业务代码。所以这篇文章不堆概念,全部围绕真实开发场景,你照着敲基本能跑。

2. 接入姿势:从 NuGet 包到第一个数据库连接

2.1 安装 SqlSugarCore 与最小可运行代码

先建一个 .NET Core 项目,然后装包,用 CLI 一条命令就行:

dotnet add package SqlSugarCore

也可以用 Visual Studio 的 NuGet 管理器搜索 SqlSugarCore。注意别装成旧版的 SqlSugar,那个是给 .NET Framework 用的;.NET Core 项目认准名字里带 Core 的这个包。装完之后,先写一段最小的连接初始化代码来验证环境通不通,我用一个静态工厂方法做演示:

using SqlSugar; public static class DbFactory { public static SqlSugarClient CreateClient() { return new SqlSugarClient(new ConnectionConfig { ConnectionString = "Server=localhost;Port=3306;Database=blog;Uid=root;Pwd=123456;charset=utf8mb4;", DbType = DbType.MySql, IsAutoCloseConnection = true, InitKeyType = InitKeyType.Attribute }); } }

这里四个配置项一个都不能马虎。ConnectionString 不用解释;DbType 是数据库类型,写在枚举里,SqlServer、MySql、PostgreSQL、Sqlite、Oracle 都支持;IsAutoCloseConnection 设为 true 表示每次操作后自动关闭连接,这是 SqlSugar 连接池友好使用的关键,不然连接一直挂着;InitKeyType.Attribute 是告诉框架,主键、自增列这些信息以实体上的特性为准,而不是去数据库系统表里读。这两者在性能和自由度上有差别,后面讲到 CodeFirst 时会更清楚。

做个连接自检也很方便,十秒钟就能暴露环境问题:

using var db = DbFactory.CreateClient(); var ok = db.Ado.IsValidConnection(); Console.WriteLine(ok ? "数据库连接正常" : "连接失败");

IsValidConnection 会真实执行一次轻量查询,能很快暴露连接串错误、网络不通、账号权限这类环境故障。很多人一上来就写业务代码,结果卡在连不上数据库,不如先跑这段验证一下。我自己每次换数据库环境都会先执行它,省得后面定位半天。

2.2 结合 .NET Core 依赖注入的正确注册方式

直接用 new 创建实例适合控制台验证,但 Web 项目里还是要把 SqlSugarClient 交给容器统一管理:

builder.Services.AddSingleton<ISqlSugarClient>(sp => { var config = new ConnectionConfig { ConnectionString = builder.Configuration.GetConnectionString("Default"), DbType = DbType.SqlServer, IsAutoCloseConnection = true, InitKeyType = InitKeyType.Attribute }; return new SqlSugarClient(config); });

注册成单例还是作用域,是我被问过最多的问题之一。SqlSugar 官方文档采取的是单例使用方式,原因是它的连接是"用到才开、用完即关"的模式,内部的查询状态不会长期占着连接不放,而且框架对线程安全做了处理,绝大多数业务场景下单例没有问题。但如果你在项目里大量使用事务,或者在同一个请求里频繁开多个并发任务,我建议考虑 AddScoped,让每个请求拿到独立的 client,避免事务状态串了的诡异问题。代价是每次请求都新建 client,几张表的轻量项目感知不到差别,真到了压力很大的系统再用单例也不迟。这算是我实际项目里总结出的中庸策略。

注册完之后,控制器或者服务类里直接构造函数注入:

public class StudentService { private readonly ISqlSugarClient _db; public StudentService(ISqlSugarClient db) { _db = db; } public List<Student> GetAdults() { return _db.Queryable<Student>().Where(s => s.Age >= 18).ToList(); } }

到这里,一个 .NET Core 项目和数据库之间的最小闭环就搭好了。后面所有增删改查,都是围绕注入进来的这个 _db 展开。

2.3 连接配置细节与三分钟自检方法

连接串是"第一次集成"翻车最多的地方,列几个常见的点。MySQL 的字符集设置对中文支持影响很大,连接串里忘了写 charset=utf8mb4,插入中文可能直接变问号或者报编码错误。国内项目几乎都有中文数据,所以建议一开始就带上。SQL Server 用 Integrated Security 或账号密码都行,注意 Data Source 的写法在不同环境格式略有差别;SQLite 的路径最好用绝对路径或者通过配置拼出来,不然换环境就找不到库文件。

还有一个隐蔽坑:SqlSugar 对实体名和表名的默认映射是保持一致的,class Student 默认对应表 Student。如果你的数据库表名是小写 student,或者业务表带 t_ 前缀,要么在实体上标 [SugarTable("t_student")],要么开 MoreSettings.IsUnderLine 让框架自动转下划线命名。很多人第一次跑查询返回空表,查了半天发现是表名对不上。高版本 SqlSugar 还内置了全局的 EntityService,可以在实体创建时统一处理表名,也派得上用场。

自检方法三步就够了:先跑一次 IsValidConnection;再用 db.DbMaintenance.GetTableInfoList(true) 看能不能列出所有表;最后找一个已知表名,写一条最简单的 Queryable 查询打印出来。三步全过,说明环境 OK,可以放心写业务。

3. 高频实操:增删改查、分页、事务一次讲透

3.1 插入、更新、删除的写法与行为边界

直接上代码,这是日常最常用的四条:

// 插入 var s = new Student { Name = "张三", Age = 18 }; var id = db.Insertable(s).ExecuteReturnIdentity(); // 自增主键 var snowId = db.Insertable(s).ExecuteReturnSnowflakeId(); // 雪花ID // 更新 db.Updateable(s).ExecuteCommand(); // 按主键更新整行 // 部分字段更新 db.Updateable<Student>() .SetColumns(x => new Student { Age = 20, Remark = "改年龄" }) .Where(x => x.Id == id) .ExecuteCommand(); // 删除 db.Deleteable<Student>().Where(x => x.Id == id).ExecuteCommand();

重点说 ExecuteReturnIdentity 和 ExecuteReturnSnowflakeId 的区别。如果主键是数据库自增列,用前者拿回自增值;如果系统用的是雪花 ID 方案,主键由代码生成,用后者更方便。这两种拿回主键的方式解决的问题是一样的:插入之后马上要用这个 ID 继续做关联操作,否则你还得再查一遍刚插的数据。

Updateable(entity) 这个写法有个容易踩的坑:默认情况它会更新这个实体上所有非 null 的字段。CreateTime、Remark 这些你没打算动的字段,只要对象上有值,也会被一起写进 update 语句。我最早用的时候就没注意,把一个实体从缓存取出来改了一个字段,整行都被刷新,CreateTime 被旧值覆盖,差点酿成一次数据事故。所以"只更新某几个字段"这种需求,建议直接用带 SetColumns 的写法,字段控制权握在自己手里。如果一定要用整行更新,也可以配合 .IgnoreColumns(x => new { x.CreateTime }) 把不想动的字段排除掉。

3.2 条件查询、表达式函数与多表联查

查询是 ORM 的核心能力,SqlSugar 的 Queryable 链式写法用起来很顺:

// 基础条件 + 排序 + 取前几条 var list = db.Queryable<Student>() .Where(s => s.Age >= 18 && s.Name.Contains("张")) .OrderBy(s => s.Id, OrderByType.Desc) .Take(10) .ToList(); // 主键查单条 var one = db.Queryable<Student>().Where(s => s.Id == 10086).First(); // IN 查询 var ids = new List<int> { 1, 2, 3 }; var rows = db.Queryable<Student>().Where(s => ids.Contains(s.Id)).ToList(); // 范围查询 var between = db.Queryable<Student>() .Where(s => SqlFunc.Between(s.Age, 18, 25)) .ToList();

Where 里就是普通的 C# 表达式。Contains 会翻译成 like,StartsWith、EndsWith、日期比较、null 判断都有对应翻译。比较有意思的是 WhereIF,它接受一个 bool,为 true 才追加条件。这个在做列表筛选时极其好用,搜索框没填就不拼 where,填了才拼,不用再写一堆 if 把查询条件拆来拆去:

var keyword = "张"; var list2 = db.Queryable<Student>() .WhereIF(!string.IsNullOrEmpty(keyword), s => s.Name.Contains(keyword!)) .WhereIF(age > 0, s => s.Age >= age) .ToList();

多表联查用 LeftJoin 写法,和 SQL 的心智模型一致:

var joinList = db.Queryable<Student>() .LeftJoin<ClassInfo>((s, c) => s.ClassId == c.Id) .Where((s, c) => c.Name == "三年二班") .Select((s, c) => new { StudentId = s.Id, StudentName = s.Name, ClassName = c.Name }) .ToList();

这个写法的好处是只要你的 SQL 功底在线,几乎不用查文档。但我要提醒一句:别一上来就全表 Select 成实体,按需投影成匿名对象或 DTO,传输字段少很多,性能提升是实打实的。

3.3 分页的正确打开方式

分页是每个后端都躲不开的功能,SqlSugar 做得很省事:

var pageIndex = 2; var pageSize = 20; var total = 0; var pageList = db.Queryable<Student>() .Where(s => s.Age >= 18) .OrderBy(s => s.Id, OrderByType.Desc) .ToPageList(pageIndex, pageSize, ref total); Console.WriteLine($"总记录数:{total},本页条数:{pageList.Count}");

ToPageList 会自动拼 limit 语句,并通过 ref 参数把总记录数带出来,一条查询完成"列表加总数"两件事。注意 total 必须先声明变量再传 ref,很多人问了半天为什么总数是 0,都是没传 ref 或者传了一个常量进去。如果你不喜欢 ref 这种风格,还有 ToPageResult 方法,返回一个带 TotalCount 和 Items 属性的分页对象,参数上更优雅。异步版本对应 ToPageListAsync 和 ToPageResultAsync,在 ASP.NET Core 环境务必用异步,这个后面讲异步陷阱时还会提到。

分页相关的排序不要放在内存里做。有同事写过先 ToList 再 OrderBy 再 SkipTake 的写法,数据量小没问题,几万条以上内存和时间都会很难看。SqlSugar 的 OrderBy 是拼进 SQL 的,顺序别搞反——先过滤、再排序、最后分页,这是写查询代码的固定心法。

3.4 事务封装与并发一致的注意事项

多表写操作要"要么都成功、要么都失败",就得上事务。SqlSugar 的写法是包一个委托:

var result = db.Ado.UseTran(() => { db.Insertable(new Student { Name = "李四", Age = 20 }).ExecuteCommand(); db.Updateable<Student>() .SetColumns(x => new Student { ClassId = 5 }) .Where(x => x.Name == "李四") .ExecuteCommand(); }); if (result.IsSuccess) { Console.WriteLine("提交成功"); } else { Console.WriteLine($"回滚了,原因:{result.ErrorMsg}"); }

UseTran 内部帮你把 BeginTran、CommitTran、RollbackTran 都包好,委托里抛异常会自动回滚,result.IsSuccess 就是判断开关。异步场景用 UseTranAsync,结构一模一样。

这里有个关键点:事务必须在同一个 SqlSugarClient 实例上执行。如果事务里 A 方法用的 client 和 B 方法用的 client 不是同一个连接,第二个操作实际跑在另一个连接上,事务根本罩不住它。所以我在 2.2 节说注册成单例还是作用域要看项目,本质原因就在这——单例天然共享同一个实例;作用域则要确保同一个请求里所有服务注入的是同一个作用域实例,千万别在一个事务里手 new 一个 client 去执行操作。

还有一个容易被忽略的:事务里不要混用同步和异步方法。混用不一定报错,但某些驱动在连接就绪状态和命令交错执行时,会出现难以复现的异常。我的习惯是:入口是异步,整个事务链路上全部用 Async 后缀方法。

4. 进阶:CodeFirst 建表、仓储封装与性能调优

4.1 CodeFirst 建表与字段特性设计

SqlSugar 的 CodeFirst 不需要专门建实体映射配置类,直接在实体类上用特性声明就行:

[SugarTable("student", tableDescription: "学生表")] public class Student { [SugarColumn(IsPrimaryKey = true, IsIdentity = true, ColumnDescription = "主键")] public int Id { get; set; } [SugarColumn(ColumnDataType = "varchar(50)", Length = 50, IsNullable = false, ColumnDescription = "姓名")] public string Name { get; set; } [SugarColumn(ColumnDataType = "int", IsNullable = false, ColumnDescription = "年龄")] public int Age { get; set; } [SugarColumn(ColumnDataType = "datetime", IsNullable = true, ColumnDescription = "创建时间")] public DateTime? CreateTime { get; set; } }

[SugarTable] 指定表名和表描述,[SugarColumn] 控制字段类型、长度、是否可空、是否主键自增。建表就一行:

db.CodeFirst.InitTables(typeof(Student));

InitTables 是"有则跳过、无则创建"的语义,多次执行不会重复建表,放在项目启动时执行是安全的,非常适合快速迭代的开发阶段。它默认不会去改已经存在的表结构,字段有变化时需要你手动处理。这不是 SqlSugar 的缺点,反而避免了很多 ORM 自动迁移导致的生产故障——生产环境里改表结构,本来就该走人工审核流程。

我个人的建议是:正式环境别光靠 CodeFirst 维护表结构,建表脚本由专人评审后执行;但在开发环境和演示项目里,CodeFirst 负责把实体和表的关系快速对齐,效率非常高。

4.2 轻量仓储封装:该不该包、怎么包

SqlSugar 本身 API 已经够丰富,很多人会问还要不要再套一层仓储。这里说点不太中听但真实的话:别把仓储包成万能壳。见过不少项目,明明 SqlSugar 三行能写完的查询,硬是包出 GetAsync、GetByIdAsync、QueryAsync 全家桶,使用方还得猜该调哪个方法,调试时在仓库层和技术层之间跳来跳去,极其痛苦。

但如果项目确实需要统一审计字段、统一软删除、统一分页返回模型,那做一层薄封装是值得的。注意封装的是"横切逻辑",而不是"把 CRUD 换个皮":

public abstract class BaseRepository<T> where T : class, new() { protected readonly ISqlSugarClient _db; protected BaseRepository(ISqlSugarClient db) { _db = db; } public virtual async Task<List<T>> GetListAsync(Expression<Func<T, bool>> where) { return await _db.Queryable<T>().Where(where).ToListAsync(); } public virtual async Task<PagedResult<T>> GetPageAsync( Expression<Func<T, bool>> where, int pageIndex, int pageSize, Expression<Func<T, object>> orderBy, OrderByType orderType) { var total = 0; var list = await _db.Queryable<T>() .Where(where) .OrderBy(orderBy, orderType) .ToPageListAsync(pageIndex, pageSize, ref total); return new PagedResult<T> { Total = total, Items = list }; } }

PagedResult 自己定义一个只带 Total 和 Items 的简单 DTO 就行。这套封装的好处是,审计字段和软删除条件可以在基类里统一拼进去,保证"所有查询默认过滤已删除数据",这个价值远大于省那几行 CRUD 代码。真要写复杂查询的时候,直接注入 ISqlSugarClient 写裸查询,不要试图把复杂 SQL 硬塞进仓储的通用方法里。

4.3 AOP 日志、SQL 追踪与性能优化建议

SqlSugar 的 AOP 钩子很适合做 SQL 日志,是排查问题的神器:

db.Aop.OnLogExecuting = (sql, pars) => { Console.WriteLine(sql); foreach (var p in pars) { Console.WriteLine($" {p.ParameterName}: {p.Value}"); } };

在开发环境下,把 Aop 日志打到控制台或日志文件,等于免费获得一条 SQL 审核通道。上线出了问题,翻日志能看到某一步到底执行了什么 SQL、参数是什么,不用靠猜。我做过一个工具类项目,所有数据操作都经过这个钩子记录,后期线上数据被莫名修改时,顺着日志一分钟就定位到了问题 SQL 和调用链路。

性能优化方面,除了前面说的按需投影、先在数据库过滤再分页,还有几个实际有效的点。第一,给高频查询加查询缓存,SqlSugar 支持 Redis 和内存缓存,配置好缓存服务后,查询上加 .WithCache() 即可。第二,批量插入用 Fastest 的 BulkCopy,几千行以上的数据差别非常明显,一条条 Insertable 会慢到让人怀疑机器坏了。第三,多表联查时注意外键列和 Where 条件列上的索引,ORM 再快,没有索引的大表查询一样是灾难。以上优化里,AOP 日志必须最先做好,因为它决定了你之后能不能高效定位和优化。

5. 真实踩坑记录与排查速查表

5.1 常见问题速查表

把这几年用 SqlSugar 遇到频次最高的问题整理成一张表,方便直接对照。

现象原因解决办法
插入中文变成问号连接串缺少字符集配置增加 charset=utf8mb4,并保证数据库默认字符集一致
Updateable 更新了不该更新的字段Updateable(entity) 默认更新全部非 null 字段改用 SetColumns 指定字段,或用 IgnoreColumns 排除
查询返回空但数据库有数据实体名表名映射不一致用 [SugarTable] 显式指定表名,检查大小写与命名约定
分页 total 始终为 0没有用 ref 传 total 变量先声明 total 变量再传入 ref total
事务里某个操作回滚不生效事务中混用了不同 client 实例确保事务内所有操作共用同一个 SqlSugarClient
异步方法报错或不稳定同步异步混用,或对异步方法使用 .Result/.Wait()全链路使用 Async 方法,避免请求线程上阻塞等待
日期字段出现 MinValue 转换错误给不可空 datetime 列传了 DateTime.MinValue改用 DateTime?,插入前对默认时间做空值处理
查出的实体导航属性为 null没有配置 Mapper 或没有使用联查用 .Mapper() 配置导航映射,或用 LeftJoin 拼查询

这张表覆盖环境配置、基础 API、事务并发、异步、类型这几大类。遇到问题先对照一遍,大部分能直接解决。

5.2 调试思路与定位技巧

最后聊几个排查方法论。SqlSugar 报错时,不要只盯着最下面的异常信息,先看它生成的 SQL。大多数运行时错误,比如列名不存在、字段类型不匹配、语法错误,把 SQL 复制到数据库客户端里跑一下,立刻能看出问题。所以我建议任何环境都把 Aop.OnLogExecuting 开着,最少也要在异常日志里带上 SQL。

如果遇到摸不着头脑的奇怪行为,比如某个查询只在某种数据量下不正常,可以通过逐步简化表达式来定位。先用最简单的常量条件能查出结果,再一点点加条件,看是哪一步引入问题。这种二分排查在表达式翻译类 ORM 上效率很高,毕竟再聪明,也不能保证每个 C# 表达式翻译出来的 SQL 都和预期一致。

还有一个容易被忽视的:实体类的类型别乱用。DateTime 和 DateTime?、int 和 int?、decimal 和 decimal?,表面上只是多一个问号,实际影响 SQL 参数的 DbType 和条件判断的生成。比如一个可空的 CreateTime 字段,你声明成非空 DateTime,查询时对 null 的处理就会很奇怪。实体字段类型要和数据库列严格对应,能标注可空就标注可空,省得排查半天。

我的习惯是每遇到一个新问题,先在本地写一个最小复现的测试方法,把问题场景缩到最小,再回填到业务代码里。以前直接在生产日志里猜问题,效率低,还容易被各种无关错误干扰。把排查路径固化下来:看 SQL、简化条件、核对类型、查连接串,这四步走完,九成以上的问题都已经浮出水面。

6. 一段个人使用体会

写这篇之前,我又把一个小项目的 SqlSugar 版本从 5.0 升到了最新,顺带把几个老接口的查询改成了异步版本。说实话,这类 ORM 的版本迭代没那么激进,接口很稳定,升级基本无痛。最后分享一个个人心得:SqlSugar 上手快,但不代表你可以跳过对数据库本身的理解。ORM 帮你省掉的是样板代码,而不是数据库设计的功课——索引、查询计划、事务隔离这些底层能力,最终还是决定系统能走多远的变量。先认真学会用,再想着怎么穿透它去做优化,这才是和 ORM 相处的正确姿势。

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

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

立即咨询