Skip to content

基础 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();
}

📊 性能对比总结 ​

操作传统方式优化方式性能提升
批量插入循环 + SaveChangesAddRange10-50x
批量更新循环 + SaveChangesExecuteUpdateAsync10-100x
批量删除循环 + SaveChangesExecuteDeleteAsync10-100x
只读查询跟踪查询AsNoTracking20-50%
关联查询N+1 查询Include50-90%

📚 延伸阅读 ​


💡 小结 ​

关键点:

  • ✅ 使用异步方法(async/await)
  • ✅ 批量操作用 AddRange/RemoveRange
  • ✅ 只读查询用 AsNoTracking
  • ✅ 避免 N+1 查询问题
  • ✅ 考虑使用软删除
  • ✅ 合理使用事务保证数据一致性

下一步:

  1. 学习 LINQ 查询基础
  2. 掌握 性能优化技巧
  3. 理解 变更跟踪原理

基于 MIT 许可发布