ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C# EF Core入门到实战:ORM、CRUD、迁移与SQLite应用

C# EF Core入门到实战:ORM、CRUD、迁移与SQLite应用 1. 这个教程讲什么以及为什么要学它先说个背景。很多朋友一开始写C#访问数据库都是直接拼SqlConnection、SqlCommand、SqlDataReader一套SQL字符串敲得飞起。代码量一大SQL散落得到处都是改个表结构所有相关SQL都要翻一遍查询结果还得手动把它填进实体对象重复劳动特别多。后来有人封装了Dapper比原生好不少但还是要自己写SQL映射模型也要手动处理稍微复杂一点的关系就有点力不从心。于是Entity Framework简称EF进入了主流视野它把数据库表映射成实体类把C#里的操作语句变成对象操作把常见的增删改查包装成几行代码就能完成省掉了大量手写SQL和手动赋值的活。这个教程适合谁一个是刚入门C#、想避开手写SQL地狱的新手另一个是用过ADO.NET或Dapper、想提升开发效率、对面向对象操作数据库有需求的老手。还有像你这种想用C#做上位机、做自动化、做工业信息采集的开发者——比如热词里提到“C#上位机”“C#连接DCS”“C#连接西门子OPC”“C# USB摄像头”这类场景虽然EF不直接负责通信但在把采集到的数据落库的时候EF能帮你把存储逻辑理得明明白白。学完这套教程你能用EF做一件事用几行C#代码完成数据库的增删改查并且让数据库结构变化通过迁移工具自动同步到代码里不用再为了数据访问写一堆样板代码。我在实际项目里用过EF6也用过EF Core踩过不少坑所以这篇教程不会停留在“Hello World”层面。我会把为什么这么设计、为什么选它、什么情况下该避开它都讲清楚。后面内容的安排是先从EF的整体设计思路和版本选型聊起然后给你一套能跑起来的基础环境搭建步骤接着把CRUD和查询、事务等核心操作手把手拆解最后把性能优化和几个高频报错的排查方法列出来。你按顺序看完再去自己做一个小项目基本就能上手了。2. EF的核心思路ORM到底是什么选EF6还是EF Core2.1 把“数据库操作”翻译成“对象操作”EF的本质是ORMObject-Relational Mapping对象关系映射。简单说它把数据库里的表当作C#类来看待把表里的一行记录当作一个类的实例把外键关联关系变成类之间的导航属性。你不需要写“insert into Users values(...)”而是写一个User对象new一个对象设置属性然后Add到上下文里调用SaveChangesEF就会自动生成并执行相应的SQL。它内部有几个核心组件DbContext代表数据库会话负责跟踪实体状态、管理连接、执行操作DbSet代表表或视图提供对实体的查询入口实体类就是数据库结构的C#表达。还有一个叫“迁移”的机制能把代码里的模型变更同步成数据库结构的更新脚本这比在数据库工具里手动建表、改字段要方便很多尤其适合团队协作时让数据库结构跟着代码走。之所以推荐用EF是因为大多数业务系统的数据访问都是“标准操作”单表查询、关联查询、分页、过滤、增删改、事务。这些场景用EF能省掉80%的重复代码。很多开发者担心的性能问题其实EF在查询编译、缓存、批量操作方面已经做了不少优化只要别把该投影的字段全表捞出来性能完全在可接受范围。2.2 版本选型EF6还是EF Core这是一个现实问题你现在搜“C# Entity Framework 教程”会看到EF6和EF Core两套体系。EF6是基于.NET Framework时代的经典版本也可以用在.NET Core上但EF Core是微软全重写的跨平台版本从.NET Core时代开始官方主推。我的建议很简单新项目尤其是.NET 6、.NET 7、.NET 8环境直接用EF Core不要犹豫。老项目、比较依赖EF6的地形模型、或者强依赖.NET Framework的遗留系统才继续在EF6上维护。为什么这么选因为EF Core不仅在性能上更好还内置了批量删除、弱同步、键集分页、数据库提供程序更丰富等新能力更重要的是微软对EF6只做安全维护不再增加新功能。从学习成本看两者核心概念差不太多学会EF Core再回头看EF6也容易。如果你是想给工控上位机、数据采集程序做本地数据库EF Core也完全能配合SQLite、LocalDB而且跨平台运行方便部署时只需要带上相应EF Core包。这里提一下热词里面的SQLiteEF Core配套的Microsoft.EntityFrameworkCore.Sqlite提供程序很活跃使用方式和SQL Server基本一致迁移时也能自动建表非常适合写单机版数据采集软件时不引入额外的数据库服务。2.3 数据访问的选型对比EF、原生ADO.NET和Dapper怎么取舍做数据访问方案选型时经常有人拿EF和Dapper对比。我的看法是它们不是对立关系是按场景选工具。原生ADO.NET执行SQL效率最高但代码重复多不适合业务逻辑复杂、模型多的情况。Dapper轻量微型ORM写SQL方便查询映射性能极高适合复杂查询、报表统计、大数据量查询场景。EF含EF Core全功能ORM自带变更跟踪、导航属性、自动迁移、事务管理适合业务系统开发写业务代码效率高。我在同一个小项目里也用过多框架简单业务用EF Core写复杂报表用Dapper直接写SQL两者共用同一个DbContext的连接不会冲突。这个思路很实用值得尝试。3. 环境准备与基础实操从空项目到第一条查询3.1 安装EF Core相关包接下来我以EF Core 8.NET 8为例把从零到能跑通的过程捋一遍。先建一个控制台项目或者类库项目然后用NuGet安装这几个包Microsoft.EntityFrameworkCoreMicrosoft.EntityFrameworkCore.SqlServer如果用SQL ServerMicrosoft.EntityFrameworkCore.Sqlite如果用SQLiteMicrosoft.EntityFrameworkCore.Design迁移时要用通常在启动项目里装在终端里装包的命令很简单dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.EntityFrameworkCore.Design如果写的是ASP.NET Core项目不需要手动安装SqlServer包可以通过下面命令在项目文件里加依赖但手动装包永远直接有效。3.2 写实体类和DbContext第一步是定义实体类。以订单和客户为例Customer和Order存在一对多关系public class Customer { public int Id { get; set; } public string Name { get; set; } public string Email { get; set; } public ListOrder Orders { get; set; } new(); } public class Order { public int Id { get; set; } public DateTime OrderDate { get; set; } public decimal TotalAmount { get; set; } public Customer Customer { get; set; } public int CustomerId { get; set; } }然后写DbContext重写OnModelCreating配置一下关系虽然按约定EF也能识别但显式配置更清晰public class AppDbContext : DbContext { public AppDbContext(DbContextOptionsAppDbContext options) : base(options) { } public DbSetCustomer Customers { get; set; } public DbSetOrder Orders { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.EntityOrder() .HasOne(o o.Customer) .WithMany(c c.Orders) .HasForeignKey(o o.CustomerId); } }我的习惯是所有实体类都放在Models文件夹DbContext放在Data文件夹这样项目结构一眼就能看清。3.3 配置连接字符串和依赖注入如果是在控制台应用最简单的方式是构造时传入连接字符串var options new DbContextOptionsBuilderAppDbContext() .UseSqlServer(Server(localdb)\\MSSQLLocalDB;DatabaseDemoDb;Trusted_ConnectionTrue;) .Options; using var db new AppDbContext(options);如果是ASP.NET Core直接在Program.cs里注册builder.Services.AddDbContextAppDbContext(options options.UseSqlServer(builder.Configuration.GetConnectionString(Default)));连接字符串放在appsettings.json不要写死在代码里。对工控上位机来说用SQLite时连接字符串极简就是一个文件路径options.UseSqlite(Data Sourcedemo.db);这里提一个经验开发时用LocalDB非常方便装上VS就有但不适合生产。生产环境要么用正式SQL Server要么考虑SQLite别为了省事在生产用LocalDB。3.4 用迁移自动建表在学习阶段你可以不用迁移直接调用EnsureCreated()建库建表。但真实项目推荐用迁移它能把模型变更记录成历史版本方便团队同步。先装好Design包执行dotnet ef migrations add InitialCreate dotnet ef database update运行成功后数据库里会出现一张__EFMigrationsHistory表这就是EF管理版本历史的地方。我踩过的坑是如果修改了实体类忘记创建新的迁移就直接跑程序编译不报错运行时会报“模型与数据库不一致”的错。这个错其实是保护机制避免模型和实际库不一致。3.5 第一个查询从数据库读数据建好表插入几条数据然后直接查询using var db new AppDbContext(options); var customers db.Customers .Where(c c.Name.Contains(张)) .ToList(); foreach (var c in customers) { Console.WriteLine(${c.Id} - {c.Name}); }这行代码只是写了个Lambda表达式EF会把它翻译成SQL并查询数据库。注意这里用了ToList()才真正执行查询前面只是构建查询计划。EF的延迟执行特性决定了你要是忘了ToList或ToArray后面做了很多过滤条件实际查询可能是在你遍历时一步到位执行的看日志时特别容易蒙。3.6 把连接日志打开看清EF生成的SQL强烈建议开发阶段打开SQL日志。EF Core里可以这样配置optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information);加上这句话后每次查询、更新EF都会在控制台输出生成的SQL语句。这能帮你快速排查“为什么我的查询结果不对”这种问题。我见过很多开发者看到EF生成的SQL和自己想象的不一样就慌了其实只要日志打开一切一目了然。4. 核心操作拆解增删改查、LINQ查询、事务这些够用的东西4.1 添加数据Add和AddRange添加单条数据var customer new Customer { Name 张三, Email zhangsanexample.com }; db.Customers.Add(customer); db.SaveChanges();添加多条var list new ListCustomer { new Customer { Name 李四 }, new Customer { Name 王五 } }; db.Customers.AddRange(list); db.SaveChanges();有一点要提醒SaveChanges是事务性的它会把所有待保存的变更一起提交中途失败就整体回滚。所以如果你要插入大量数据一次性AddRange几千条再SaveChanges事务可能太大性能也不见得好。我的实操经验是批量插入比如超过几百条时考虑分批保存或者直接迎合场景使用EF Core 7的ExecuteUpdate和ExecuteDelete做高效批量操作。4.2 查询数据不仅仅是Where和ToListEF Core的LINQ查询非常强大。下面这些场景是我在项目里经常用的。过滤和排序var result db.Orders .Where(o o.TotalAmount 100 o.OrderDate DateTime.Now.AddDays(-7)) .OrderByDescending(o o.TotalAmount) .ToList();关联查询如果没开懒加载用Include把导航属性捞出来var orders db.Orders .Include(o o.Customer) .Where(o o.CustomerId 1) .ToList();用Select做投影避免把整个实体拉出来var result db.Customers .Where(c c.Orders.Any()) .Select(c new { c.Id, c.Name, OrderCount c.Orders.Count }) .ToList();这个Select操作很重要很多时候你只需要几个字段却把几十个字段全查出来徒增网络和内存开销。实体跟踪也是一个点默认情况下查询出来的实体都会被DbContext跟踪如果只是只读展示不需要跟踪可以加AsNoTracking()来提升性能var data db.Customers.AsNoTracking().ToList();分页是老生常谈但EF Core也有更优解。Skip和Take做普通分页在小数据量没问题大数据量用键集分页更稳定。像这样var pageSize 20; var lastId 0; // 上一页最后的Id var page await db.Orders .AsNoTracking() .OrderBy(o o.Id) .Where(o o.Id lastId) .Take(pageSize) .ToListAsync();键集分页的好处是翻页时不会因为中间插入数据而导致重复或缺失而且索引利用更高效对动态查询性能提升明显。4.3 更新数据先查后改还是直接执行更新传统EF更新方式是先从数据库把实体查出来修改属性再SaveChanges。比如var customer db.Customers.First(x x.Id 1); customer.Email newexample.com; db.SaveChanges();这种方式简洁但有额外查询成本和跟踪开销。如果只是单纯更新某个字段而不需要读取实体完整数据可以用ExecuteUpdate直接生成update SQL跳过加载实体db.Customers .Where(c c.Id 1) .ExecuteUpdate(setters setters.SetProperty(c c.Email, newexample.com));这个API是EF Core 7引入的我用下来很方便尤其在批量更新状态时性能提升明显。同理删除也能用ExecuteDeletedb.Orders .Where(o o.OrderDate DateTime.Now.AddYears(-5)) .ExecuteDelete();执行后直接影响数据库不再有跟踪和先查后删的开销。如果你用的是EF6没有这两个API那就要坚持“先查对象再Remove”的老路。4.4 删除数据和级联删除要注意的事常规删除var order db.Orders.Find(1); db.Orders.Remove(order); db.SaveChanges();删除也存在操作前判断依赖关系的问题。EF默认配置下删掉有子记录的父记录会因为外键约束报错。要么删除子记录后再删父记录要么在模型关系配置里设置级联删除行为。我一般倾向于在业务层控制删除顺序不会在数据库层面直接级联删除因为一旦误删数据恢复很痛。还有一个小坑利用Find(id)如果找不到返回null直接Remove会抛ArgumentNullException。所以删除前先做判断或者用ExecuteDelete直接按条件删避免了先查后删的麻烦。4.5 事务从单操作到多操作的一致性保障SaveChanges本身就在一个事务里执行但如果一个流程涉及多个SaveChanges或者还要掺杂其它数据源操作就需要显式事务。EF Core写法using var db new AppDbContext(options); using var transaction await db.Database.BeginTransactionAsync(); try { db.Customers.Add(new Customer { Name 测试 }); await db.SaveChangesAsync(); // 这里可以再做另一件操作比如调用其他服务或者写文件 await SomeOtherOperationAsync(); await transaction.CommitAsync(); } catch { await transaction.RollbackAsync(); throw; }这里要理解不管SaveChanges执行几次如果没Commit那所有操作在事务结束时都会回滚。我在实际项目里用EF做工业数据采集经常需要把多个传感器数据写入同一批事务里如果中途某条数据异常整体回滚避免出现部分写入、数据库脏数据的情况。4.6 异步方法别把UI线程卡死在上位机或WinForms/WPF项目里异步非常重要。EF Core提供了所有异步版本SaveChangesAsync、ToListAsync、FirstAsync、ForEachAsync等。用异步方法能避免UI卡顿也能提升服务端吞吐量。写异步查询需要注意的一点异步LINQ运算符和同步版不能混用。例如不能在一个IQueryable上用await去遍历也不能在ToListAsync返回前用First同步而不管异步上下文。始终从数据库提供的DbSet开始用异步的ToListAsync、FirstOrDefaultAsync等。热词里提到“C#多线程”如果我做数据采集程序会开多个Task并发采集数据并分别通过同一个DbContext不行DbContext不是线程安全不能在多线程里共用同一个实例。正确做法每个线程/任务里创建新DbContext或者使用IDbContextFactory来创建。EF Core内置了AddDbContextFactory注册专门解决这类问题非常有用。5. 进阶技巧用日志、依赖注入、迁移和性能优化让项目更稳5.1 在EF Core里接入NLog或其他日志框架你搜“c#如何用nlog”正好和EF日志能结合。开发时把SQL输出到控制台没问题生产环境最好集成到NLog/Serilog。EF Core提供了LogTo重载可以注入一个logger对象也可以自己封装ILoggerProvider。最省事的做法是不直接绑定某个框架而是把日志事件转发到自己的全局日志队列。我自己的经验是生产环境把EF日志级别调成Information记录SQL会严重影响性能和磁盘IO一般只有在排查问题时临时开启平时用Warning级别就够了。推荐在配置文件中用开关控制。5.2 泛型仓储到底要不要用网上很多EF教程都喜欢包装一个IRepository 为的是方便替换数据库和单元测试。但EF Core本身的DbContext已经提供了类似仓储的能力再加一层往往就是“为了用而用”。我的态度是简单的CRUD项目直接用DbContext别套仓储层只有业务规则复杂、需要集中处理查询条件的地方才做一个查询服务或者规范模式。太多层间接层不仅增加代码量还会让调试SQL变成灾难尤其是当你往下追了两层才发现原查询是拼接出来的。5.3 使用迁移管理生产数据库而不是每次重新建库生产环境数据库升级是很谨慎的。迁移分两步第一步先创建迁移文件migrations add XXX第二步在发布时执行database update脚本或者使用dotnet ef database update --connection来部署。还有更进阶的做法利用EF Core的脚本生成功能把迁移翻译成纯SQL脚本交给DBA审核后执行dotnet ef migrations script InitialCreate LatestMigration -o upgrade.sql这个方式在生产环境里特别靠谱避免在服务器上再装dotnet环境。5.4 性能优化延迟加载、即时加载还是显式加载EF Core默认不带延迟加载Lazy Loading需要加包Microsoft.EntityFrameworkCore.Proxies并在配置时UseLazyLoadingProxies()。但我非常不推荐一来就开懒加载因为它在遍历时会逐条发SQL查询遇到N1查询问题性能瞬间崩。更推荐用Include做聚合查询或者用Select投影成DTO。先有查询优化意识比什么都重要。打开日志后你要能够识别出N1日志里出现几百条几乎一样的SELECT语句多半就是导航属性触发的懒加载或缺乏Include。解决方法一般在Include或Select里做文章。5.5 并发控制一份数据多人同时改怎么处理在业务系统里两个人同时编辑同一条数据是很常见的情况。EF Core提供了并发令牌机制在实体属性上加[ConcurrencyCheck]或配置IsConcurrencyToken。例如用户表里有一个RowVersion字段public class Customer { public int Id { get; set; } public string Name { get; set; } public byte[] RowVersion { get; set; } }配置modelBuilder.EntityCustomer() .Property(c c.RowVersion) .IsRowVersion();这样在更新时EF会把RowVersion作为WHERE条件之一如果数据库里这一行已被别人改过RowVersion不匹配SaveChanges就会抛出DbUpdateConcurrencyException。捕获它重新读取最新数据再让用户选择覆盖或重试。这个方法在工业数据采集这类写多读少场景同样有用多个线程或进程同时写同一条记录时用并发令牌能有效避免最后写入覆盖前一次数据的情况。6. 常见问题排查与实操避坑6.1 安装EF工具时遇到“No project was found”怎么办执行dotnet ef命令常会找不到项目原因是EF工具需要设计时DbContext。先确保安装了Microsoft.EntityFrameworkCore.Design包然后在项目目录下执行。如果项目是解决方案里多个项目要指出来dotnet ef migrations add Init --project src/Demo.Data --startup-project src/Demo.Web我踩过的坑是只装包忘了在Program.cs里配置服务导致EF工具无法构造出DbContext。检查一下是否真的通过UseSqlServer/UseSqlite调用过连接配置。6.2 连接字符串中的密码传到数据库时报错连接字符串写错是最常见的。SQL Server推荐用TrustServerCertificateTrueSQLite注意路径的写法和权限尤其Windows服务里访问相对路径时工作目录可能不在预期位置。建议连接字符串使用ConfigurationBuilder读配置文件而不要在代码里硬编码。6.3 查询结果不按预期返回查下SQL日志不少朋友遇到“我明明过滤了NULL怎么还查出来了”常见原因是空值判断写法有问题。在EF中判断可空字段为空可以这样.Where(c c.Age ! null)这会翻译成IS NOT NULL如果写c.Age 18NULL记录会自动被过滤掉数据库的语义。这和C#里不null安全判定的直觉不同初学容易被坑。6.4 ExecuteDelete在某些数据库提供程序上不支持EF Core的ExecuteUpdate/Delete需要提供程序支持SQL Server和SQLite近期版本都支持了但老版本EF Core 6没有。我建议看报错信息如果不支持就退回老办法查出来删。一般来说升级到EF Core 8后问题会少很多。6.5 DbContext生命周期管理错误ASP.NET Core里DbContext默认作用域生命周期是Scoped一个请求一个实例控制台程序里要自己管理using。腰以下这些我都踩过在单例服务里注入DbContext导致线程安全问题在后台服务里每次都new一个DbContext导致连接池耗尽。正确做法短生命周期用完就释放要么用IDbContextFactory。在上位机场景多线程采集时千万别跨线程用同一个DbContext。我习惯用一个专门的DatabaseService通过lock或者单线程任务队列来执行数据写入或者用IDbContextFactory加一个using创建独立的上下文实例。6.6 迁移时如何保留已有数据如果你已经有生产数据修改模型后不要直接DropDatabase要创建新的迁移来执行ALTER TABLE。如果只是加字段EF Core迁移会生成AddColumn如果加了非空字段又没有默认值迁移会失败你需要在迁移文件或者模型配置里给默认值。解决办法是在迁移代码里添加defaultValue参数或者先把列设为可空。这类问题在自动化部署前一定要在测试环境模拟一遍。6.7 看待“热词”里涉及的话题EF和反射、委托、字符串截取的关系你搜到的热词里很多是C#基础问题比如反射、委托、数组和集合的区别。这些和EF本身关系不大但工作里确实会结合比如用反射动态读取实体类的属性用于生成通用查询条件用委托在EF的日志或查询管道里做扩展。我的建议是先学扎实基础再来玩EF的高级玩法。切忌在不会LINQ和匿名类型之前就陷入修改EF源码的内部陷阱。7. 一个完整示例用EF Core做一个SQLite数据记录器这个示例可以贴合很多场景比如你的上位机需要定时从仪表读取数据并写入本地数据库。我在这里演示一个控制台程序的基本框架实体定义public class SensorRecord { public int Id { get; set; } public DateTime Timestamp { get; set; } public string SensorName { get; set; } public double Value { get; set; } }DbContextpublic class SensorContext : DbContext { public DbSetSensorRecord Records { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) optionsBuilder.UseSqlite(Data Sourcesensor.db); }写入逻辑模拟多线程采集入口using var scope new SensorContext(); scope.Records.Add(new SensorRecord { Timestamp DateTime.Now, SensorName temp_1, Value 25.6 }); await scope.SaveChangesAsync();查询最近的记录await using var db new SensorContext(); var last await db.Records .OrderByDescending(r r.Timestamp) .Take(10) .Select(r new { r.Timestamp, r.Value }) .ToListAsync();就这么简单你已经能跑通一个本地记录系统。实际生产还要考虑缓冲区、批量写入、异常重试等但数据访问层已经足够清晰了。在这类场景里我最终把EF Core的写法固定成了一套模式建立独立的Repository或者直接用DbContext配合简单Service写入时把多个传感器数据攒在一起后一次性SaveChanges减少事务数读取时统一使用AsNoTracking Select投影减少上下文跟踪负担。8. 写在最后的几点个人经验第一点EF Core不是银弹。当你遇到了需要极高性能、复杂SQL、报表多维分析的场景Dapper或原生ADO.NET仍是更顺手的选择。不要因为是教程推荐EF就拿着它硬造轮子。反而应该把它放在“业务开发效率”和“常规CRUD”这两个词的位置上。第二点学习和调试EF Core时一定要把日志打开。如果不打开日志你根本不知道EF替你做了什么SQL很多问题因为黑盒就够你排查半天。我身边所有用过EF一段时间的朋友最后都会养成先开日志、再看异常的习惯。第三点涉及连接数据库的工控/上位机场景数据库选型要三思。如果只是单机记录SQLite最省心如果有多客户端同时写要上性格更好的数据库如果通信数据量大又需要锁机制EF Core配合SQL Server或PostgreSQL也能扛住。但别忘了采集服务的稳定性和数据落地的时序正确性往往比ORM本身更重要。第四点数据库迁移这个东西越早使用越划算。很多小项目一开始手动画表后面项目一上线再改表结构时心惊胆战。所以从建项目的第一天起就用Migrations管理表结构哪怕很简单。不要犯我早期犯过的错等生产数据一堆了才想着引入迁移那时的痛苦你想象不到。这篇教程该讲的都讲了剩下的就是动手敲。拿一个小项目、一张表开始把增删改查跑通再把迁移和日志打开。一点点积累你很快就能从“CtrlC、CtrlV”过渡到能独立设计一套合理的数据访问层。我当年就是一边翻文档一边试踩了大半年的坑才把EF Core用得顺手。你现在有这篇梳理过的经验相信能走得更快。
返回列表