C#开发者的数据层利器:SugarSql ORM入门与实践
1. SugarSql初探为什么我在这时候选择引入它写这个系列写到现在前面的文章一直在用原生的ADO.NET和一些基础的封装方式做数据操作。很多读者可能在评论区已经看出来了越写到后面对手写SQL和DataTable的封装就越觉得繁琐尤其是在做条件拼接和分页这类重复性极强的工作时代码量完全压不住。所以这一篇我决定正式把SugarSql拉进来做一次初步的探索和对比。SugarSql是国产ORM框架里比较活跃的一个官方定位是“高性能、轻量级、支持多数据库的ORM”。它支持SqlServer、MySql、Oracle、Sqlite、PostgreSQL等主流数据库并且提供了非常丰富的封装方法从基础的增删改查、到分页、事务、多表查询、批量插入甚至代码生成器和实体类生成都有内置支持。对于中小型项目和个人开发者来说用它的效率比手写SQL高很多。这篇文章适合谁看如果你是一个已经能写基本SQL、但还没怎么接触过ORM的C#开发者或者说你用Dapper用得很顺手但觉得手写SQL还是太累再或者说你正在选型一个适合快速开发的数据层框架那这篇“初探”会比较对路。我会从最基础的安装配置开始把第一次接触SugarSql会遇到的困惑和坑尽量都趟一遍。可能有人会问既然前面写了那么多原生方式为什么现在要引入一个ORM框架我的答案很简单项目复杂度上去之后纯手写SQL的维护成本会爆炸而SugarSql在保留SQL灵活性的同时给了你足够多的便捷封装。这一篇不会写太深主要是把“第一次使用SugarSql”的完整路径走通环境准备、基础配置、第一个增删改查、以及几个最常用的进阶功能初体验。2. 前置认知SugarSql到底解决了什么问题2.1 从手写SQL的痛点说起在没有ORM之前C#开发者做数据操作大概是什么状态写一个SqlConnection拼一个SqlCommand然后手动添加参数、执行、再手动映射结果集到实体对象。这套流程偶尔写一两次还好但当你有一个系统里有几十张表、几百个查询场景时你会发现大量的时间都花在了重复的机械劳动上。举一个非常典型的场景做条件分页查询。同一个用户列表你要支持按用户名模糊查询、按状态筛选、按时间范围筛选还要分页。如果三个条件都没有选中SQL就是SELECT * FROM Users如果选了用户名就得拼一个WHERE UserName LIKE name如果同时选了状态和时间拼接逻辑会更长。这种“动态拼接SQL”的逻辑写多了之后非常容易出错——少了一个空格、多了一个AND、参数顺序不对这些坑我都踩过。SugarSql把这个过程简化成了链式查询。你不需要拼SQL字符串而是通过方法调用描述查询条件。框架自动帮你拼SQL、传参数、映射结果。动态条件处理也简单很多理论上可以让你把更多精力放在业务逻辑本身而不是和字符串拼接搏斗。2.2 SugarSql的核心价值定位SugarSql最让我喜欢的点是它在“便捷”和“灵活”之间找到了一个比较好的平衡点。它不像某些重量级ORM那样过度封装导致学习成本很高也不像纯微ORM那样几乎所有SQL都要自己写。它的核心设计理念可以概括为简单场景直接调用方法复杂场景提供原生SQL的逃生通道。它对多种数据库的支持也是亮点之一。你写好的查询代码理论上换一个数据库只需要改连接串大部分代码不需要变动。这一点在做多数据库兼容的产品时非常实用我见过不少项目因为后期需要从SqlServer迁移到MySql而大改数据层代码用SugarSql的话这种迁移成本会低很多。另外它的文档和示例代码在国内社区是比较齐全的。不管是官网文档、博客教程还是各种开源项目的源码都能找到大量参考。对中文开发者来说这一点在使用体验上是很加分的。2.3 和几个主流ORM的粗略对比这里我不想做太多主观的“谁优谁劣”评价只是从实际使用感受上做一个横向对比。框架核心特点上手难度适合场景原生ADO.NET最底层完全控制SQL中性能极致敏感、SQL极复杂Dapper轻量微ORMSQL由开发者控制低追求性能且不介意写SQLEntity Framework功能全面的重量级ORM中高大型项目、复杂模型关系、迁移SugarSql轻量封装多数据库支持低中小项目、快速开发、多数据库兼容单论“第一次接触的友好程度”SugarSql在国产ORM里确实算对新手比较友好的。而且它的作者一直在持续更新社区活跃度也比较高。3. 环境准备从NuGet安装到第一个连接3.1 安装和引入我用的是最普通的.NET环境创建一个控制台项目来做初探实验。如果你的开发环境是通用的.NET版本操作上基本大同小异。在Visual Studio或者Rider里可以直接通过NuGet包管理器搜索SqlSugar安装最新稳定版即可。如果你习惯用命令行在项目目录下执行dotnet add package SqlSugar安装完成后项目中会自动添加引用。这时候需要注意一件小事如果你的项目目标是老版本的.NET Framework可能还需要额外安装System.ValueTuple这类兼容包。不过现在大部分新项目用新版.NET的话基本没有这些麻烦事。3.2 创建连接实例SugarSql的核心入口是SqlSugarClient或者它的异步版本。你可以把它理解成数据库连接的管理器所有数据库操作都是通过它来执行的。最简单的初始化代码如下public class DbContext { public static SqlSugarClient GetInstance() { var connectionString Serverlocalhost;DatabaseTestDb;User Idsa;Password123456;; var db new SqlSugarClient(new ConnectionConfig { ConnectionString connectionString, DbType DbType.SqlServer, IsAutoCloseConnection true, InitKeyType InitKeyType.Attribute }); return db; } }有几个配置项值得说明一下。IsAutoCloseConnection true这个选项非常实用它会在每次操作完成后自动关闭连接省去了手动管理连接的麻烦。在原生ADO.NET时代每次操作都要记得关连接有时候异常导致连接泄漏排查起来很头疼。SugarSql默认不自动关但这个开关强烈建议打开尤其是初学者。InitKeyType.Attribute表示通过实体类上的特性来指定主键等元信息。还有一种方式是InitKeyType.SystemTable那种情况下框架会查询数据库的表结构来获取元数据。实际开发中我更推荐用Attribute方式因为显式声明比隐式探测更可控。3.3 日志与调试配置第一次使用SugarSql强烈建议把SQL日志输出打开。这算是我的一个经验习惯不管是学习阶段还是项目开发阶段能看到框架实际生成的SQL语句能让你更好地理解它做了什么排查问题的时候也方便得多。配置方法如下var db new SqlSugarClient(new ConnectionConfig { ConnectionString connectionString, DbType DbType.SqlServer, IsAutoCloseConnection true, InitKeyType InitKeyType.Attribute, MoreSettings new ConnMoreSettings { IsAutoRemoveDataCache true } }); db.Aop.OnLogExecuting (sql, pars) { Console.WriteLine($SQL: {sql}); Console.WriteLine($Params: {string.Join(, , pars.Select(p ${p.ParameterName}{p.Value}))}); };通过Aop.OnLogExecuting这个事件每次框架执行SQL前都会走这个回调。打印出来的SQL日志真的很有用你能清楚地看到链式查询最终生成了什么SQL也能验证参数是否正确。对了这个Aop事件里还可以做性能监控、SQL拦截等高级操作后面有机会再展开。设置好之后试着跑一次简单的查询如果看到了SQL日志输出说明环境已经通了。3.4 注意事项连接串和版本差异有一类问题是初级使用者最容易遇到的连接串错误或者数据库版本不匹配。SugarSql本身是个客户端库对数据库服务端的版本没有严格要求但连接串一定要正确。如果你用的是SqlServer且是默认实例Serverlocalhost一般就够了。如果是命名实例需要写成Serverlocalhost\\SQLEXPRESS这种形式。账号密码用SQL Server身份验证还是Windows身份验证也有区别后者连接串里不需要用户名密码改成Integrated Securitytrue。另外一个值得注意的点是如果你在项目里同时用了SugarSql的高版本包和某些依赖旧版SugarSql的第三方库可能产生版本冲突。这类问题的一般解决思路是把SugarSql统一升级到最新稳定版或者用NuGet的“绑定重定向”功能来解决。我第一次遇到的时候困惑了一段时间后来发现是版本冲突导致的。4. 基础实操实体类与增删改查全流程4.1 实体类的两种映射方式ORM的核心思想是把数据库表和对象模型做映射SugarSql也不例外。使用SugarSql时你得先定义一个实体类这个类对应一张表。最简单的实体类定义方式利用特性标注映射关系[SugarTable(Users)] public class User { [SugarColumn(IsPrimaryKey true, IsIdentity true)] public int Id { get; set; } [SugarColumn(ColumnName user_name)] public string UserName { get; set; } public string Email { get; set; } [SugarColumn(IsNullable true)] public int? Age { get; set; } }这里有几个细节说明一下。SugarTable特性用于指定数据库表名。如果类名和表名不一致就必须要加这个特性。SugarColumn特性用来指定列名。C#属性的命名习惯是帕斯卡命名而数据库表的列名风格多种多样——有下划线风格的有全小写的甚至有大写驼峰的。通过ColumnName属性可以让实体属性和物理列名解耦。主键和自增列的标注很重要。IsPrimaryKey true标示主键IsIdentity true标示自增列。如果没有正确配置主键SugarSql在执行更新或删除操作时可能无法生成准确的WHERE条件会误伤多条数据或者报错。还有一种映射方式是不写任何特性让SugarSql自动通过类名和属性名匹配表名和列名。如果数据库表的设计命名恰好和类、属性保持一致这种方式可以省掉一堆特性标注。但我的建议是如果团队有统一的命名规范并且表名和代码模型名恰好一致可以偷懒否则建议显式标注避免歧义。4.2 插入操作同步与异步SugarSql封装了非常简洁的插入方法。var user new User { UserName 张三, Email zhangsanexample.com, Age 25 }; var db DbContext.GetInstance(); // 同步方式 var insertResult db.Insertable(user).ExecuteCommand(); // 异步方式 var insertResultAsync await db.Insertable(user).ExecuteCommandAsync();ExecuteCommand()返回受影响的行数一般插入一条数据返回1。如果你想插入后拿到自增主键的值可以用ExecuteReturnIdentity()或者ExecuteReturnEntity()。前者返回自增ID后者会把自增ID直接回填到传入的实体对象里方便后续使用。批量插入也是类似的方式var userList new ListUser { /* ... */ }; var bulkResult db.Insertable(userList).ExecuteCommand();对于几千条数据量的批量插入SugarSql还做了性能优化比循环单条插入快很多。4.3 查询按主键查、列表查、条件查SugarSql的查询写法很灵活从最简单的到复杂的都能覆盖。按主键查询是使用频率最高的操作var user db.QueryableUser().InSingle(1);这个InSingle(1)表示根据主键为1查询一条记录。这里的“In”是三值操作符的一种封装含义是“主键在给定集合中”当只给一个值时就是精确匹配。如果用Where(u u.Id 1)也能达到同样效果但InSingle更简洁。查询列表也很简单var allUsers db.QueryableUser().ToList();动态条件查询可以这样写var query db.QueryableUser(); if (!string.IsNullOrEmpty(name)) { query query.Where(u u.UserName.Contains(name)); } if (minAge.HasValue) { query query.Where(u u.Age minAge.Value); } var result query.ToList();这里最让人舒服的是条件可以通过if分支动态追加SugarSql内部会自动处理条件逻辑不需要手写字符串拼接。这就是我前面说“动态条件能力带来幸福感”的来源。查询一条指定的记录var singleUser db.QueryableUser() .Where(u u.UserName 张三) .First();需要注意的是如果查询结果不止一条First()会返回第一条。如果期望结果唯一且不存在时报错可以用Single()方法但它有一个要求查询结果必须恰好是一条否则抛异常。更多的时候我们只是想知道“是否存在符合条件的数据”SugarSql也提供了对应的判断方法var exists db.QueryableUser().Any(u u.Email testexample.com);这个Any会被翻译成EXISTS语句性能上比查出所有数据再Count要好。4.4 更新操作实体更新与列级控制更新操作SugarSql提供了多种模式。最简单的是根据实体更新前提是实体必须携带主键值var user db.QueryableUser().InSingle(1); user.UserName 李四; var updateResult db.Updateable(user).ExecuteCommand();SugarSql会以实体的主键作为条件更新实体的所有非空列。注意这里有个隐含行为实体中有值非默认值的属性会被更新而值为null或默认值的属性可能不会被更新。这是很多初学者容易踩的坑。如果你需要精确控制更新哪些列可以这样写var updateResult db.Updateable(user) .UpdateColumns(u new { u.UserName, u.Email }) .ExecuteCommand();这样只有UserName和Email会被更新其他列不受影响。还有一种反向操作是排除指定列var updateResult db.Updateable(user) .IgnoreColumns(u new { u.Age }) .ExecuteCommand();我实际开发中比较推荐显式指定更新列的写法因为你不想哪天不小心把不该更新的字段覆盖了。UpdateColumns的表达式写法虽然多敲几个字但安全性和可读性都更好。还有一种不带实体的纯列更新方式db.UpdateableUser() .SetColumns(u u.Age 30) .Where(u u.Id 1) .ExecuteCommand();这种写法适合批量更新。比如“把ID为1的用户的Age改成30”这一句话需求不需要先查出来再改直接一条语句搞定。4.5 删除操作两种常见模式删除操作有两种常见写法按实体删除和按条件删除。按实体删除var user db.QueryableUser().InSingle(1); var deleteResult db.Deleteable(user).ExecuteCommand();按条件删除是更常用的方式var deleteResult db.DeleteableUser() .Where(u u.Id 1) .ExecuteCommand();批量条件删除比如删除User表中Age小于18的所有数据var deleteResult db.DeleteableUser() .Where(u u.Age 18) .ExecuteCommand();这里有个安全提醒条件删除时尽可能用明确的、可预期的条件。如果不写Where条件SugarSql会直接清空全表数据这个操作没有二次确认。我建议在代码里始终写清楚Where条件哪怕你这次就是想清空表也要写一个Where(u true)的显式条件防止误触。5. 进阶功能初体验分页、排序、联表与事务5.1 分页查询的正确姿势分页是几乎所有业务系统都会遇到的需求。SugarSql的分页设计得非常好用官方叫“SugarPagedList”。常用的分页写法如下var pageIndex 1; var pageSize 10; var list db.QueryableUser() .OrderBy(u u.Id, OrderByType.Desc) .ToPageList(pageIndex, pageSize, ref totalCount); Console.WriteLine($总条数: {totalCount});ToPageList有两个重载传入ref int totalCount时框架会自动执行一个COUNT(*)查询来获取总条数如果不传这个参数则只返回分页数据适合那种不需要知道总数的场景。使用分页时有一个性能注意点先写条件过滤再做分页不要先查全表再在内存里分页那样在大数据量下性能会非常差。SugarSql的分页是在数据库层面执行的生成的是ROW_NUMBER()或OFFSET/FETCH这类语句性能是可靠的。但我见过有人在条件查询上加了ToList()然后再用LINQ的Skip/Take做内存分页——这就完全绕开了数据库分页优化数据量一大页面就卡死。SugarSql的链式写法本身是“延迟执行”的只要不调用ToList()所有条件都是组合后一次性生成的SQL。5.2 排序与多字段排序排序也是常用的功能。上面提到单字段排序直接OrderBy(u u.Id, OrderByType.Desc)。多字段排序可以继续追加var list db.QueryableUser() .OrderBy(u u.Id, OrderByType.Desc) .OrderBy(u u.Age, OrderByType.Asc) .ToList();注意这种连续调用OrderBy的方式不是覆盖而是追加排序条件最终生成的SQL是按Id DESC, Age ASC的顺序排序符合直觉。还有一个排序细节是排序最好在数据库层面完成不要先取回内存再用LINQ排序效率和可维护性都不好。5.3 联表查询一瞥初探级别的文章不深入写联表但SugarSql的联表能力还是值得提一句的。它支持LeftJoin、InnerJoin等常见联表方式。var query db.QueryableUser() .LeftJoinOrder((u, o) u.Id o.UserId) .Select((u, o) new { UserName u.UserName, OrderNo o.OrderNo, }) .ToList();这里通过Lambda表达式指定关联条件和选择列SugarSql会帮你生成对应的LEFT JOIN语句和列选择。这种写法比手写JOIN SQL要简洁不少而且类型安全——如果表名写错或者字段名拼错编译期就能发现。如果你之前用惯了字符串SQL的联表查询刚开始切换到Lambda表达式联表可能会觉得不习惯但用熟了以后会觉得这种“强类型”提示真的很方便不会出现那种可恶的“查询时才发现列名拼错了”的场景。5.4 事务的基本使用事务是数据一致性的基础保障。SugarSql提供了一个简单好用的事务包装方法UseTran。var result db.Ado.UseTran(() { db.Insertable(new User { UserName 事务测试, Email tranexample.com, Age 30 }).ExecuteCommand(); // 模拟一个失败操作 db.DeleteableUser() .Where(u u.Id -1) .ExecuteCommand(); throw new Exception(模拟出错回滚所有操作); }); if (result.IsSuccess) { Console.WriteLine(事务成功); } else { Console.WriteLine($事务失败: {result.ErrorMessage}); }这段代码里的throw模拟异常如果事务被正确执行前面已经完成的插入操作会被回滚。UseTran方法的回调里如果抛异常框架会自动回滚事务无需手动Rollback。如果你需要更细粒度的控制也可以用BeginTran()和CommitTran()/RollbackTran()手动管理。但我在实践里的建议是优先使用UseTran这种封装方式因为“异常自动回滚”这个行为能防止你忘记回滚导致的脏数据问题。5.5 复杂SQL的逃生通道虽然SugarSql的链式查询覆盖了大部分场景但总有那么一些SQL需求是用Lambda表达式不太好描述的——比如复杂的递归查询、某些特殊的数据库方言函数、或者一段优化到极致的原生SQL。SugarSql给你留了后门db.Ado.SqlQueryT。var list db.Ado.SqlQueryUser(SELECT * FROM Users WHERE Age age, new SugarParameter(age, 18));这里用SugarParameter来传参核心好处是防止SQL注入——千万别用字符串拼接的方式把值拼进去。还有执行非查询语句的方法ExecuteCommandvar count db.Ado.ExecuteCommand(DELETE FROM Users WHERE Age age, new SugarParameter(age, 18));这个“逃生通道”是我认为SugarSql设计得比较成熟的一点。框架没有试图把你禁锢在它的API体系里当你需要直接写SQL的时候它也能无缝支持。这种“能用简单方式解决绝不用复杂方式”的设计哲学很实用。6. 常见问题与排查技巧实录6.1 执行查询时报“object reference not set to an instance of an object”这是我当初踩的第一个坑刚接触SugarSql时经常遇到。排查下来最常见的原因是实体类主键没有正确标注。当SugarSql无法识别主键时它在某些操作内部需要获取主键信息的地方就会得到空引用。解决办法就是检查实体类上[SugarColumn(IsPrimaryKey true)]是否标注正确尤其是用InSingle方法的时候这个方法本质上就是依赖主键信息的。6.2 批量插入很慢用循环一条条插入几千条数据你会发现慢得离谱。这种慢不是SugarSql的问题而是你的使用方式问题。每执行一次Insertable().ExecuteCommand()都是独立的一次数据库往返几千次往返的延迟累加起来自然会慢。解决方案就是用批量插入APIdb.Insertable(list).ExecuteCommand();如果数据量特别大比如几万条还可以考虑分批次提交或者使用ExecuteBulkCopy这种方式利用数据库的BulkCopy特性速度会有一个质的飞跃。6.3 查询结果映射不准确实体属性名和表列名不一致时查询出来你会发现某些字段没有值。排查方式是检查SugarColumn(ColumnName...)是否写正确了表列名。另外有一种情况是SQL查询用了SELECT u.Id, u.Name AS UserName但列名和实体属性名不完全匹配SugarSql的映射规则是按列名匹配的所以别名必须准确对应实体属性名。6.4 异步方法卡死死锁在部分老项目的代码里如果在同步上下文环境中调用异步方法没有正确使用await可能会遇到死锁。比如你在某个事件处理器里写了var task db.QueryableUser().ToListAsync(); task.Wait();这时候可能卡住不动。解决方案是统一使用await方式调用异步API尽量不要混用同步等待。如果是在控制台或后台服务中本身没有同步上下文一般不会死锁但UI程序里要注意。6.5 常见问题速查表现象常见原因解决思路空引用异常主键未标注或连接未初始化检查实体类主键特性检查连接配置中文乱码连接串缺少字符集设置添加CharSetutf8或对应字符集参数数据插入很慢循环单条插入使用批量插入接口查询结果字段为空列名映射不正确使用SugarColumn(ColumnName)显式映射更新时误更新了不需要的字段使用了全字段更新使用UpdateColumns显式指定要更新的列异步操作死锁同步上下文等待异步任务尽量使用await链式异步调用大分页查询越来越慢深分页的固有问题用游标分页或基于ID的键集分页优化7. 初探总结与下一步学习建议这一篇“初探”主要把SugarSql最基础的使用路径走通了。从环境配置到实体映射从基本的增删改查到分页、联表和事务的简单使用再到遇到常见问题时的排查思路算是给后续深入使用打了一个基础。如果你按照这篇文章的步骤操作一遍并且把SQL日志打开观察过生成的SQL语句那么你对SugarSql的运行机制应该已经有了一个比较直观的认识。我个人在实际操作中的体会是SugarSql最值得称道的地方并不是它的某个单一功能而是整个数据访问层代码的写法变得更统一、更紧凑。用惯了原生ADO.NET再切换到SugarSql阅读代码时你会感觉到代码的意图更清晰业务逻辑也更突出。继续深入的方向我建议这样安排优先掌握条件表达式Expression的高级用法因为这是进行复杂查询的基础然后是事务的进阶用法比如跨库事务和分布式事务这类场景在微服务架构里经常遇到再然后就是性能优化方面比如缓存机制、异步批量等这几个方向的实际应用效果都很明显。如果你在实际使用中碰到了这篇文章之外的问题建议你优先去查看SugarSql的官方文档那里面的内容覆盖得非常全面。同时打开SQL日志输出也很重要——日志一旦打开框架做了什么基本就一目了然了排查问题会容易很多。