Appearance
基础 CRUD 操作
掌握 EF Core 的增删改查基本操作
📖 CRUD 概述
CRUD 是数据操作的四个基本动作:
- Create(创建)- 添加新记录
- Read(读取)- 查询数据
- Update(更新)- 修改现有记录
- Delete(删除)- 移除记录
1️⃣ Create - 创建数据
添加单个实体
csharp
// 方式 1:Add + SaveChangesAsync
var product = new Product
{
Name = "Laptop",
Price = 999.99m,
CategoryId = 1
};
context.Products.Add(product);
await context.SaveChangesAsync();
Console.WriteLine($"Product ID: {product.Id}"); // 自动获取生成的 ID
// 方式 2:直接添加到 DbSet
await context.Products.AddAsync(product);
await context.SaveChangesAsync();生成的 SQL:
sql
INSERT INTO [Products] ([Name], [Price], [CategoryId])
VALUES (@p0, @p1, @p2);
SELECT [Id] FROM [Products] WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();批量添加实体
csharp
// ✅ 推荐:使用 AddRange
var products = new List<Product>
{
new Product { Name = "Laptop", Price = 999.99m },
new Product { Name = "Mouse", Price = 29.99m },
new Product { Name = "Keyboard", Price = 79.99m }
};
await context.Products.AddRangeAsync(products);
await context.SaveChangesAsync();
// ❌ 避免:循环中多次调用 SaveChangesAsync
foreach (var product in products)
{
context.Products.Add(product);
await context.SaveChangesAsync(); // N 次数据库往返!
}性能对比:
AddRange+ 单次SaveChangesAsync:1 次数据库往返- 循环 + 多次
SaveChangesAsync:N 次数据库往返 - 性能差异:10-100 倍
添加关联实体
csharp
// 一对多关系
var category = new Category { Name = "Electronics" };
category.Products = new List<Product>
{
new Product { Name = "Laptop", Price = 999.99m },
new Product { Name = "Mouse", Price = 29.99m }
};
context.Categories.Add(category);
await context.SaveChangesAsync();
// EF Core 自动设置外键
// Products 表的 CategoryId 会自动填充2️⃣ Read - 读取数据
根据主键查询
csharp
// FindAsync - 先检查缓存,再查询数据库
var product = await context.Products.FindAsync(1);
// FirstOrDefaultAsync - 始终查询数据库
var product = await context.Products
.FirstOrDefaultAsync(p => p.Id == 1);
// SingleOrDefaultAsync - 期望唯一结果
var product = await context.Products
.SingleOrDefaultAsync(p => p.Sku == "LAPTOP-001");对比:
| 方法 | 缓存检查 | 适用场景 |
|---|---|---|
FindAsync | ✅ 是 | 根据主键查询 |
FirstOrDefaultAsync | ❌ 否 | 条件查询 |
SingleOrDefaultAsync | ❌ 否 | 唯一约束查询 |
查询列表
csharp
// 查询所有
var products = await context.Products.ToListAsync();
// 条件查询
var expensiveProducts = await context.Products
.Where(p => p.Price > 100)
.ToListAsync();
// 排序
var sortedProducts = await context.Products
.OrderByDescending(p => p.Price)
.ToListAsync();
// 组合查询
var result = await context.Products
.Where(p => p.IsActive && p.Price > 50)
.OrderByDescending(p => p.CreatedAt)
.Take(10)
.ToListAsync();生成的 SQL:
sql
SELECT TOP 10 * FROM Products
WHERE IsActive = 1 AND Price > 50
ORDER BY CreatedAt DESC投影查询(推荐)
csharp
// DTO 投影 - 只查询需要的字段
var products = await context.Products
.Where(p => p.IsActive)
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price
// 不加载 Description、CreatedAt 等字段
})
.ToListAsync();
// 匿名类型投影
var result = await context.Products
.Select(p => new
{
p.Id,
p.Name,
DiscountedPrice = p.Price * 0.9m
})
.ToListAsync();优点:
- ✅ 减少数据传输
- ✅ 提升查询性能
- ✅ 降低内存占用
包含导航属性
csharp
// Include - 预加载关联数据
var orders = await context.Orders
.Include(o => o.Customer)
.Include(o => o.Items)
.ThenInclude(i => i.Product)
.ToListAsync();
// 过滤 Include(EF Core 5.0+)
var categories = await context.Categories
.Include(c => c.Products.Where(p => p.IsActive))
.ToListAsync();生成的 SQL:
sql
-- 查询 Orders
SELECT * FROM Orders
-- 查询 Customers
SELECT * FROM Customers WHERE Id IN (...)
-- 查询 OrderItems + Products
SELECT * FROM OrderItems
JOIN Products ON ...
WHERE OrderId IN (...)聚合查询
csharp
// 计数
var count = await context.Products.CountAsync();
// 条件计数
var activeCount = await context.Products
.CountAsync(p => p.IsActive);
// 求和
var totalValue = await context.Products
.SumAsync(p => p.Price);
// 平均值
var avgPrice = await context.Products
.AverageAsync(p => p.Price);
// 最小值/最大值
var minPrice = await context.Products.MinAsync(p => p.Price);
var maxPrice = await context.Products.MaxAsync(p => p.Price);
// 分组统计
var stats = await context.Products
.GroupBy(p => p.CategoryId)
.Select(g => new
{
CategoryId = g.Key,
Count = g.Count(),
AvgPrice = g.Average(p => p.Price),
MaxPrice = g.Max(p => p.Price)
})
.ToListAsync();分页查询
csharp
public async Task<PagedResult<Product>> GetProductsAsync(
int pageNumber,
int pageSize)
{
var query = context.Products.AsNoTracking();
// 总数
var totalCount = await query.CountAsync();
// 分页数据
var items = await query
.OrderByDescending(p => p.CreatedAt)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return new PagedResult<Product>
{
Items = items,
TotalCount = totalCount,
PageNumber = pageNumber,
PageSize = pageSize,
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize)
};
}
public class PagedResult<T>
{
public List<T> Items { get; set; }
public int TotalCount { get; set; }
public int PageNumber { get; set; }
public int PageSize { get; set; }
public int TotalPages { get; set; }
}使用:
csharp
var result = await GetProductsAsync(1, 20);
Console.WriteLine($"Page 1 of {result.TotalPages}, Total: {result.TotalCount}");3️⃣ Update - 更新数据
追踪实体更新(推荐)
csharp
// 1. 查询实体(自动开始跟踪)
var product = await context.Products.FindAsync(1);
if (product != null)
{
// 2. 修改属性
product.Name = "Updated Laptop";
product.Price = 899.99m;
// 3. 保存更改
await context.SaveChangesAsync();
}生成的 SQL:
sql
UPDATE [Products]
SET [Name] = @p0, [Price] = @p1
WHERE [Id] = @p2;非追踪更新(附加实体)
csharp
// 适用于 Web API 场景(DTO → Entity)
var product = new Product
{
Id = 1,
Name = "Updated Name",
Price = 899.99m
};
// 附加实体
context.Products.Attach(product);
// 标记修改的属性
context.Entry(product).Property(p => p.Name).IsModified = true;
context.Entry(product).Property(p => p.Price).IsModified = true;
await context.SaveChangesAsync();优点:
- ✅ 无需先查询数据库
- ✅ 减少一次 SELECT 查询
缺点:
- ⚠️ 需要手动指定修改的属性
- ⚠️ 可能覆盖并发修改
批量更新(EF Core 8+)
csharp
// 批量更新所有符合条件的记录
await context.Products
.Where(p => p.CategoryId == 1)
.ExecuteUpdateAsync(setters => setters
.SetProperty(p => p.Price, p => p.Price * 1.1m) // 涨价 10%
.SetProperty(p => p.ModifiedAt, DateTime.UtcNow));
// 生成的 SQL:
// UPDATE Products
// SET Price = Price * 1.1, ModifiedAt = GETUTCDATE()
// WHERE CategoryId = 1性能优势:
- 传统方式:N+1 次数据库往返
ExecuteUpdateAsync:1 次数据库往返- 性能提升:10-100 倍
更新关联实体
csharp
var order = await context.Orders
.Include(o => o.Items)
.FirstOrDefaultAsync(o => o.Id == orderId);
if (order != null)
{
// 修改订单信息
order.Status = OrderStatus.Shipped;
order.ShippedAt = DateTime.UtcNow;
// 修改订单项
foreach (var item in order.Items)
{
item.Quantity += 1;
}
await context.SaveChangesAsync();
}4️⃣ Delete - 删除数据
删除单个实体
csharp
// 1. 查询实体
var product = await context.Products.FindAsync(1);
if (product != null)
{
// 2. 标记删除
context.Products.Remove(product);
// 3. 保存更改
await context.SaveChangesAsync();
}生成的 SQL:
sql
DELETE FROM [Products] WHERE [Id] = @p0;批量删除(EF Core 8+)
csharp
// 批量删除所有符合条件的记录
await context.Products
.Where(p => !p.IsActive)
.ExecuteDeleteAsync();
// 生成的 SQL:
// DELETE FROM Products WHERE IsActive = 0性能优势:
- 传统方式:N+1 次数据库往返
ExecuteDeleteAsync:1 次数据库往返
软删除(推荐)
csharp
public interface ISoftDelete
{
bool IsDeleted { get; set; }
DateTime? DeletedAt { get; set; }
}
public class Product : ISoftDelete
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
// 软删除字段
public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
}
// DbContext 配置全局过滤器
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>(entity =>
{
entity.HasQueryFilter(p => !p.IsDeleted);
});
}
// 软删除方法
public async Task SoftDeleteProductAsync(int productId)
{
var product = await context.Products.FindAsync(productId);
if (product != null)
{
product.IsDeleted = true;
product.DeletedAt = DateTime.UtcNow;
await context.SaveChangesAsync();
}
}
// 查询时自动过滤已删除的记录
var products = await context.Products.ToListAsync();
// SQL: SELECT * FROM Products WHERE IsDeleted = 0
// 如需包含已删除的记录
var allProducts = await context.Products
.IgnoreQueryFilters()
.ToListAsync();优点:
- ✅ 数据可恢复
- ✅ 保留历史记录
- ✅ 避免外键约束问题
级联删除
csharp
// 配置级联删除
modelBuilder.Entity<Category>(entity =>
{
entity.HasMany(c => c.Products)
.WithOne(p => p.Category)
.HasForeignKey(p => p.CategoryId)
.OnDelete(DeleteBehavior.Cascade);
});
// 删除分类时自动删除相关产品
var category = await context.Categories
.Include(c => c.Products)
.FirstOrDefaultAsync(c => c.Id == categoryId);
if (category != null)
{
context.Categories.Remove(category);
await context.SaveChangesAsync();
// Products 表中相关记录也会被删除
}删除行为对比:
| 行为 | 说明 |
|---|---|
Cascade | 自动删除子实体 |
Restrict | 如果有子实体,禁止删除 |
SetNull | 将子实体的外键设为 NULL |
NoAction | 不执行任何操作 |
🔄 事务处理
隐式事务(默认)
csharp
// SaveChangesAsync 自动在事务中执行
context.Products.Add(product1);
context.Products.Add(product2);
await context.SaveChangesAsync();
// 要么全部成功,要么全部回滚显式事务
csharp
await using var transaction = await context.Database.BeginTransactionAsync();
try
{
// 操作 1
var order = new Order { /* ... */ };
context.Orders.Add(order);
await context.SaveChangesAsync();
// 操作 2
var payment = new Payment
{
OrderId = order.Id,
Amount = order.TotalAmount
};
context.Payments.Add(payment);
await context.SaveChangesAsync();
// 提交事务
await transaction.CommitAsync();
}
catch (Exception)
{
// 回滚事务
await transaction.RollbackAsync();
throw;
}事务隔离级别
csharp
await using var transaction = await context.Database.BeginTransactionAsync(
System.Data.IsolationLevel.Serializable);
// Serializable - 最严格,防止幻读
// RepeatableRead - 防止不可重复读
// ReadCommitted - 防止脏读(默认)
// ReadUncommitted - 允许脏读(性能最高)💡 最佳实践
1. 始终使用异步方法
csharp
// ✅ 正确
await context.SaveChangesAsync();
await context.Products.ToListAsync();
// ❌ 错误:可能导致死锁
context.SaveChanges();
context.Products.ToList();2. 批量操作用 AddRange/RemoveRange
csharp
// ✅ 推荐
await context.Products.AddRangeAsync(products);
await context.SaveChangesAsync();
// ❌ 避免
foreach (var product in products)
{
context.Products.Add(product);
}
await context.SaveChangesAsync();3. 只读查询使用 AsNoTracking
csharp
// ✅ 性能提升 20-50%
var products = await context.Products
.AsNoTracking()
.Where(p => p.IsActive)
.ToListAsync();
// ❌ 不必要的跟踪
var products = await context.Products
.Where(p => p.IsActive)
.ToListAsync();4. 避免 N+1 查询
csharp
// ❌ 错误:N+1 查询
var orders = await context.Orders.ToListAsync();
foreach (var order in orders)
{
var customer = await context.Customers.FindAsync(order.CustomerId);
}
// ✅ 正确:使用 Include
var orders = await context.Orders
.Include(o => o.Customer)
.ToListAsync();
// ✅ 更好:投影查询
var orders = await context.Orders
.Select(o => new
{
o.Id,
CustomerName = o.Customer.Name
})
.ToListAsync();5. 合理处理异常
csharp
try
{
context.Products.Add(product);
await context.SaveChangesAsync();
}
catch (DbUpdateException ex)
{
// 数据库更新失败(约束违反、外键冲突等)
_logger.LogError(ex, "Failed to save product");
throw;
}
catch (DbUpdateConcurrencyException ex)
{
// 并发冲突
_logger.LogWarning(ex, "Concurrency conflict detected");
throw;
}⚠️ 常见陷阱
1. 忘记调用 SaveChangesAsync
csharp
// ❌ 错误
context.Products.Add(product);
// 忘记调用 SaveChangesAsync,数据不会保存
// ✅ 正确
context.Products.Add(product);
await context.SaveChangesAsync();2. 在循环中保存
csharp
// ❌ 慢
foreach (var product in products)
{
context.Products.Add(product);
await context.SaveChangesAsync(); // N 次数据库往返
}
// ✅ 快
context.Products.AddRange(products);
await context.SaveChangesAsync(); // 1 次数据库往返3. 修改未跟踪的实体
csharp
// ❌ 错误
var product = new Product { Id = 1, Name = "New Name" };
context.Entry(product).State = EntityState.Modified;
await context.SaveChangesAsync();
// 如果实体不存在,会抛出异常
// ✅ 正确
var product = await context.Products.FindAsync(1);
if (product != null)
{
product.Name = "New Name";
await context.SaveChangesAsync();
}📊 性能对比总结
| 操作 | 传统方式 | 优化方式 | 性能提升 |
|---|---|---|---|
| 批量插入 | 循环 + SaveChanges | AddRange | 10-50x |
| 批量更新 | 循环 + SaveChanges | ExecuteUpdateAsync | 10-100x |
| 批量删除 | 循环 + SaveChanges | ExecuteDeleteAsync | 10-100x |
| 只读查询 | 跟踪查询 | AsNoTracking | 20-50% |
| 关联查询 | N+1 查询 | Include | 50-90% |
📚 延伸阅读
💡 小结
关键点:
- ✅ 使用异步方法(async/await)
- ✅ 批量操作用 AddRange/RemoveRange
- ✅ 只读查询用 AsNoTracking
- ✅ 避免 N+1 查询问题
- ✅ 考虑使用软删除
- ✅ 合理使用事务保证数据一致性
下一步: