EF Core 保存数据与事务处理
1. 基本的 CRUD 操作
1.1 创建数据(Create)
csharp
// 创建单个实体
using var context = new ApplicationDbContext();
var newProduct = new Product
{
Name = "新产品",
Price = 99.99m,
CategoryId = 1,
IsActive = true,
CreatedAt = DateTime.Now
};
context.Products.Add(newProduct);
var result = await context.SaveChangesAsync();
Console.WriteLine($"保存成功,影响了 {result} 条记录");
Console.WriteLine($"新创建的产品ID: {newProduct.Id}"); // ID 会被自动回填
// 创建多个实体
var newProducts = new List<Product>
{
new Product { Name = "产品A", Price = 199.99m, CategoryId = 1 },
new Product { Name = "产品B", Price = 299.99m, CategoryId = 2 },
new Product { Name = "产品C", Price = 399.99m, CategoryId = 1 }
};
context.Products.AddRange(newProducts);
await context.SaveChangesAsync();
// 使用实体状态设置
var product = new Product { Name = "手动添加", Price = 499.99m };
context.Entry(product).State = EntityState.Added;
await context.SaveChangesAsync();1.2 读取数据(Read)
csharp
// 根据主键查询
var product = context.Products.Find(1);
// 条件查询
var expensiveProducts = context.Products
.Where(p => p.Price > 100)
.ToList();
// 异步查询
var recentProducts = await context.Products
.Where(p => p.CreatedAt > DateTime.Now.AddDays(-30))
.ToListAsync();
// 包含相关数据
var productsWithCategories = context.Products
.Include(p => p.Category)
.ToList();1.3 更新数据(Update)
csharp
// 方法1:先查询再更新(推荐)
using var context = new ApplicationDbContext();
var product = await context.Products.FindAsync(1);
if (product != null)
{
product.Name = "更新后的产品名称";
product.Price = 129.99m;
product.LastUpdated = DateTime.Now;
// EF Core 会自动跟踪更改
var result = await context.SaveChangesAsync();
Console.WriteLine($"更新成功,影响了 {result} 条记录");
}
// 方法2:使用 Update 方法(适用于分离的实体)
var detachedProduct = new Product { Id = 2, Name = "分离实体", Price = 199.99m };
context.Products.Update(detachedProduct);
await context.SaveChangesAsync();
// 方法3:更新多个实体
var productsToUpdate = await context.Products
.Where(p => p.CategoryId == 1)
.ToListAsync();
foreach (var p in productsToUpdate)
{
p.Price *= 1.1m; // 涨价10%
}
await context.SaveChangesAsync();
// 方法4:使用 Entry API 更新特定属性
var productEntry = context.Entry(product);
productEntry.Property(p => p.Price).CurrentValue = 159.99m;
// 也可以只更新部分属性,其他保持不变
productEntry.State = EntityState.Unchanged;
productEntry.Property(p => p.Price).IsModified = true;
await context.SaveChangesAsync();1.4 删除数据(Delete)
csharp
// 方法1:先查询再删除(推荐)
using var context = new ApplicationDbContext();
var product = await context.Products.FindAsync(1);
if (product != null)
{
context.Products.Remove(product);
var result = await context.SaveChangesAsync();
Console.WriteLine($"删除成功,影响了 {result} 条记录");
}
// 方法2:使用 Remove 方法(适用于分离的实体)
var detachedProduct = new Product { Id = 2 }; // 只需要主键
context.Products.Remove(detachedProduct);
await context.SaveChangesAsync();
// 方法3:删除多个实体
var productsToDelete = await context.Products
.Where(p => p.IsActive == false && p.LastUpdated < DateTime.Now.AddYears(-1))
.ToListAsync();
context.Products.RemoveRange(productsToDelete);
await context.SaveChangesAsync();
// 方法4:使用实体状态设置
var productToDelete = new Product { Id = 3 };
context.Entry(productToDelete).State = EntityState.Deleted;
await context.SaveChangesAsync();2. 批量操作
2.1 批量添加
csharp
// 添加大量数据
using var context = new ApplicationDbContext();
// 分批添加以避免内存问题
var batchSize = 1000;
var allProducts = GetLargeProductList(); // 假设这是一个返回大量产品的方法
for (int i = 0; i < allProducts.Count; i += batchSize)
{
var batch = allProducts.Skip(i).Take(batchSize).ToList();
context.Products.AddRange(batch);
await context.SaveChangesAsync();
// 重置 DbContext 以释放内存
context.ChangeTracker.Clear();
}2.2 批量更新
csharp
// 批量更新特定条件的记录
// 方法1:使用循环(适用于中小规模数据)
var products = context.Products
.Where(p => p.CategoryId == 1)
.ToList();
foreach (var product in products)
{
product.Price *= 1.1m;
}
await context.SaveChangesAsync();
// 方法2:使用 ExecuteUpdate(EF Core 7.0+)- 性能更好
var affectedRows = await context.Products
.Where(p => p.CategoryId == 1)
.ExecuteUpdateAsync(s => s
.SetProperty(p => p.Price, p => p.Price * 1.1m)
.SetProperty(p => p.LastUpdated, DateTime.Now));
Console.WriteLine($"更新了 {affectedRows} 条记录");2.3 批量删除
csharp
// 方法1:使用 RemoveRange(适用于中小规模数据)
var productsToDelete = context.Products
.Where(p => p.IsActive == false)
.ToList();
context.Products.RemoveRange(productsToDelete);
await context.SaveChangesAsync();
// 方法2:使用 ExecuteDelete(EF Core 7.0+)- 性能更好
var affectedRows = await context.Products
.Where(p => p.IsActive == false && p.LastUpdated < DateTime.Now.AddYears(-1))
.ExecuteDeleteAsync();
Console.WriteLine($"删除了 {affectedRows} 条记录");3. 事务管理
3.1 自动事务
EF Core 的 SaveChanges 方法默认在单个事务中执行所有更改。
csharp
using var context = new ApplicationDbContext();
// 所有这些操作都在同一个事务中执行
var order = new Order { CustomerId = 1, OrderDate = DateTime.Now };
context.Orders.Add(order);
var orderItem1 = new OrderItem { Order = order, ProductId = 1, Quantity = 2, UnitPrice = 99.99m };
var orderItem2 = new OrderItem { Order = order, ProductId = 2, Quantity = 1, UnitPrice = 199.99m };
context.OrderItems.AddRange(orderItem1, orderItem2);
// 如果任何操作失败,整个事务会回滚
await context.SaveChangesAsync();3.2 显式事务
对于更复杂的场景,可以使用显式事务。
csharp
using var context = new ApplicationDbContext();
// 开始事务
using var transaction = await context.Database.BeginTransactionAsync();
try
{
// 执行操作
var product = new Product { Name = "事务中的产品", Price = 299.99m };
context.Products.Add(product);
await context.SaveChangesAsync();
var order = new Order { CustomerId = 1, ProductId = product.Id };
context.Orders.Add(order);
await context.SaveChangesAsync();
// 提交事务
await transaction.CommitAsync();
Console.WriteLine("事务提交成功");
}
catch (Exception ex)
{
// 回滚事务
await transaction.RollbackAsync();
Console.WriteLine($"事务回滚: {ex.Message}");
throw;
}3.3 分布式事务
在需要跨多个数据库或资源进行事务操作时,可以使用分布式事务。
csharp
// 使用 TransactionScope(需要 .NET 5+)
using var scope = new TransactionScope(
TransactionScopeOption.Required,
new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted },
TransactionScopeAsyncFlowOption.Enabled);
try
{
// 第一个数据库操作
using var context1 = new ApplicationDbContext();
var order = new Order { CustomerId = 1 };
context1.Orders.Add(order);
await context1.SaveChangesAsync();
// 第二个数据库操作
using var context2 = new InventoryDbContext();
var inventory = new Inventory { ProductId = 1, Quantity = -2 };
context2.Inventories.Add(inventory);
await context2.SaveChangesAsync();
// 完成事务
await scope.CompleteAsync();
Console.WriteLine("分布式事务完成");
}
catch (Exception ex)
{
// 自动回滚
Console.WriteLine($"分布式事务失败: {ex.Message}");
throw;
}4. 并发控制
4.1 乐观并发控制
乐观并发控制假设并发冲突很少发生,只有在实际发生冲突时才进行处理。
csharp
// 1. 在实体中添加并发标记
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
[ConcurrencyCheck] // 标记为并发检查属性
public DateTime LastUpdated { get; set; }
[Timestamp] // 时间戳属性(推荐)
public byte[] RowVersion { get; set; }
}
// 2. 处理并发冲突
using var context = new ApplicationDbContext();
var product = await context.Products.FindAsync(1);
if (product != null)
{
product.Price = 299.99m;
product.LastUpdated = DateTime.Now;
try
{
await context.SaveChangesAsync();
Console.WriteLine("更新成功");
}
catch (DbUpdateConcurrencyException ex)
{
Console.WriteLine("检测到并发冲突");
// 处理并发冲突
foreach (var entry in ex.Entries)
{
if (entry.Entity is Product)
{
// 获取数据库中的当前值
var databaseValues = await entry.GetDatabaseValuesAsync();
if (databaseValues != null)
{
// 将数据库值应用到实体
entry.OriginalValues.SetValues(databaseValues);
// 现在可以重新尝试保存或提示用户解决冲突
Console.WriteLine("已刷新数据,建议重新尝试操作");
}
else
{
Console.WriteLine("实体已被删除");
}
}
}
}
}4.2 悲观并发控制
悲观并发控制通过锁定资源来防止并发冲突。
csharp
// 使用原始 SQL 锁定行
using var context = new ApplicationDbContext();
// 使用 WITH (UPDLOCK) 锁定行
var product = context.Products
.FromSqlRaw("SELECT * FROM Products WITH (UPDLOCK) WHERE Id = {0}", 1)
.AsTracking()
.FirstOrDefault();
if (product != null)
{
// 处理产品
product.Price = 399.99m;
await context.SaveChangesAsync();
// 事务提交后释放锁
}5. 实体状态管理
5.1 实体状态
EF Core 中的实体可以有以下几种状态:
- Added: 实体是新的,尚未保存在数据库中
- Unchanged: 实体已存在于数据库中,且未修改
- Modified: 实体已存在于数据库中,且已修改
- Deleted: 实体已存在于数据库中,且标记为删除
- Detached: 实体不在 DbContext 的跟踪范围内
5.2 查看和修改实体状态
csharp
using var context = new ApplicationDbContext();
// 添加实体
var product = new Product { Name = "测试产品", Price = 99.99m };
context.Products.Add(product);
// 查看实体状态
var entityState = context.Entry(product).State; // Added
// 保存更改
await context.SaveChangesAsync();
entityState = context.Entry(product).State; // Unchanged
// 修改实体
product.Price = 199.99m;
entityState = context.Entry(product).State; // Modified
// 手动设置状态
context.Entry(product).State = EntityState.Unchanged;
entityState = context.Entry(product).State; // Unchanged
// 将实体标记为已修改(但不跟踪具体哪些属性已更改)
context.Entry(product).State = EntityState.Modified;
// 标记特定属性为已修改
context.Entry(product).Property(p => p.Price).IsModified = true;
// 查看哪些属性已修改
var modifiedProperties = context.Entry(product)
.Properties
.Where(p => p.IsModified)
.Select(p => p.Metadata.Name)
.ToList();5.3 处理分离实体
csharp
// 获取分离的实体(例如从API接收)
var productDTO = GetProductFromApi(); // 假设这是从外部获取的产品数据
var detachedProduct = new Product
{
Id = productDTO.Id,
Name = productDTO.Name,
Price = productDTO.Price
};
using var context = new ApplicationDbContext();
// 方法1:附加并更新
context.Products.Attach(detachedProduct);
context.Entry(detachedProduct).State = EntityState.Modified;
await context.SaveChangesAsync();
// 方法2:先查询数据库中的实体,然后应用更改
var dbProduct = await context.Products.FindAsync(detachedProduct.Id);
if (dbProduct != null)
{
// 手动复制属性
dbProduct.Name = detachedProduct.Name;
dbProduct.Price = detachedProduct.Price;
// 或者使用 AutoMapper
// mapper.Map(detachedProduct, dbProduct);
await context.SaveChangesAsync();
}
// 方法3:使用 Update 方法(自动附加并标记为 Modified)
context.Products.Update(detachedProduct);
await context.SaveChangesAsync();6. 优化保存操作
6.1 减少跟踪实体数量
csharp
// 使用 AsNoTracking 查询只读数据
var products = context.Products.AsNoTracking().ToList();
// 查询后清除 ChangeTracker
context.ChangeTracker.Clear();
// 使用新的 DbContext 实例进行保存操作
using var saveContext = new ApplicationDbContext();
saveContext.Products.Add(new Product { Name = "新产品", Price = 99.99m });
await saveContext.SaveChangesAsync();6.2 批量保存优化
csharp
// 批量操作时禁用自动检测更改以提高性能
using var context = new ApplicationDbContext();
context.ChangeTracker.AutoDetectChangesEnabled = false;
try
{
for (int i = 0; i < 1000; i++)
{
context.Products.Add(new Product { Name = $"产品{i}", Price = i * 10m });
// 每 100 个实体保存一次
if (i % 100 == 99)
{
context.ChangeTracker.DetectChanges(); // 手动检测更改
await context.SaveChangesAsync();
context.ChangeTracker.Clear(); // 清除跟踪
}
}
// 保存剩余的实体
if (context.ChangeTracker.HasChanges())
{
context.ChangeTracker.DetectChanges();
await context.SaveChangesAsync();
}
}
finally
{
// 恢复自动检测更改
context.ChangeTracker.AutoDetectChangesEnabled = true;
}6.3 禁用验证
如果已经在其他地方进行了验证,可以临时禁用 EF Core 的验证以提高性能。
csharp
using var context = new ApplicationDbContext();
// 禁用验证
context.ValidateOnSaveEnabled = false;
try
{
// 执行批量保存操作
// ...
}
finally
{
// 恢复验证
context.ValidateOnSaveEnabled = true;
}7. 审计日志
7.1 实现简单的审计日志
csharp
public class ApplicationDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
public DbSet<AuditLog> AuditLogs { get; set; }
// 跟踪当前用户
public string CurrentUserId { get; set; }
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{}
protected override void OnSaveChanges()
{
// 创建审计日志
var auditEntries = OnBeforeSaveChanges();
// 保存更改
base.OnSaveChanges();
// 保存审计日志
OnAfterSaveChanges(auditEntries);
}
private List<AuditEntry> OnBeforeSaveChanges()
{
ChangeTracker.DetectChanges();
var auditEntries = new List<AuditEntry>();
foreach (var entry in ChangeTracker.Entries())
{
if (entry.Entity is AuditLog || entry.State == EntityState.Detached ||
entry.State == EntityState.Unchanged)
continue;
var auditEntry = new AuditEntry(entry);
auditEntry.TableName = entry.Entity.GetType().Name;
auditEntry.UserId = CurrentUserId;
auditEntries.Add(auditEntry);
foreach (var property in entry.Properties)
{
string propertyName = property.Metadata.Name;
if (property.Metadata.IsPrimaryKey())
{
auditEntry.KeyValues[propertyName] = property.CurrentValue;
continue;
}
switch (entry.State)
{
case EntityState.Added:
auditEntry.NewValues[propertyName] = property.CurrentValue;
break;
case EntityState.Deleted:
auditEntry.OldValues[propertyName] = property.OriginalValue;
break;
case EntityState.Modified:
if (property.IsModified && property.OriginalValue?.ToString() !=
property.CurrentValue?.ToString())
{
auditEntry.OldValues[propertyName] = property.OriginalValue;
auditEntry.NewValues[propertyName] = property.CurrentValue;
}
break;
}
}
}
return auditEntries;
}
private void OnAfterSaveChanges(List<AuditEntry> auditEntries)
{
if (auditEntries == null || auditEntries.Count == 0)
return;
foreach (var auditEntry in auditEntries)
{
// 保存审计日志
AuditLogs.Add(auditEntry.ToAuditLog());
}
base.SaveChanges();
}
}
public class AuditEntry
{
public AuditEntry(EntityEntry entry)
{
Entry = entry;
KeyValues = new Dictionary<string, object>();
OldValues = new Dictionary<string, object>();
NewValues = new Dictionary<string, object>();
}
public EntityEntry Entry { get; }
public string TableName { get; set; }
public Dictionary<string, object> KeyValues { get; }
public Dictionary<string, object> OldValues { get; }
public Dictionary<string, object> NewValues { get; }
public string UserId { get; set; }
public AuditLog ToAuditLog()
{
return new AuditLog
{
TableName = TableName,
KeyValues = JsonConvert.SerializeObject(KeyValues),
OldValues = OldValues.Count == 0 ? null : JsonConvert.SerializeObject(OldValues),
NewValues = NewValues.Count == 0 ? null : JsonConvert.SerializeObject(NewValues),
UserId = UserId,
Timestamp = DateTime.Now
};
}
}
public class AuditLog
{
public int Id { get; set; }
public string TableName { get; set; }
public string KeyValues { get; set; }
public string OldValues { get; set; }
public string NewValues { get; set; }
public string UserId { get; set; }
public DateTime Timestamp { get; set; }
}8. 常见问题与解决方案
8.1 性能问题
问题: 批量保存操作太慢 解决方案: 分批次保存,禁用自动检测更改,使用 EF Core 7.0+ 的 ExecuteUpdate/ExecuteDelete
问题: 内存使用过高 解决方案: 定期清除 ChangeTracker,使用多个小型 DbContext 实例
8.2 并发冲突
- 问题: 多个用户同时修改同一数据 解决方案: 实现乐观并发控制,使用 Timestamp 属性
8.3 数据验证
- 问题: 保存时出现验证错误 解决方案: 在保存前验证数据,使用数据注解或自定义验证器
8.4 事务失败
- 问题: 复杂操作中事务失败 解决方案: 实现适当的异常处理,确保事务正确提交或回滚
9. 小结
本章节详细介绍了 EF Core 中的数据保存和事务处理机制,包括:
- 基本的 CRUD 操作:创建、读取、更新和删除数据
- 批量操作技术:批量添加、更新和删除数据
- 事务管理:自动事务、显式事务和分布式事务
- 并发控制策略:乐观并发控制和悲观并发控制
- 实体状态管理:查看和修改实体状态,处理分离实体
- 性能优化技巧:减少跟踪实体、批量保存优化、禁用验证
- 审计日志实现:记录数据变更历史
- 常见问题与解决方案
通过掌握这些技术,你可以有效地管理数据持久化过程,确保数据的完整性和一致性,同时优化应用程序的性能。