EF Core 高级特性与性能优化
1. 索引
1.1 索引的基础
索引是提高数据库查询性能的关键。EF Core 允许你在实体类上定义各种类型的索引。
csharp
// 使用数据注解定义索引
public class Product
{
public int Id { get; set; }
[Index(nameof(Name))] // 在 Name 属性上创建索引
public string Name { get; set; }
[Index(nameof(CategoryId), nameof(Price))] // 复合索引
public int CategoryId { get; set; }
public decimal Price { get; set; }
}
// 使用 Fluent API 定义索引
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>()
.HasIndex(p => p.Name) // 单列索引
.HasDatabaseName("IX_Products_Name") // 设置索引名称
.IsUnique(); // 唯一索引
modelBuilder.Entity<Product>()
.HasIndex(p => new { p.CategoryId, p.Price }) // 复合索引
.HasFilter($"[{nameof(Product.IsActive)}] = 1"); // 筛选索引
}1.2 高级索引配置
csharp
// 唯一索引
modelBuilder.Entity<Product>()
.HasIndex(p => p.SerialNumber)
.IsUnique();
// 包含列的索引(EF Core 5.0+)
modelBuilder.Entity<Product>()
.HasIndex(p => p.CategoryId)
.IncludeProperties(p => new { p.Name, p.Price }); // 包含其他列以支持覆盖查询
// 筛选索引
modelBuilder.Entity<Product>()
.HasIndex(p => p.Price)
.HasFilter($"[{nameof(Product.IsActive)}] = 1 AND [{nameof(Product.Price)}] > 100");
// 索引排序方向(EF Core 5.0+)
modelBuilder.Entity<Order>()
.HasIndex(o => new { o.CustomerId, o.OrderDate })
.IsDescending(false, true); // CustomerId 升序,OrderDate 降序
// 最大长度索引(适用于字符串列)
modelBuilder.Entity<Product>()
.HasIndex(p => p.Name)
.HasDatabaseName("IX_Products_Name")
.HasMaxLength(100);2. 查询优化
2.1 使用 IQueryable 延迟执行
csharp
// 延迟执行查询直到调用 ToListAsync、FirstAsync 等
var query = context.Products.Where(p => p.Price > 100);
// 添加更多条件
query = query.Where(p => p.IsActive);
// 排序
query = query.OrderByDescending(p => p.CreatedAt);
// 现在执行查询
var products = await query.ToListAsync();2.2 非跟踪查询
对于只读操作,使用非跟踪查询可以显著提高性能。
csharp
// 使用 AsNoTracking
var products = await context.Products
.AsNoTracking()
.Where(p => p.Price > 100)
.ToListAsync();
// 全局禁用跟踪(不推荐,除非所有操作都是只读的)
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("connection string")
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
}
// 条件性使用跟踪或非跟踪
var query = context.Products.AsQueryable();
if (isReadOnly)
query = query.AsNoTracking();
var result = await query.ToListAsync();2.3 选择投影
只检索需要的数据,避免加载不必要的列。
csharp
// 使用匿名类型
var productSummaries = await context.Products
.Where(p => p.CategoryId == 1)
.Select(p => new
{
p.Id,
p.Name,
p.Price
})
.ToListAsync();
// 使用 DTO(数据传输对象)
var productDTOs = await context.Products
.Where(p => p.CategoryId == 1)
.Select(p => new ProductDTO
{
Id = p.Id,
Name = p.Name,
Price = p.Price,
CategoryName = p.Category.Name // 包含导航属性的特定列
})
.ToListAsync();2.4 分页查询
对于大型数据集,始终使用分页。
csharp
int pageNumber = 1;
int pageSize = 20;
var pagedProducts = await context.Products
.OrderBy(p => p.Name)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
// 计算总数
int totalCount = await context.Products.CountAsync();
int totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);2.5 避免 N+1 查询问题
N+1 查询问题指的是在加载包含导航属性的实体时,EF Core 先执行一个查询获取主实体(1次),然后为每个主实体单独执行查询获取关联实体(N次)。
csharp
// 问题代码:会导致 N+1 查询
var orders = await context.Orders.ToListAsync();
foreach (var order in orders)
{
// 每个 order 都会触发单独的查询
var customer = await context.Entry(order).Reference(o => o.Customer).LoadAsync();
}
// 解决方案1:使用 Include 预先加载
var ordersWithCustomers = await context.Orders
.Include(o => o.Customer)
.ToListAsync();
// 解决方案2:使用 ThenInclude 加载多级导航属性
var ordersDetails = await context.Orders
.Include(o => o.Customer)
.Include(o => o.OrderItems)
.ThenInclude(oi => oi.Product)
.ToListAsync();
// 解决方案3:使用 Select 投影(更灵活)
var orderDTOs = await context.Orders
.Select(o => new OrderDTO
{
OrderId = o.Id,
OrderDate = o.OrderDate,
CustomerName = o.Customer.Name,
Items = o.OrderItems.Select(oi => new OrderItemDTO
{
ProductName = oi.Product.Name,
Quantity = oi.Quantity,
Price = oi.Price
}).ToList()
})
.ToListAsync();3. 变更跟踪优化
3.1 禁用和启用变更跟踪
csharp
// 临时禁用自动检测更改
context.ChangeTracker.AutoDetectChangesEnabled = false;
try
{
// 批量操作
for (int i = 0; i < 1000; i++)
{
context.Products.Add(new Product { Name = $"产品{i}", Price = i * 10m });
}
// 手动触发变更检测
context.ChangeTracker.DetectChanges();
await context.SaveChangesAsync();
}
finally
{
// 确保恢复自动检测
context.ChangeTracker.AutoDetectChangesEnabled = true;
}3.2 清除跟踪实体
csharp
// 清除所有跟踪的实体
context.ChangeTracker.Clear();
// 清除特定类型的实体
context.Products.Local.Clear();
// 批量操作中定期清除以减少内存使用
for (int i = 0; i < 10000; i += 1000)
{
var batch = products.Skip(i).Take(1000);
context.Products.AddRange(batch);
await context.SaveChangesAsync();
context.ChangeTracker.Clear(); // 清除跟踪以释放内存
}3.3 只跟踪必要的属性
csharp
// 只跟踪特定属性
var product = context.Products.First();
context.Entry(product).State = EntityState.Unchanged;
context.Entry(product).Property(p => p.Name).IsModified = true;4. 缓存
4.1 内存缓存
EF Core 提供了内存缓存选项来存储频繁查询的结果。
csharp
// 在 Startup.cs 中配置内存缓存
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddMemoryCache();
services.AddScoped<IProductService, ProductService>();
}
// 在服务中使用缓存
public class ProductService : IProductService
{
private readonly ApplicationDbContext _context;
private readonly IMemoryCache _cache;
private const string ProductsCacheKey = "products_list";
private const int CacheDuration = 60; // 缓存60秒
public ProductService(ApplicationDbContext context, IMemoryCache cache)
{
_context = context;
_cache = cache;
}
public async Task<List<Product>> GetProductsAsync()
{
// 尝试从缓存获取
if (_cache.TryGetValue(ProductsCacheKey, out List<Product> products))
{
return products;
}
// 缓存未命中,查询数据库
products = await _context.Products.AsNoTracking().ToListAsync();
// 设置缓存
_cache.Set(ProductsCacheKey, products, TimeSpan.FromSeconds(CacheDuration));
return products;
}
// 数据更改时清除缓存
public async Task<Product> AddProductAsync(Product product)
{
_context.Products.Add(product);
await _context.SaveChangesAsync();
// 清除缓存
_cache.Remove(ProductsCacheKey);
return product;
}
}4.2 查询标记
EF Core 2.0+ 支持查询标记,可以将查询缓存起来。
csharp
// 使用查询标记
var products = await context.Products
.TagWith("获取活跃产品") // 添加标记,对缓存和日志有用
.Where(p => p.IsActive)
.ToListAsync();5. 视图映射
5.1 将实体映射到数据库视图
csharp
// 定义视图实体
public class ProductSummary
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public string CategoryName { get; set; }
public decimal AveragePrice { get; set; }
public int OrderCount { get; set; }
}
// 在 DbContext 中添加 DbSet
public DbSet<ProductSummary> ProductSummaries { get; set; }
// 在 OnModelCreating 中将实体映射到视图
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ProductSummary>(entity =>
{
entity.HasNoKey(); // 视图没有主键
entity.ToView("ProductSummariesView"); // 指定视图名称
});
}
// 查询视图
var productSummaries = await context.ProductSummaries
.Where(ps => ps.OrderCount > 10)
.OrderByDescending(ps => ps.AveragePrice)
.ToListAsync();5.2 创建数据库视图
通常,你需要在数据库迁移中创建视图。
csharp
// 在迁移文件中创建视图
protected override void Up(MigrationBuilder migrationBuilder)
{
// 创建视图
migrationBuilder.Sql(@"
CREATE VIEW ProductSummariesView AS
SELECT
p.Id AS ProductId,
p.Name AS ProductName,
c.Name AS CategoryName,
p.Price AS AveragePrice,
COUNT(oi.OrderId) AS OrderCount
FROM
Products p
JOIN Categories c ON p.CategoryId = c.Id
LEFT JOIN OrderItems oi ON p.Id = oi.ProductId
GROUP BY
p.Id, p.Name, c.Name, p.Price
");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// 删除视图
migrationBuilder.Sql("DROP VIEW ProductSummariesView");
}6. 存储过程
6.1 调用存储过程
csharp
// 调用返回实体的存储过程
var products = await context.Products
.FromSqlRaw("EXEC GetProductsByCategory @CategoryId = {0}", 1)
.ToListAsync();
// 调用不返回实体的存储过程
await context.Database.ExecuteSqlRawAsync(
"EXEC UpdateProductPrices @Percentage = {0}, @CategoryId = {1}",
10, // 10% 涨价
1 // 类别ID
);
// 使用参数化查询(更安全)
var categoryIdParam = new SqlParameter("CategoryId", 1);
var priceParam = new SqlParameter("MinPrice", 100);
var products = await context.Products
.FromSqlRaw("EXEC GetProductsByCategoryAndPrice @CategoryId, @MinPrice",
categoryIdParam, priceParam)
.ToListAsync();
// 使用 DbSet.FromSqlInterpolated(简化语法)
int categoryId = 1;
decimal minPrice = 100;
var products = await context.Products
.FromSqlInterpolated($"EXEC GetProductsByCategoryAndPrice {categoryId}, {minPrice}")
.ToListAsync();6.2 使用存储过程进行CRUD操作
csharp
// 创建实体时使用存储过程
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>()
.ToTable("Products")
.HasKey(p => p.Id);
// 使用存储过程进行插入
modelBuilder.Entity<Product>()
.HasDataAccessKind(DataAccessKind.ReadWrite)
.ToFunction("InsertProduct");
}
// 或者使用原始SQL调用存储过程进行插入
var nameParam = new SqlParameter("@Name", "新产品");
var priceParam = new SqlParameter("@Price", 199.99);
var categoryIdParam = new SqlParameter("@CategoryId", 1);
var idParam = new SqlParameter("@Id", SqlDbType.Int) { Direction = ParameterDirection.Output };
await context.Database.ExecuteSqlRawAsync(
"EXEC InsertProduct @Name, @Price, @CategoryId, @Id OUTPUT",
nameParam, priceParam, categoryIdParam, idParam);
int newProductId = (int)idParam.Value;7. 表拆分和继承
7.1 表拆分
表拆分允许将一个实体类型映射到多个表。
csharp
// 主实体
public class Order
{
public int Id { get; set; }
public DateTime OrderDate { get; set; }
public int CustomerId { get; set; }
public OrderDetails Details { get; set; } // 关联到拆分的表
}
// 拆分实体
public class OrderDetails
{
public int OrderId { get; set; }
public string ShippingAddress { get; set; }
public string BillingAddress { get; set; }
public string TrackingNumber { get; set; }
public Order Order { get; set; } // 反向导航
}
// 配置表拆分
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>()
.HasOne(o => o.Details)
.WithOne(d => d.Order)
.HasForeignKey<OrderDetails>(d => d.OrderId);
// 将两个实体映射到不同的表
modelBuilder.Entity<Order>().ToTable("Orders");
modelBuilder.Entity<OrderDetails>().ToTable("OrderDetails");
}
// 使用表拆分查询
var orders = await context.Orders
.Include(o => o.Details)
.ToListAsync();7.2 继承映射
EF Core 支持三种继承映射策略:TPH(每个层次结构一张表)、TPT(每个类型一张表)和 TPC(每个具体类型一张表)。
7.2.1 TPH (Table Per Hierarchy)
所有继承层次结构中的类型都映射到单个表,使用鉴别器列来区分不同的类型。
csharp
// 基类
public abstract class Payment
{
public int Id { get; set; }
public decimal Amount { get; set; }
public DateTime PaymentDate { get; set; }
public string PaymentStatus { get; set; }
}
// 派生类
public class CreditCardPayment : Payment
{
public string CardNumber { get; set; }
public string CardholderName { get; set; }
public string ExpirationDate { get; set; }
}
public class PayPalPayment : Payment
{
public string Email { get; set; }
public string TransactionId { get; set; }
}
// 配置 TPH 继承(EF Core 默认)
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Payment>()
.HasDiscriminator<string>("PaymentType") // 指定鉴别器列
.HasValue<CreditCardPayment>("CreditCard") // 指定值
.HasValue<PayPalPayment>("PayPal");
}
// 查询所有支付
var payments = await context.Payments.ToListAsync();
// 查询特定类型的支付
var creditCardPayments = await context.Payments
.OfType<CreditCardPayment>()
.Where(p => p.CardholderName.Contains("Smith"))
.ToListAsync();7.2.2 TPT (Table Per Type)
每个类型映射到单独的表,基类表包含共享属性,派生类表包含特定属性和外键。
csharp
// 配置 TPT 继承
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Payment>().ToTable("Payments");
modelBuilder.Entity<CreditCardPayment>().ToTable("CreditCardPayments");
modelBuilder.Entity<PayPalPayment>().ToTable("PayPalPayments");
}8. 拦截器
8.1 查询拦截器
csharp
// 创建拦截器类
public class LoggingInterceptor : DbCommandInterceptor
{
private readonly ILogger<LoggingInterceptor> _logger;
public LoggingInterceptor(ILogger<LoggingInterceptor> logger)
{
_logger = logger;
}
public override InterceptionResult<DbDataReader> ReaderExecuting(
DbCommand command,
CommandEventData eventData,
InterceptionResult<DbDataReader> result)
{
_logger.LogInformation("执行查询: {sql}", command.CommandText);
return result;
}
public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
DbCommand command,
CommandEventData eventData,
InterceptionResult<DbDataReader> result,
CancellationToken cancellationToken = default)
{
_logger.LogInformation("执行异步查询: {sql}", command.CommandText);
return ValueTask.FromResult(result);
}
public override InterceptionResult<int> NonQueryExecuting(
DbCommand command,
CommandEventData eventData,
InterceptionResult<int> result)
{
_logger.LogInformation("执行非查询命令: {sql}", command.CommandText);
return result;
}
}
// 在配置中注册拦截器
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("connection string")
.AddInterceptors(new LoggingInterceptor(new Logger<LoggingInterceptor>(new LoggerFactory())));
}
// 或者在 Startup.cs 中注册
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))
.AddInterceptors(new LoggingInterceptor(new Logger<LoggingInterceptor>(new LoggerFactory()))));
}8.2 事务拦截器
csharp
public class TransactionInterceptor : DbTransactionInterceptor
{
private readonly ILogger<TransactionInterceptor> _logger;
public TransactionInterceptor(ILogger<TransactionInterceptor> logger)
{
_logger = logger;
}
public override void TransactionStarting(
DbConnection connection,
TransactionEventData eventData,
TransactionStartingEventData interceptionData)
{
_logger.LogInformation("开始事务");
}
public override void TransactionCommitted(
DbConnection connection,
TransactionEndEventData eventData)
{
_logger.LogInformation("事务提交成功");
}
public override void TransactionFailed(
DbConnection connection,
TransactionErrorEventData eventData)
{
_logger.LogError(eventData.Exception, "事务失败");
}
public override void TransactionRolledBack(
DbConnection connection,
TransactionEndEventData eventData)
{
_logger.LogInformation("事务回滚");
}
}9. 性能分析工具
9.1 使用 EF Core Profiler
EF Core Profiler 是一个商业工具,可以帮助你监控和优化 EF Core 的性能。
9.2 使用内置日志
csharp
// 在配置中启用详细日志
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("connection string")
.LogTo(Console.WriteLine, LogLevel.Information) // 输出到控制台
.EnableSensitiveDataLogging(); // 启用敏感数据日志(开发环境)
}9.3 使用 SQL Server Profiler
对于 SQL Server 数据库,可以使用 SQL Server Profiler 来监控生成的 SQL 查询。
9.4 使用 DbContext.Database.GenerateCreateScript()
csharp
// 生成创建数据库的脚本
using var context = new ApplicationDbContext();
string script = context.Database.GenerateCreateScript();
Console.WriteLine(script);10. 最佳实践
10.1 性能优化最佳实践
- 使用异步操作:优先使用
Async方法,避免阻塞线程 - 避免加载不必要的数据:使用投影和
Select只检索需要的列 - 合理使用索引:为频繁查询的列添加适当的索引
- 优化查询:避免 N+1 查询,使用
Include和ThenInclude预先加载相关数据 - 使用非跟踪查询:对于只读操作,使用
AsNoTracking - 分页处理大数据集:始终对大型结果集进行分页
- 合理配置 DbContext:避免在长生命周期内保持 DbContext 实例
- 使用缓存:缓存频繁访问且变化不频繁的数据
- 优化批量操作:对于大量数据操作,考虑使用 ExecuteUpdate/ExecuteDelete 或第三方库
- 定期监控性能:使用日志和分析工具监控查询性能
10.2 设计最佳实践
- 合理设计模型:遵循领域驱动设计原则,保持实体简洁
- 使用适当的数据类型:选择合适的数据类型以减少存储空间
- 避免深层导航:尽量避免复杂的多级导航属性
- 考虑并发控制:在多用户环境中实现适当的并发控制机制
- 遵循命名约定:使用一致的命名约定,便于理解和维护
11. 小结
本章详细介绍了 EF Core 的高级特性和性能优化技术,包括:
- 索引:创建和配置各种类型的索引以提高查询性能
- 查询优化:使用延迟执行、非跟踪查询、投影和分页等技术优化查询
- 变更跟踪优化:通过禁用自动检测更改、清除跟踪实体等方式优化变更跟踪
- 缓存:使用内存缓存和查询标记缓存查询结果
- 视图映射:将实体映射到数据库视图
- 存储过程:调用和使用存储过程
- 表拆分和继承:实现表拆分和各种继承映射策略
- 拦截器:创建和使用查询拦截器和事务拦截器
- 性能分析工具:使用各种工具分析和监控性能
- 最佳实践:遵循性能优化和设计最佳实践
通过掌握这些高级特性和优化技术,你可以构建高性能、可伸缩的应用程序,充分发挥 EF Core 的强大功能。