我依然记得第一次抛弃ADO.NET转向Entity Framework的场景。那种感觉就像从打字机换成了MacBook——抽象层级、优雅设计、生产力飙升,EF带来的清新空气令人陶醉。但随着EF进化到EF Core,这种抽象逐渐变成了双刃剑。好用吗?绝对好用。可预测吗?未必总是。
当EF Core首次发布时,我正为某物流公司重构单体架构转微服务项目。这个遗留系统堪称噩梦——到处都是存储过程、深度嵌套的联表查询,领域模型与数据层完全纠缠不清。
我们决定用.NET Core重写模块时,EF Core似乎是顺理成章的选择。但这个选择绝非简单的工具切换,而是持久化思维模式的根本转变。
第一个震撼:EF Core不是EF6的移植版,而是彻底重写。那些你以为理所当然的特性——延迟加载、复杂查询转换、甚至简单的Include链——都可能出现意外行为。
早期教训告诉我:必须理解EF Core的内部机制。不能盲目相信它的查询优化能力。即便在写LINQ时,也要保持SQL思维。比如这个看似无害的查询:
var orders = await _context.Orders
.Include(o => o.Customer)
.Where(o => o.Date > DateTime.Now.AddDays(-30))
.Select(o => new { o.Id, o.Total, o.Customer.Name })
.ToListAsync();
生产环境却产生了30秒的查询延迟,生成的SQL包含数十个JOIN和子查询。关键教训:永远用ToQueryString()
检查实际SQL。现在我立下规矩:每个新查询都必须审查原始SQL。甚至专门写了中间件来记录超阈值的查询。
EF Core默认会追踪所有提取的实体。多数情况下这很美好,直到我们加载1200条订单记录进行只读处理时,内存暴增导致容器重启。解决方案?一个简单的.AsNoTracking()
减少70%内存占用。从此我们确立原则:不修改就禁用追踪。
后来我构建了轻量仓储层,默认采用.AsNoTracking()
,将变更追踪变为显式选项。这个微调在高吞吐系统中效果显著。
在多人共用一个DbContext的团队中,迁移文件的合并冲突堪称噩梦。因为操作顺序至关重要,错位的AlterColumn
或缺失的外键约束都会破坏迁移管道。
我们最终采用隔离迁移分支策略,仅在PR合并后生成模型快照。在严格监管环境中,我们甚至考虑用Flyway/Liquibase手动管理SQL脚本。核心认知:EF Core提供工具,但不提供流程规范。
新手常犯的错误是像操作内存集合那样写LINQ:嵌套循环、条件中的投影、中途调用ToList()
。这些代码在本地运行良好,却在集成测试时崩溃。应对策略:将LINQ视为SQL而非内存操作。每个查询都要自问:"这能用SQL表达吗?"
EF Core对值对象(Owned Types)的支持很出色,但需要精细配置。我们在银行项目中大量使用值对象(货币、税号、国际代码),但调试模型绑定问题成了常态。教训:一个错误配置就能让整个模型对EF不可读。
影子属性(如LastUpdated
)虽然方便,但缺乏编译时检查。我们曾因删除影子属性导致软删除功能静默失败。建议:优先使用显式字段,审计追踪这类横切关注点考虑用中间件实现。
EF Core采用乐观并发控制(版本号/时间戳),但优雅处理并发异常绝非易事。我们遇到过订单并行编辑导致的DbUpdateConcurrencyException
,最终不得不构建自定义冲突解决流程。
事务支持很完善,但跨DbContext或跨数据源时需要引入分布式事务。在某些服务中,我们最终采用Outbox模式确保消息与数据库的原子性。
关于单元测试的争论从未停止:内存提供程序?SQLite?真实SQL Server?我都试过。最终方案:在Docker中启动SQL Server测试实例。虽然笨重,但能确保测试环境与生产一致。
对于模拟,过去用Moq伪造DbSet<T>
,现在更倾向于依赖接口设计,让测试只关注行为而非EF实现。
// 反模式:产生N+1查询
var orders = await _context.Orders.ToListAsync();
foreach (var order in orders)
{
var items = await _context.OrderItems
.Where(i => i.OrderId == order.Id)
.ToListAsync();
}
优化为急加载后,API延迟从4800ms降至400ms:
var ordersWithItems = await _context.Orders
.Include(o => o.OrderItems)
.ToListAsync();
// 错误:先加载15万条记录再过滤
var orders = _context.Orders.ToList();
var filtered = orders.Where(o => o.Status == OrderStatus.Pending);
// 正确:数据库端过滤
var filtered = await _context.Orders
.Where(o => o.Status == OrderStatus.Pending)
.ToListAsync();
这个优化将内存占用从600MB降至40MB。
EF Core 8之前缺乏原生批量支持,第三方库或原生SQL成为必选:
// 低效方案
var users = await _context.Users.Where(u => u.IsInactive).ToListAsync();
foreach (var user in users) user.IsDeleted = true;
await _context.SaveChangesAsync(); // 产生N条UPDATE
// 高效方案
await _context.Database.ExecuteSqlRawAsync(
"UPDATE Users SET IsDeleted = 1 WHERE IsInactive = 1");
某审计清理任务执行时间从14秒缩短至300ms。
SaveChanges()
的隐式事务仅适用于单次操作。跨操作必须显式控制:
using var transaction = await _context.Database.BeginTransactionAsync();
try {
// 库存扣减
var stock = await _context.Stock.FirstOrDefaultAsync(...);
stock.Quantity -= order.Quantity;
// 发票创建
_context.Invoices.Add(new Invoice { ... });
await _context.SaveChangesAsync();
await transaction.CommitAsync();
}
catch {
await transaction.RollbackAsync();
throw;
}
| 场景 | 优化前 | 优化后 | 提升幅度 | |---------------------|----------|----------|--------| | 分页查询(500条) | 1300ms | 250ms | 5.2x | | 批量更新(2万条) | 14000ms | 300ms | 46x | | 高并发订单提交 | 38%失败率 | 0.2%失败率 | 190x |
.Select(o => new OrderDto { Id = o.Id, ... })
_context.ChangeTracker.AutoDetectChangesEnabled = false;
options.UseSqlServer(connectionString, opts =>
opts.CommandTimeout(60).MaxPoolSize(200));
EF Core可以成为高性能数据访问层,也可能悄然成为系统瓶颈。真正的性能提升不在于缓存或服务器调优,而在于像数据库工程师那样编写查询,将EF Core视为带类型安全的SQL生成器。
对于事务,不要等到系统崩溃才理解原子性的真谛。设计原则:
在云原生时代,EF Core仍是值得信赖的战友——只要你足够了解它的脾性。