我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

.NET Core数据访问层实战:深入解析SqlSugar ORM高效用法

.NET Core数据访问层实战:深入解析SqlSugar ORM高效用法 最近在忙一个 .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 Serverlocalhost;Port3306;Databaseblog;Uidroot;Pwd123456;charsetutf8mb4;, 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.AddSingletonISqlSugarClient(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 ListStudent GetAdults() { return _db.QueryableStudent().Where(s s.Age 18).ToList(); } }到这里一个 .NET Core 项目和数据库之间的最小闭环就搭好了。后面所有增删改查都是围绕注入进来的这个 _db 展开。2.3 连接配置细节与三分钟自检方法连接串是第一次集成翻车最多的地方列几个常见的点。MySQL 的字符集设置对中文支持影响很大连接串里忘了写 charsetutf8mb4插入中文可能直接变问号或者报编码错误。国内项目几乎都有中文数据所以建议一开始就带上。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.UpdateableStudent() .SetColumns(x new Student { Age 20, Remark 改年龄 }) .Where(x x.Id id) .ExecuteCommand(); // 删除 db.DeleteableStudent().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.QueryableStudent() .Where(s s.Age 18 s.Name.Contains(张)) .OrderBy(s s.Id, OrderByType.Desc) .Take(10) .ToList(); // 主键查单条 var one db.QueryableStudent().Where(s s.Id 10086).First(); // IN 查询 var ids new Listint { 1, 2, 3 }; var rows db.QueryableStudent().Where(s ids.Contains(s.Id)).ToList(); // 范围查询 var between db.QueryableStudent() .Where(s SqlFunc.Between(s.Age, 18, 25)) .ToList();Where 里就是普通的 C# 表达式。Contains 会翻译成 likeStartsWith、EndsWith、日期比较、null 判断都有对应翻译。比较有意思的是 WhereIF它接受一个 bool为 true 才追加条件。这个在做列表筛选时极其好用搜索框没填就不拼 where填了才拼不用再写一堆 if 把查询条件拆来拆去var keyword 张; var list2 db.QueryableStudent() .WhereIF(!string.IsNullOrEmpty(keyword), s s.Name.Contains(keyword!)) .WhereIF(age 0, s s.Age age) .ToList();多表联查用 LeftJoin 写法和 SQL 的心智模型一致var joinList db.QueryableStudent() .LeftJoinClassInfo((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.QueryableStudent() .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.UpdateableStudent() .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 BaseRepositoryT where T : class, new() { protected readonly ISqlSugarClient _db; protected BaseRepository(ISqlSugarClient db) { _db db; } public virtual async TaskListT GetListAsync(ExpressionFuncT, bool where) { return await _db.QueryableT().Where(where).ToListAsync(); } public virtual async TaskPagedResultT GetPageAsync( ExpressionFuncT, bool where, int pageIndex, int pageSize, ExpressionFuncT, object orderBy, OrderByType orderType) { var total 0; var list await _db.QueryableT() .Where(where) .OrderBy(orderBy, orderType) .ToPageListAsync(pageIndex, pageSize, ref total); return new PagedResultT { 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 遇到频次最高的问题整理成一张表方便直接对照。现象原因解决办法插入中文变成问号连接串缺少字符集配置增加 charsetutf8mb4并保证数据库默认字符集一致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 相处的正确姿势。
返回列表