1. 为什么选择SqlSugar + SQLite作为你的第一个ORM项目?
如果你刚开始接触后端开发,或者想给自己的桌面应用、移动端App加一个轻量级的本地数据存储,那么“数据库”这个词可能会让你感到既熟悉又陌生。熟悉是因为它无处不在,陌生是因为从写SQL语句到真正在代码里把数据存进去、查出来,中间好像隔着一道鸿沟。直接拼装SQL字符串?太容易出错,而且代码又臭又长。这时候,ORM(对象关系映射)工具就像一位翻译官,帮你把面向对象的代码(C#里的类)和关系型数据库的表自动对应起来,让你能用操作“对象”的方式去操作“数据”。
在.NET生态里,SqlSugar是近年来非常热门的一个国产ORM框架。它轻量、高性能、语法糖丰富(这也是它名字的由来),对开发者非常友好。而SQLite,则是一个无需安装、配置、零管理的嵌入式数据库,一个.db文件就是整个数据库,完美契合开发测试、单机应用、移动端等场景。把这两者结合起来,你几乎可以零成本地搭建一个可用的数据访问层,快速验证你的业务逻辑。这比一上来就折腾SQL Server、MySQL的安装和远程连接要友好得多。我见过很多新手项目死在环境配置上,而SqlSugar+SQLite这个组合,能让你跳过那些坑,直接专注于业务代码本身,体验从零到一建立起数据持久化能力的完整过程,这种正反馈对学习至关重要。
2. 项目起手式:环境搭建与第一个DbContext
理论说再多不如动手。我们首先需要一个.NET项目。这里以主流的.NET 6/8的控制台应用为例,当然,你也可以用在ASP.NET Core Web API、WPF、MAUI等项目里,原理相通。
第一步:创建项目与安装NuGet包打开你的IDE(Visual Studio, VS Code, Rider等),创建一个新的控制台应用项目。然后,通过NuGet包管理器安装以下两个核心包:
Install-Package SqlSugarCore Install-Package System.Data.SQLite.Core第一个是SqlSugar本体,第二个是SQLite的ADO.NET数据提供程序,SqlSugar需要通过它来和SQLite.db文件对话。
第二步:配置并初始化SqlSugar的DbContext在ORM的世界里,DbContext(数据库上下文)是你的指挥中心,它管理着数据库连接、事务,以及所有实体类的映射关系。我们来创建一个:
using SqlSugar; using System; namespace SqlSugarDemo { public class SqliteDbContext { // SqlSugarClient 是操作数据库的主要入口 public SqlSugarClient Db { get; } public SqliteDbContext() { // 1. 配置连接字符串 // Data Source 指定数据库文件路径。这里使用相对路径,会在程序运行目录下创建 test.db 文件 // 如果文件不存在,SQLite会自动创建它。 string connectionString = "Data Source=./test.db"; // 2. 创建并配置 SqlSugarClient Db = new SqlSugarClient(new ConnectionConfig() { ConnectionString = connectionString, // 连接字符串 DbType = DbType.Sqlite, // 数据库类型 IsAutoCloseConnection = true, // 是否自动关闭连接(建议true,由框架管理) InitKeyType = InitKeyType.Attribute // 主键、自增等如何生成,这里使用特性(Attribute)标注 }); // 3. (可选但推荐) 输出SqlSugar生成的SQL语句到控制台,方便调试和学习 Db.Aop.OnLogExecuting = (sql, pars) => { Console.WriteLine($"【SQL语句】{sql}"); // 如果需要打印参数,可以遍历 pars }; } } }这段代码有几个关键点值得展开:
- 连接字符串:
Data Source=./test.db是最简单的形式。你可以用绝对路径(如C:\mydata.db),也可以用:memory:来创建一个仅存在于内存中的临时数据库,非常适合单元测试。 - IsAutoCloseConnection:设为
true是推荐做法。SqlSugar会在每次数据库操作(查询、插入等)后,自动判断并关闭连接,避免了传统ADO.NET中需要手动Open()和Close()的繁琐,也减少了连接泄露的风险。 - InitKeyType:设为
Attribute意味着我们将在实体类上用[SugarColumn]等特性来详细定义字段属性。另一种方式是InitKeyType.SystemTable,通过读取数据库现有表结构来反向生成实体信息,但在代码先行(Code First)的开发模式中,使用特性更直观、可控。 - Aop.OnLogExecuting:这是一个神器。它会在SqlSugar执行SQL前触发,把生成的原始SQL和参数打印出来。对于初学者,这是理解ORM如何工作的“透视镜”;对于老手,这是排查慢查询、验证逻辑的调试利器。
3. 定义你的数据蓝图:实体类与特性映射
有了指挥中心,我们需要定义“士兵”——也就是实体类。它对应数据库里的一张表。假设我们要做一个简单的用户管理系统,先定义一个User类。
using SqlSugar; namespace SqlSugarDemo.Entities { [SugarTable("Users")] // 指定该实体类映射到数据库中的表名,如果不加,默认用类名 public class User { // SqlColumn是字段特性,IsPrimaryKey代表主键,IsIdentity代表自增(对于SQLite,通常是整数主键自动自增) [SugarColumn(ColumnName = "Id", IsPrimaryKey = true, IsIdentity = true)] public int Id { get; set; } [SugarColumn(ColumnName = "Name")] public string Name { get; set; } [SugarColumn(ColumnName = "Age")] public int Age { get; set; } [SugarColumn(ColumnName = "Email", IsNullable = true)] // IsNullable = true 表示字段可空 public string? Email { get; set; } [SugarColumn(ColumnName = "CreatedAt")] public DateTime CreatedAt { get; set; } } }这里详细拆解一下[SugarColumn]特性的常用属性:
- ColumnName:指定实体属性对应数据库表的哪个字段。如果属性名和字段名一致,可以省略。
- IsPrimaryKey:是否为主键。一个表必须有一个主键。
- IsIdentity:是否为自增列。对于SQLite,通常将整数主键设为自增,插入时不用管Id,数据库会自动分配。
- IsNullable:字段是否允许为
NULL。对于引用类型(如string),默认就是可空的(C# 8.0+的可空引用类型上下文下),但显式声明是个好习惯。值类型(如int)需要设置为IsNullable = true才能在数据库里存NULL。 - ColumnDataType:可以强制指定字段在数据库中的类型,如
ColumnDataType = "VARCHAR(50)"。但SqlSugar通常能根据C#类型智能推断,除非有特殊需求。
踩坑点1:SQLite的“伪自增”很多人以为在SQLite里,把主键设为
INTEGER PRIMARY KEY就会自动自增。实际上,如果你在插入时显式指定了该列的值(即使为0或NULL),SQLite会尝试使用你提供的值,可能导致主键冲突。而SqlSugar的IsIdentity=true会在插入时主动忽略这个属性的值,确保由数据库分配ID,这才是真正的“自增”行为。所以,在定义实体时,明确标出IsIdentity很重要。
踩坑点2:字符串长度与索引SQLite对
VARCHAR长度限制比较宽松,但如果你知道某个字段(如用户名、邮箱)有最大长度,最好用ColumnDataType或Length属性([SugarColumn(Length=100)])进行限定。这不仅是数据完整性的要求,更重要的是,如果你未来要为这个字段创建索引,不定长度的字段可能会影响索引效率和查询性能。虽然入门阶段可能用不到,但养成好习惯能避免后期重构。
4. 从创建表到增删改查:完整的CRUD实战
DbContext和实体类都准备好了,现在让我们把它们用起来。我们在Program.cs或Main方法中编写以下代码。
4.1 创建数据库表(Code First)
Code First模式允许我们通过代码来创建或更新表结构。
using SqlSugarDemo; using SqlSugarDemo.Entities; var dbContext = new SqliteDbContext(); // 创建数据库表(如果不存在) dbContext.Db.CodeFirst.InitTables(typeof(User)); Console.WriteLine("表‘Users’已就绪。");InitTables方法会检查当前数据库,如果Users表不存在,则根据User实体类的定义创建它;如果存在,但结构不一致(比如新增了字段),它会尝试修改表结构(添加新列)。注意:对于已有数据的表,修改列类型或删除列等危险操作,SqlSugar默认是关闭的,需要额外配置,生产环境务必谨慎。
4.2 插入数据(Create)
插入单条数据非常简单:
var newUser = new User { Name = "张三", Age = 25, Email = "zhangsan@example.com", CreatedAt = DateTime.Now }; // Insertable 返回一个插入构建器,ExecuteReturnIdentity 执行插入并返回自增的主键Id var userId = dbContext.Db.Insertable(newUser).ExecuteReturnIdentity(); Console.WriteLine($"插入成功,用户Id: {userId}");批量插入能极大提升性能:
var userList = new List<User> { new User { Name = "李四", Age = 30, CreatedAt = DateTime.Now }, new User { Name = "王五", Age = 28, Email = "wangwu@example.com", CreatedAt = DateTime.Now }, }; // ExecuteReturnIdentity 在批量插入时返回的是第一个插入行的Id,不是列表。 var affectedRows = dbContext.Db.Insertable(userList).ExecuteCommand(); Console.WriteLine($"批量插入成功,影响行数: {affectedRows}");4.3 查询数据(Read)
查询是ORM最核心也最丰富的功能。SqlSugar提供了接近LINQ的链式查询语法,非常直观。
(1)查询所有记录
var allUsers = dbContext.Db.Queryable<User>().ToList(); foreach (var user in allUsers) { Console.WriteLine($"ID:{user.Id}, 姓名:{user.Name}, 年龄:{user.Age}"); }Queryable<T>()是查询的起点,ToList()执行查询并将结果映射到List<User>。
(2)带条件的查询(Where)
// 查询年龄大于25岁的用户 var adultUsers = dbContext.Db.Queryable<User>() .Where(u => u.Age > 25) .ToList(); // 查询邮箱不为空的用户,并按年龄倒序排列 var usersWithEmail = dbContext.Db.Queryable<User>() .Where(u => u.Email != null) .OrderBy(u => u.Age, OrderByType.Desc) // OrderByType.Desc 表示降序 .ToList();这里的Where和OrderBy用法和LINQ几乎一样,SqlSugar会将其转换为对应的SQLWHERE和ORDER BY子句。打开之前配置的SQL输出,你就能看到生成的语句。
(3)分页查询分页是Web应用中最常见的需求。
int pageIndex = 1; // 第1页 int pageSize = 10; // 每页10条 RefAsync<int> totalCount = 0; // 用于接收总记录数 var pagedList = await dbContext.Db.Queryable<User>() .ToPageListAsync(pageIndex, pageSize, totalCount); // 同步方法:.ToPageList(pageIndex, pageSize, ref totalCount) Console.WriteLine($"第{pageIndex}页数据,共{totalCount}条记录,每页{pageSize}条。"); foreach (var user in pagedList) { Console.WriteLine(user.Name); }ToPageListAsync方法会执行两次查询:一次是COUNT(*)获取总数,另一次是LIMIT ... OFFSET ...获取当前页数据。这对于大数据量表是必要的开销。
(4)查询单个字段或简单统计
// 查询所有用户的名字列表 var names = dbContext.Db.Queryable<User>().Select(u => u.Name).ToList(); // 查询用户平均年龄 var avgAge = dbContext.Db.Queryable<User>().Avg(u => u.Age); // 类似的还有 .Sum(), .Max(), .Min(), .Count()4.4 更新数据(Update)
更新操作需要指定条件,否则会变成全表更新(极其危险!SqlSugar默认有保护机制,但自己也要小心)。
// 更新指定Id的用户信息 var updateResult = dbContext.Db.Updateable<User>() .SetColumns(u => new User() { Name = "张老三", Email = "newemail@example.com" }) .Where(u => u.Id == userId) .ExecuteCommand(); // 另一种更简洁的写法:根据实体对象的主键值进行更新 var userToUpdate = dbContext.Db.Queryable<User>().First(u => u.Id == userId); if (userToUpdate != null) { userToUpdate.Age = 26; userToUpdate.Email = "updated@example.com"; dbContext.Db.Updateable(userToUpdate).ExecuteCommand(); }SetColumns方法允许你只更新指定的列,这对于只需要修改部分字段的场景非常高效,能避免全字段更新带来的并发问题和不必要的网络传输。
4.5 删除数据(Delete)
删除操作更要慎之又慎,务必确保Where条件准确。
// 删除指定Id的用户 var deleteResult = dbContext.Db.Deleteable<User>() .Where(u => u.Id == someId) .ExecuteCommand(); // 根据主键值直接删除 dbContext.Db.Deleteable<User>().In(1).ExecuteCommand(); // 删除Id为1的记录 dbContext.Db.Deleteable<User>().In(new[]{1,2,3}).ExecuteCommand(); // 删除Id为1,2,3的记录In方法对于按主键批量删除非常方便。
5. 进阶技巧与实战避坑指南
掌握了基本的CRUD,你已经可以应付大多数简单场景。但要想写出更健壮、高效的代码,还需要了解下面这些进阶知识和常见陷阱。
5.1 事务处理:保证数据一致性
事务用于确保一系列数据库操作要么全部成功,要么全部失败。比如转账操作:A账户扣钱和B账户加钱必须同时成功。
try { dbContext.Db.Ado.BeginTran(); // 开始事务 // 操作1:A账户扣款 dbContext.Db.Updateable<Account>().SetColumns(a => a.Balance - 100).Where(a => a.Id == 1).ExecuteCommand(); // 模拟一个可能失败的操作 // int a = 0; // int b = 1 / a; // 这里会抛出异常,触发回滚 // 操作2:B账户收款 dbContext.Db.Updateable<Account>().SetColumns(a => a.Balance + 100).Where(a => a.Id == 2).ExecuteCommand(); dbContext.Db.Ado.CommitTran(); // 提交事务 Console.WriteLine("事务执行成功!"); } catch (Exception ex) { dbContext.Db.Ado.RollbackTran(); // 回滚事务 Console.WriteLine($"事务执行失败,已回滚。错误:{ex.Message}"); }关键点:务必使用try-catch包裹事务代码,在catch中执行回滚。CommitTran只有在所有操作都成功后才会被调用。SqlSugar也支持更简洁的UseTran方法,它自动处理提交和回滚。
5.2 实体类与数据库的同步策略
在项目初期,表结构变动频繁。除了手动调用CodeFirst.InitTables,你还可以配置更智能的同步策略。在创建SqlSugarClient时进行配置:
var db = new SqlSugarClient(new ConnectionConfig(){...}); // 配置实体同步策略 db.CodeFirst .SetStringDefaultLength(200) // 设置string类型字段的默认长度 .BackupTable().InitTables(typeof(User), typeof(Product)); // 修改表时备份数据BackupTable()是一个很实用的方法。当表结构发生变化(如添加新列)时,SqlSugar会先备份原表数据,然后创建新表,最后把数据导回去。这比直接ALTER TABLE在某些复杂变更上更安全。但对于删除列、修改列类型等操作,仍需评估数据丢失风险。
5.3 性能优化初探
- 异步操作:尽可能使用
*Async方法(如ToListAsync,ExecuteCommandAsync),特别是在Web应用中,可以避免阻塞线程,提高并发能力。 - 批量操作:如前所述,插入、更新、删除都支持批量操作,能减少数据库往返次数,性能提升一个数量级。
- 查询优化:
- 只查询需要的字段:使用
.Select(u => new { u.Id, u.Name })而不是查询整个实体,尤其是在表字段很多或包含大文本字段时。 - 合理使用索引:虽然ORM层面不直接创建索引,但你要知道,在
Where、OrderBy、GroupBy中频繁使用的字段,应该在数据库层面建立索引。对于SQLite,你可以在实体类特性中标注[SugarColumn(IsIndex = true)],然后在InitTables时让SqlSugar帮你创建,或者手动执行SQL:CREATE INDEX IX_Users_Age ON Users(Age);。 - 警惕N+1查询问题:这是一个ORM通病。例如,你先查询一个用户列表,然后遍历每个用户去查询他的订单详情,就会产生1次查询用户 + N次查询订单的N+1次查询。解决方法是使用联表查询或导航属性(SqlSugar支持
[Navigate]特性)。
- 只查询需要的字段:使用
5.4 联表查询入门
假设我们还有一个Order表,通过UserId关联User表。SqlSugar的联表查询非常强大。
// 假设有Order实体,包含UserId字段 var list = dbContext.Db.Queryable<User>() .LeftJoin<Order>((u, o) => u.Id == o.UserId) // 左连接 .Where((u, o) => o.Amount > 100) // 联表后的条件 .Select((u, o) => new // 选择需要返回的字段 { UserId = u.Id, UserName = u.Name, OrderId = o.Id, OrderAmount = o.Amount }) .ToList();联表查询是进阶必备技能,它能将多次数据库查询合并为一次,极大提升效率。掌握InnerJoin、LeftJoin、Where条件和Select投影的写法,是脱离新手村的重要标志。
从环境搭建到完成基础的CRUD,再到事务、同步和性能优化的初步了解,这条路径覆盖了一个ORM项目从零开始所需的核心知识。SqlSugar的文档相当丰富,当你遇到更复杂的需求时,如分库分表、读写分离、过滤器等,其官方文档是最好的下一站。记住,ORM的目的是提升开发效率,而不是完全取代SQL。理解它生成的SQL,并在必要时使用db.Ado.SqlQuery<T>执行原生SQL,才是灵活而强大的做法。