1. EFCore数据库操作概述
作为一名.NET开发者,我使用Entity Framework Core(简称EFCore)进行数据库操作已经有五年多的时间。EFCore是微软推出的轻量级、可扩展、跨平台的对象关系映射(ORM)框架,它让我们可以用面向对象的方式操作数据库,而不用直接编写SQL语句。在实际项目中,EFCore极大地提高了开发效率,特别是在快速迭代的业务场景中。
EFCore支持多种数据库引擎,包括SQL Server、MySQL、PostgreSQL、SQLite等,这使得它成为.NET生态中最受欢迎的数据库访问技术之一。通过DbContext这个核心类,我们可以定义数据模型、执行查询、跟踪变更并保存到数据库。EFCore还提供了强大的迁移功能,可以随着业务需求的变化而演进数据库架构。
提示:虽然EFCore简化了数据库操作,但理解其底层工作原理对于编写高效、可靠的代码至关重要。特别是在处理复杂查询或大数据量时,了解EFCore如何将LINQ转换为SQL可以帮助我们避免性能陷阱。
2. EFCore核心概念与配置
2.1 DbContext与DbSet
DbContext是EFCore的核心,它代表与数据库的会话,负责:
- 管理数据库连接
- 跟踪实体变更
- 执行数据库操作
- 提供LINQ查询支持
一个典型的DbContext定义如下:
public class AppDbContext : DbContext { public DbSet<Product> Products { get; set; } public DbSet<Category> Categories { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=MyShop;Trusted_Connection=True;"); } }每个DbSet属性对应数据库中的一个表。EFCore通过约定优于配置的原则,自动推断表名、主键等元数据,同时也支持通过Fluent API或数据注解进行自定义配置。
2.2 实体配置方式
EFCore提供了三种配置实体和关系的方式:
- 约定:默认行为,如Id属性自动成为主键
- 数据注解:通过特性标注,如[Key]、[Required]
- Fluent API:在DbContext的OnModelCreating方法中配置
对于复杂模型,我推荐使用Fluent API,因为它提供了最灵活和集中的配置方式:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Product>() .HasKey(p => p.ProductId); modelBuilder.Entity<Product>() .Property(p => p.Name) .IsRequired() .HasMaxLength(100); modelBuilder.Entity<Product>() .HasOne(p => p.Category) .WithMany(c => c.Products) .HasForeignKey(p => p.CategoryId); }3. 基本CRUD操作
3.1 添加数据
添加新记录非常简单,只需创建实体实例并添加到对应的DbSet:
using (var context = new AppDbContext()) { var category = new Category { Name = "Electronics" }; context.Categories.Add(category); var product = new Product { Name = "Smartphone", Price = 599.99m, Category = category }; context.Products.Add(product); int recordsAffected = context.SaveChanges(); Console.WriteLine($"Saved {recordsAffected} records."); }注意:SaveChanges()是原子操作,要么全部成功,要么全部回滚。对于大批量插入,考虑使用AddRange()和批量操作优化。
3.2 查询数据
EFCore支持强大的LINQ查询功能:
// 基本查询 var cheapProducts = context.Products .Where(p => p.Price < 100) .ToList(); // 包含关联数据 var productsWithCategory = context.Products .Include(p => p.Category) .ToList(); // 投影查询 var productNames = context.Products .Select(p => new { p.Name, p.Price }) .ToList();3.3 更新数据
更新操作遵循"查询-修改-保存"模式:
var product = context.Products.Find(1); if (product != null) { product.Price *= 1.1m; // 涨价10% context.SaveChanges(); }3.4 删除数据
删除同样需要先加载实体:
var product = context.Products.Find(1); if (product != null) { context.Products.Remove(product); context.SaveChanges(); }或者使用EntityState直接标记删除:
var product = new Product { ProductId = 1 }; context.Entry(product).State = EntityState.Deleted; context.SaveChanges();4. 高级查询技术
4.1 延迟加载与立即加载
EFCore支持两种加载关联数据的方式:
- 立即加载(Eager Loading):使用Include方法预先加载关联数据
var orders = context.Orders .Include(o => o.Customer) .Include(o => o.OrderItems) .ThenInclude(oi => oi.Product) .ToList();- 延迟加载(Lazy Loading):访问导航属性时自动加载,需安装Microsoft.EntityFrameworkCore.Proxies包并启用:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseLazyLoadingProxies(); }提示:延迟加载可能导致N+1查询问题,在大数据量场景下应谨慎使用。
4.2 原始SQL查询
当LINQ无法满足复杂查询需求时,可以使用原始SQL:
var products = context.Products .FromSqlRaw("SELECT * FROM Products WHERE Price > {0}", 50) .ToList();对于存储过程:
var products = context.Products .FromSqlRaw("EXEC GetExpensiveProducts @minPrice={0}", 100) .ToList();4.3 全局查询过滤器
EFCore支持定义全局查询条件,这在多租户或软删除场景中特别有用:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Product>() .HasQueryFilter(p => !p.IsDeleted); }5. 性能优化技巧
5.1 批量操作
默认情况下,SaveChanges()对每个变更生成单独的SQL语句。使用AddRange()可以优化插入操作:
var products = new List<Product>(); for (int i = 0; i < 1000; i++) { products.Add(new Product { Name = $"Product {i}" }); } context.Products.AddRange(products); context.SaveChanges(); // 仍然生成1000条INSERT语句对于EF Core 5.0+,可以使用批量操作:
context.BulkInsert(products); // 使用第三方库如EFCore.BulkExtensions5.2 异步操作
所有数据库操作都有异步版本,适合Web应用:
var product = await context.Products.FindAsync(1); var products = await context.Products.ToListAsync(); await context.SaveChangesAsync();5.3 查询优化
- 只选择需要的字段:
var productNames = await context.Products .Select(p => p.Name) .ToListAsync();- 禁用变更跟踪:
var products = await context.Products .AsNoTracking() .ToListAsync();- 编译查询:
private static readonly Func<AppDbContext, int, Task<Product>> GetProductById = EF.CompileAsyncQuery((AppDbContext context, int id) => context.Products.FirstOrDefault(p => p.ProductId == id)); // 使用 var product = await GetProductById(context, 1);6. 事务管理
6.1 隐式事务
每个SaveChanges()调用自动创建一个事务:
using (var context = new AppDbContext()) { context.Products.Add(new Product { Name = "New Product" }); context.SaveChanges(); // 自动创建并提交事务 }6.2 显式事务
对于跨多个操作的事务:
using (var transaction = context.Database.BeginTransaction()) { try { context.Products.Add(new Product { Name = "Product A" }); context.SaveChanges(); context.Products.Add(new Product { Name = "Product B" }); context.SaveChanges(); transaction.Commit(); } catch { transaction.Rollback(); throw; } }6.3 分布式事务
在.NET Core 3.1+中,可以使用TransactionScope:
using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) { // 跨多个DbContext的操作 await context1.SaveChangesAsync(); await context2.SaveChangesAsync(); scope.Complete(); }7. 数据库迁移
7.1 创建迁移
安装EF Core工具后,通过命令行创建迁移:
dotnet ef migrations add InitialCreate这会生成迁移文件,包含Up(应用迁移)和Down(回滚迁移)方法。
7.2 应用迁移
可以通过代码或命令行应用迁移:
context.Database.Migrate();或命令行:
dotnet ef database update7.3 自定义迁移
对于复杂变更,可以手动编辑迁移文件:
protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AddColumn<decimal>( name: "Price", table: "Products", type: "decimal(10,2)", nullable: false, defaultValue: 0m); migrationBuilder.CreateIndex( name: "IX_Products_CategoryId", table: "Products", column: "CategoryId"); }8. 日志记录与诊断
8.1 配置日志记录
EFCore可以与各种日志框架集成:
optionsBuilder.UseSqlServer(connectionString) .LogTo(Console.WriteLine, LogLevel.Information) .EnableSensitiveDataLogging();8.2 性能诊断
使用Diagnostics监听器监控查询性能:
var listener = new DiagnosticListener("Microsoft.EntityFrameworkCore"); var subscription = listener.Subscribe(new MyDiagnosticListener()); public class MyDiagnosticListener : IObserver<KeyValuePair<string, object>> { public void OnNext(KeyValuePair<string, object> value) { if (value.Key == "Microsoft.EntityFrameworkCore.Database.Command.CommandExecuting") { var command = (string)value.Value.GetType().GetProperty("CommandText").GetValue(value.Value); Console.WriteLine($"Executing: {command}"); } } public void OnCompleted() {} public void OnError(Exception error) {} }9. 常见问题与解决方案
9.1 并发冲突
处理并发更新的两种方式:
- 乐观并发控制:
modelBuilder.Entity<Product>() .Property(p => p.RowVersion) .IsRowVersion();- 处理并发异常:
try { await context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException ex) { var entry = ex.Entries.Single(); var databaseValues = await entry.GetDatabaseValuesAsync(); var clientValues = entry.Entity as Product; // 解决冲突逻辑 }9.2 连接池问题
典型症状是连接耗尽错误。解决方案:
- 确保DbContext及时释放
- 增加连接池大小
- 减少连接存活时间
services.AddDbContextPool<AppDbContext>(options => options.UseSqlServer(connectionString, sqlOptions => sqlOptions.EnableRetryOnFailure()));9.3 查询性能问题
常见原因和解决方案:
| 问题 | 解决方案 |
|---|---|
| N+1查询 | 使用Include加载关联数据 |
| 大量数据 | 分页查询(.Skip().Take()) |
| 复杂查询 | 使用原始SQL或存储过程 |
| 缺少索引 | 通过迁移添加适当索引 |
10. 实际项目经验分享
在我最近的一个电商项目中,我们使用EFCore处理了每天数十万的订单。以下是一些关键经验:
批量插入优化:使用EFCore.BulkExtensions将订单明细批量插入,性能提升20倍。
读写分离:为DbContext配置不同的连接字符串,读操作使用只读副本。
查询缓存:对常用但不常变的数据(如产品分类)实现二级缓存。
DbContext生命周期:在Web应用中,通常将DbContext配置为Scoped生命周期,每个请求一个实例。
复杂查询分解:将复杂报表查询分解为多个较小查询,在内存中组合结果。
重要提示:EFCore虽然强大,但并非所有场景都适用。对于超高性能要求的场景,可能需要考虑Dapper或直接ADO.NET。关键是根据业务需求选择合适的技术。