SqlSugar与SQLite:.NET轻量级数据持久化黄金组合实战指南
1. 项目概述:为什么选择SqlSugar与SQLite这对黄金搭档?
如果你刚开始接触.NET开发,或者正在寻找一个轻量、快速上手的数据持久化方案,那么“SqlSugar操作SQLite数据库”这个组合,绝对值得你花时间研究。我最初接触这个组合,是在一个需要快速原型验证的小工具项目里。当时的需求很简单:需要一个零配置、单文件、无需安装额外服务的数据库,同时,操作数据库的代码要足够简洁优雅,不能到处都是拼凑的SQL字符串。SQLite和SqlSugar的相遇,完美地解决了这两个痛点。
SQLite,你可能早就听说过,它是一个进程内的、无服务器的、零配置的、事务性的SQL数据库引擎。说人话就是,它就是一个.db或.sqlite文件,你的程序直接读写这个文件,不需要像MySQL或SQL Server那样先安装、再启动一个数据库服务。这对于客户端应用、移动应用、小型网站或者测试环境来说,简直是神器。部署简单,拷贝文件就行;开发也简单,用个可视化工具(比如DB Browser for SQLite)就能直接查看和修改数据。
而SqlSugar,是一个在国内开发者中非常流行的.NET ORM框架。ORM(Object-Relational Mapping)对象关系映射,它的目标就是让你能用操作C#对象(类)的方式去操作数据库表,省去手写大量枯燥且易错的ADO.NET代码。SqlSugar以其高性能、语法糖丰富(顾名思义)、以及对中国开发者友好的中文文档和社区支持而著称。它的链式查询语法写起来非常流畅,学习曲线相对平缓。
把这两者结合起来,你得到的就是一个“开箱即用”的快速开发数据层方案。你不需要操心数据库安装,只需要一个NuGet包和一个数据库文件;你也不需要深入学习复杂的SQL和ADO.NET,就能完成绝大多数增删改查(CRUD)操作。这对于入门者构建第一个有数据存储功能的小项目,或者老手快速搭建演示、测试项目,效率提升是立竿见影的。接下来,我会带你从零开始,一步步搭建环境,完成基础的CRUD,并分享一些我实际使用中总结出来的经验和避坑点。
2. 环境准备与项目搭建
2.1 创建项目与安装NuGet包
首先,我们得有一个.NET项目。这里以最常见的控制台应用为例,使用.NET 6或更高版本(它们都是长期支持版本,且项目文件简洁)。你可以通过Visual Studio的创建向导,或者直接用命令行:
dotnet new console -n SqlSugarWithSQLiteDemo cd SqlSugarWithSQLiteDemo项目创建好后,我们需要引入两个核心的NuGet包。你可以通过Visual Studio的NuGet包管理器图形界面搜索安装,或者使用包管理器控制台,当然,最“极客”的方式还是命令行:
dotnet add package sqlite-net-pcl dotnet add package sqlsugarcore这里解释一下为什么是两个包:
sqlite-net-pcl: 这是SQLite的.NET实现,提供了与SQLite数据库引擎交互的基础能力。SqlSugar底层需要依赖它来连接和操作SQLite文件。sqlsugarcore: 这就是SqlSugar ORM框架本身的核心库。注意,如果你用的是.NET Framework的老项目,可能需要安装SqlSugar包,但对于.NET Core/.NET 5+的项目,SqlSugarCore是更好的选择。
安装完成后,你的项目文件(.csproj)里应该能看到对应的包引用。这一步是基石,务必确保安装成功,没有版本冲突。
2.2 初始化数据库与SqlSugar配置
安装好包之后,我们就要开始写代码了。首先,在Program.cs(如果是.NET 6+的单文件模板)或者你定义的启动类里,我们需要配置SqlSugar来连接我们的SQLite数据库。
假设我们的数据库文件叫demo.db,放在程序运行目录下。我们来创建一个简单的配置类或方法:
using SqlSugar; using System; namespace SqlSugarWithSQLiteDemo { public class Program { // 声明一个静态的SqlSugarClient实例,方便全局使用(对于简单Demo或小应用可以这样,生产环境建议依赖注入) public static SqlSugarClient Db; static void Main(string[] args) { // 1. 配置数据库连接 Db = new SqlSugarClient(new ConnectionConfig() { ConnectionString = "Data Source=./demo.db", // 连接字符串,指向当前目录下的demo.db文件 DbType = DbType.Sqlite, // 数据库类型 IsAutoCloseConnection = true, // 是否自动关闭连接,建议设为true InitKeyType = InitKeyType.Attribute // 初始化主键和自增列的方式,通过特性(Attribute) }); // 2. 输出SQL日志(开发调试非常有用) Db.Aop.OnLogExecuting = (sql, pars) => { Console.WriteLine($"【SQL语句】: {sql}"); // 如果需要打印参数,可以这样: // if (pars != null && pars.Length > 0) // { // Console.WriteLine($"【参数】: {string.Join(", ", pars.Select(p => $"{p.ParameterName}:{p.Value}"))}"); // } }; Console.WriteLine("数据库连接配置完成!"); // 后续的演示代码将在这里调用Db对象 // CreateTableIfNotExists(); // InsertData(); // QueryData(); // UpdateData(); // DeleteData(); } } }关键配置解析:
- ConnectionString:
Data Source=./demo.db。这是SQLite最基础的连接字符串格式,./表示当前程序运行目录。你也可以用绝对路径,如D:\mydata\demo.db。 - DbType: 必须指定为
DbType.Sqlite,告诉SqlSugar我们连接的是哪种数据库。 - IsAutoCloseConnection: 设置为
true是个好习惯。这意味着每次执行完数据库操作,SqlSugar会自动帮你关闭连接,避免连接泄露。对于SQLite这种文件数据库,虽然连接泄露的后果不像服务器数据库那么严重,但保持良好的习惯总是对的。 - InitKeyType: 设置为
InitKeyType.Attribute。这表示我们将通过C#属性上的特性(如[SugarColumn(IsPrimaryKey = true)])来标识主键和自增列。这是SqlSugar推荐的方式,清晰且与实体类强关联。 - Aop.OnLogExecuting: 这是一个事件钩子,当SqlSugar执行SQL前会触发。我们把执行的SQL语句打印到控制台。这是开发阶段最重要的调试工具!你能看到SqlSugar帮你生成了什么样的SQL,对于排查问题、理解ORM行为有巨大帮助。
注意:在生产环境中,日志输出需要更谨慎,避免将敏感数据或过多的日志输出到控制台,应集成到如Serilog、NLog等日志框架中。
3. 定义实体类与表结构映射
ORM的核心思想是“对象-关系映射”。所以,我们需要先定义C#的实体类(对象),来对应数据库中的表。SqlSugar提供了丰富的特性(Attribute)来定义这种映射关系。
假设我们要创建一个Student(学生)表。我们先在项目中创建一个Models文件夹,然后添加Student.cs类:
using SqlSugar; namespace SqlSugarWithSQLiteDemo.Models { [SugarTable("Student")] // 指定该实体类映射到数据库中的表名,如果不加,默认用类名 public class Student { [SugarColumn(IsPrimaryKey = true, IsIdentity = true)] // 标识为主键且自增 public int Id { get; set; } [SugarColumn(Length = 50, IsNullable = false)] // 长度50,不可为空(NOT NULL) public string Name { get; set; } public int Age { get; set; } [SugarColumn(Length = 100, IsNullable = true)] // 长度100,可以为空 public string? Email { get; set; } // C# 8.0+ 的可空引用类型,与IsNullable=true对应 [SugarColumn(ColumnDataType = "datetime", IsNullable = true)] public DateTime? EnrollmentDate { get; set; } // 可为空的日期时间 // 这是一个不映射到数据库的普通属性 [SugarColumn(IsIgnore = true)] public string DisplayInfo => $"{Name} (Age: {Age})"; } }特性详解与避坑指南:
[SugarTable]: 类级别的特性。用于指定实体对应的数据库表名。如果省略,SqlSugar默认使用类名作为表名(区分大小写取决于数据库,SQLite默认不区分)。建议显式指定,避免因类名更改或数据库命名规范不同导致问题。[SugarColumn]: 属性级别的特性,功能强大。IsPrimaryKey = true: 声明该属性是表的主键。IsIdentity = true: 声明该列是自增列(通常与主键Id搭配使用)。对于SQLite,自增主键通常是INTEGER类型。Length: 设置字符串字段的最大长度。对于string类型,强烈建议设置Length,否则SqlSugar可能会使用默认长度(如nvarchar(max)),在SQLite中虽然可能没问题,但不利于数据规范化和迁移到其他数据库。IsNullable: 指示该列是否允许为NULL。这里的设置需要与C#属性的可空性匹配。例如,string? Email对应IsNullable = true;string Name对应IsNullable = false。如果不匹配,在生成表或插入数据时可能会出错。ColumnDataType: 直接指定数据库中的列类型,如"datetime","decimal(10,2)"。当SqlSugar默认推断的类型不符合你要求时使用。例如,SQLite的DateTime映射有时需要明确。IsIgnore = true: 该属性将不会映射到数据库表中。常用于计算属性、临时字段或敏感信息。
- 命名规范: SqlSugar默认使用“原样映射”,即属性名
Name对应列名Name。如果你想使用不同的命名约定(如驼峰转下划线),可以在ConnectionConfig中配置MoreSettings,但这属于进阶内容,入门期保持默认即可。
实操心得:在项目初期,花点时间设计好实体类结构,合理使用特性,能为后续开发省去大量麻烦。特别是主键、自增、可空性和长度的设定,是数据完整性的第一道防线。
4. 基础CRUD操作详解
环境、配置、实体类都准备好了,现在让我们进入最核心的部分——增删改查。我会用最直观的链式调用语法来演示。
4.1 创建表(Code First)
在插入数据前,我们需要确保表存在。SqlSugar的Code First功能可以帮你根据实体类自动创建或更新表结构。
在Main方法中调用以下方法:
static void CreateTableIfNotExists() { // Db.CodeFirst 是Code First操作的入口 // .StringDefaultLength(50) 设置string类型属性的默认长度,避免每个属性都写Length // .InitTables(typeof(Student)) 初始化指定实体类对应的表 // 如果表不存在则创建,存在则检查列,缺少的列会自动添加(但不会删除列或修改列类型,这是安全策略) Db.CodeFirst.SetStringDefaultLength(50).InitTables(typeof(Student)); Console.WriteLine("Student表检查/创建完成。"); }执行这个方法后,去你的项目目录下看看,demo.db文件应该已经被创建了。你可以用DB Browser for SQLite打开它,会发现里面多了一个Student表,字段和我们在Student类中定义的一模一样。
注意:
InitTables是“初始化”表,它只负责创建不存在的表和添加不存在的列。它不会删除表中已有的列,也不会修改已有列的数据类型。这是一种安全的同步策略。如果你需要修改列类型或删除列,需要手动执行SQL(Db.Ado.ExecuteCommand)或使用更高级的迁移工具。对于入门项目,通常重建数据库文件(删除旧的.db文件)再运行InitTables更简单。
4.2 插入数据(Create)
插入单条数据非常简单:
static void InsertSingleStudent() { var newStudent = new Student { Name = "张三", Age = 20, Email = "zhangsan@example.com", EnrollmentDate = DateTime.Now }; // 插入并返回受影响的行数(通常是1) var count = Db.Insertable(newStudent).ExecuteCommand(); Console.WriteLine($"插入了 {count} 条数据。新学生的ID是:{newStudent.Id}"); // 插入并返回自增的主键值(推荐,更直观) var id = Db.Insertable(newStudent).ExecuteReturnIdentity(); Console.WriteLine($"插入了数据,新学生的ID是:{id}。实体对象Id属性也被更新了:{newStudent.Id}"); }关键点:
Insertable(T): 创建一个插入操作构建器。ExecuteCommand(): 执行命令,返回受影响行数。ExecuteReturnIdentity(): 执行命令,并返回插入行的自增主键值。注意:执行此方法后,传入的实体对象newStudent的Id属性也会被自动赋值为这个新ID,非常方便。- 批量插入可以使用
Insertable(List<T>),然后调用ExecuteCommandAsync或ExecuteReturnIdentityAsync(异步版本),效率更高。
4.3 查询数据(Read)
查询是ORM最常用的功能,SqlSugar的查询语法非常强大且易读。
(1)查询所有记录:
static void QueryAllStudents() { var list = Db.Queryable<Student>().ToList(); Console.WriteLine($"共有 {list.Count} 个学生:"); foreach (var stu in list) { Console.WriteLine($" ID:{stu.Id}, 姓名:{stu.Name}, 年龄:{stu.Age}, 邮箱:{stu.Email}"); } }Queryable<T>()是查询的起点,.ToList()将结果转换为List<T>。
(2)带条件的查询(链式调用):
static void QueryWithCondition() { // 查询年龄大于18岁,且姓名包含“张”的学生,按年龄降序排列 var list = Db.Queryable<Student>() .Where(stu => stu.Age > 18) // Lambda表达式条件 .Where(stu => stu.Name.Contains("张")) // 可以链式多个Where,效果是AND .OrderBy(stu => stu.Age, OrderByType.Desc) // 排序 .ToList(); // 或者使用更简洁的语法,多个条件在一个Where里 // .Where(stu => stu.Age > 18 && stu.Name.Contains("张")) Console.WriteLine($"符合条件的學生有 {list.Count} 个。"); }(3)查询单条记录:
static void QuerySingleStudent() { // 根据主键查询(最常用) var student = Db.Queryable<Student>().InSingle(1); // 查询Id=1的学生 if (student != null) { Console.WriteLine($"找到学生:{student.Name}"); } // 根据其他条件查询单条,如果有多条会抛出异常 var student2 = Db.Queryable<Student>().Where(stu => stu.Name == "李四").First(); // 使用FirstOrDefault更安全,查不到返回null var student3 = Db.Queryable<Student>().Where(stu => stu.Name == "王五").FirstOrDefault(); }(4)分页查询:
分页是Web开发中的必备技能。
static void QueryWithPaging() { int pageIndex = 1; // 第1页 int pageSize = 5; // 每页5条 int totalCount = 0; // 用于接收总记录数 var pageList = Db.Queryable<Student>() .OrderBy(stu => stu.Id) // 分页必须排序 .ToPageList(pageIndex, pageSize, ref totalCount); // 关键方法 Console.WriteLine($"第{pageIndex}页,共{Math.Ceiling(totalCount * 1.0 / pageSize)}页,本页{pageList.Count}条,总计{totalCount}条。"); foreach (var stu in pageList) { Console.WriteLine($" ID:{stu.Id}, 姓名:{stu.Name}"); } }ToPageList方法非常方便,一次性获取当前页数据和总记录数。
4.4 更新数据(Update)
更新操作需要指定要更新的实体和更新条件。
static void UpdateStudent() { // 方式1:先查询,修改对象,然后更新(全字段更新) var stuToUpdate = Db.Queryable<Student>().InSingle(1); if (stuToUpdate != null) { stuToUpdate.Age = 21; stuToUpdate.Email = "updated@example.com"; var count = Db.Updateable(stuToUpdate).ExecuteCommand(); Console.WriteLine($"更新了 {count} 条数据。"); } // 方式2:只更新指定字段(更高效,推荐) var count2 = Db.Updateable<Student>() .SetColumns(stu => new Student { Age = 22, Email = "partial@example.com" }) // 只更新Age和Email .Where(stu => stu.Id == 1) .ExecuteCommand(); Console.WriteLine($"部分更新了 {count2} 条数据。"); // 方式3:基于原值的更新(如年龄+1) var count3 = Db.Updateable<Student>() .SetColumns(stu => stu.Age == stu.Age + 1) // 年龄自增1 .Where(stu => stu.Id == 2) .ExecuteCommand(); Console.WriteLine($"自增更新了 {count3} 条数据。"); }实操心得:在大多数业务场景中,方式2(部分更新)是最佳实践。它只生成更新特定字段的SQL(如UPDATE Student SET Age=22, Email='partial@example.com' WHERE Id=1),避免了全字段更新可能带来的并发问题(如其他人修改了Name字段,却被你无意中覆盖回旧值),并且性能更好。
4.5 删除数据(Delete)
删除操作相对简单,但需谨慎。
static void DeleteStudent() { // 根据主键删除 var count1 = Db.Deleteable<Student>().In(1).ExecuteCommand(); // 删除Id=1的记录 Console.WriteLine($"根据主键删除了 {count1} 条数据。"); // 根据条件删除 var count2 = Db.Deleteable<Student>() .Where(stu => stu.Age < 18) // 删除年龄小于18岁的学生 .ExecuteCommand(); Console.WriteLine($"根据条件删除了 {count2} 条数据。"); // 删除所有数据(危险!慎用) // var count3 = Db.Deleteable<Student>().ExecuteCommand(); }重要警告:
Deleteable<T>().ExecuteCommand()不带任何条件时,会删除整张表的所有数据!在生产环境中执行删除操作前,务必再三确认Where条件,最好先Select一下看看会影响到哪些数据。对于重要数据,建议采用“软删除”(即增加一个IsDeleted标志位,用Update来标记删除)而非物理删除。
5. 进阶技巧与常见问题排查
掌握了基础的CRUD,你已经可以应对很多场景了。但在实际项目中,总会遇到一些更复杂的需求或奇怪的问题。这里分享几个我踩过坑后总结的进阶技巧和排查方法。
5.1 事务处理
数据库事务用于确保一系列操作要么全部成功,要么全部失败。SqlSugar提供了非常简洁的事务支持。
static void TransactionDemo() { try { Db.Ado.BeginTran(); // 开始事务 // 操作1:插入学生A var stuA = new Student { Name = "事务学生A", Age = 25 }; Db.Insertable(stuA).ExecuteReturnIdentity(); // 操作2:插入学生B var stuB = new Student { Name = "事务学生B", Age = 26 }; Db.Insertable(stuB).ExecuteReturnIdentity(); // 模拟一个可能失败的操作 // int a = 0; // int b = 1 / a; // 这里会抛出除以零异常,触发回滚 Db.Ado.CommitTran(); // 提交事务 Console.WriteLine("事务提交成功,两条数据均已插入。"); } catch (Exception ex) { Db.Ado.RollbackTran(); // 回滚事务 Console.WriteLine($"事务执行失败,已回滚。错误:{ex.Message}"); } }关键点:务必使用try-catch块包裹事务操作,在catch中执行回滚。BeginTran,CommitTran,RollbackTran必须配对使用。也可以使用Db.UseTran(() => { ... })的语法糖,它自动处理提交和回滚,代码更简洁。
5.2 使用AOP日志排查问题
我们在配置阶段已经开启了SQL日志。这是排查问题最强大的武器。当你发现查询结果不对、更新失败时,第一件事就是去看控制台输出的SQL语句。
常见问题1:查询条件没生效?看看生成的SQL的WHERE子句对不对,参数值是否正确传递了。
常见问题2:插入失败,主键冲突?检查是不是手动设置了自增主键Id的值,而该值已存在。
常见问题3:更新了不该更新的字段?检查SetColumns语句,看它生成的SET部分是否只包含了你想要的字段。
养成看SQL日志的习惯,你能更深入地理解SqlSugar在背后做了什么,也能更快地定位问题是出在ORM配置上,还是出在SQL逻辑本身。
5.3 实体类与数据库表不同步怎么办?
这是Code First开发中常见的问题。比如,你给Student类新增了一个Address属性,但数据库表里没有这个字段。
- 安全模式(默认):使用
Db.CodeFirst.InitTables,它会添加新列,但不会删除或修改已有列。对于新增属性,这通常没问题。 - 强制同步(危险):SqlSugar提供了
Db.CodeFirst.SetStringDefaultLength(50).InitTables<T>(true)的重载,传入true参数可以尝试更激进的同步(如修改列类型),但行为可能因数据库而异,且有丢失数据的风险,不建议在生产数据库上直接使用。 - 推荐做法:对于已有数据的表结构变更,最稳妥的方式是:
- 编写SQL迁移脚本(如
ALTER TABLE Student ADD COLUMN Address TEXT;)。 - 使用
Db.Ado.ExecuteCommand来执行这个脚本。 - 或者,使用专业的数据库迁移工具(如FluentMigrator),并与SqlSugar结合。
- 编写SQL迁移脚本(如
5.4 性能优化小贴士
- 批量操作:插入、更新、删除大量数据时,务必使用批量方法(如
Insertable(List<T>)),而不是在循环中执行单条操作。性能差异可能是几十倍甚至上百倍。 - 只查询需要的字段:如果实体类有很多字段(如大文本、二进制字段),但本次查询只需要其中几个,可以使用
.Select(stu => new { stu.Id, stu.Name })来投影到匿名对象,减少数据传输量。 - 合理使用索引:虽然SqlSugar不直接创建索引,但你应该根据查询条件(特别是
Where和OrderBy中的字段),在数据库表中创建合适的索引。这能极大提升查询速度。可以通过Db.Ado.ExecuteCommand("CREATE INDEX ...")来执行创建索引的SQL。 - 关闭不必要的日志:生产环境中,记得关闭或调整
Aop.OnLogExecuting的日志级别,避免输出海量SQL日志影响性能。
5.5 常见错误速查表
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
SqlSugar.SqlSugarException: Connection open error . | 1. 数据库文件路径错误或不可写。 2. 文件被其他进程独占锁定(如另一个程序打开着 .db文件)。 | 1. 检查ConnectionString中的路径,确保程序有读写权限。2. 关闭所有可能访问该文件的程序(如DB Browser)。 |
Microsoft.Data.Sqlite.SqliteException: SQLite Error 1: 'no such table: Student'. | 表不存在。 | 确保已成功执行Db.CodeFirst.InitTables创建表。检查实体类是否添加了[SugarTable]特性或类名是否正确。 |
SqlSugar.SqlSugarException: The entity does not have a primary key. | 执行InSingle、按主键更新/删除等操作时,实体类没有用[SugarColumn(IsPrimaryKey=true)]标记主键。 | 在实体类中正确定义主键属性。 |
插入后newStudent.Id始终为0 | 1. 主键未设置为自增(IsIdentity=true)。2. 使用了 ExecuteCommand()而不是ExecuteReturnIdentity()。 | 1. 检查主键属性的[SugarColumn]特性。2. 插入后需要获取自增ID时,使用 ExecuteReturnIdentity()。 |
| 查询结果与预期不符 | 1. 查询条件(Lambda)写错了。 2. 大小写敏感问题(SQLite默认不区分大小写,但某些操作可能区分)。 | 1. 查看AOP输出的SQL日志,核对生成的WHERE条件。2. 在查询中使用 SqlFunc.ToLower()或SqlFunc.ToUpper()进行规范化比较。 |
| 更新了所有记录! | Updateable操作忘记了加.Where()条件。 | 立即检查代码!更新和删除操作前,必须明确指定Where条件。这是最容易出事故的地方。 |
掌握了这些基础操作、进阶技巧和排错方法,你已经可以自信地使用SqlSugar和SQLite来为你的.NET项目构建数据访问层了。这个组合在轻量级应用、原型开发、单元测试等领域有着极高的效率和便利性。记住,多动手实践,多查看生成的SQL日志,是掌握任何ORM框架的最佳途径。