在C#项目中,EF Core通过对象关系映射让开发者用强类型方式操作数据库。它把数据表转换为实体类,将LINQ查询翻译为对应数据库的SQL,从而屏蔽底层差异。掌握增删改查,核心在于理解DbContext的变更跟踪与SaveChanges的提交机制。

一、环境准备与DbContext配置
使用EF Core的第一步是定义实体类和继承DbContext的上下文类。实体类的属性通常对应数据表字段,主键使用Key特性或约定命名Id。DbContext中通过DbSet<T>暴露各个表的集合,并在OnConfiguring中指定数据库提供程序与连接串。
下面以SQLite为例展示最小配置。实际项目中建议将连接串放在配置文件,并使用依赖注入注册上下文,而非在代码中硬编码。注意UseSqlite来自Microsoft.EntityFrameworkCore.Sqlite包,不同数据库需引用对应提供程序。
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
public class Product
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
public class AppDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
options.UseSqlite("Data Source=app.db");
}
}
二、新增数据操作
新增分为单条与批量两种常见形式。对于被上下文跟踪的新实体,调用Add或AddRange后,状态会被标记为Added,执行SaveChanges时才会真正生成INSERT语句。若实体未被跟踪,也可先附加再改状态,但一般直接用Add更直观。
下面的代码演示插入单个商品与一次插入多个商品。SaveChanges默认在事务中执行,要么全部成功要么回滚,保证数据一致性。如果需要在插入后获取自增主键,EF Core会在提交后自动将数据库生成的值同步回实体对象。
using (var db = new AppDbContext())
{
// 单条新增
var p1 = new Product { Name = "键盘", Price = 99.9m };
db.Products.Add(p1);
db.SaveChanges();
Console.WriteLine("新Id:" + p1.Id);
// 批量新增
var list = new List<Product>
{
new Product { Name = "鼠标", Price = 59.0m },
new Product { Name = "显示器", Price = 899.0m }
};
db.Products.AddRange(list);
db.SaveChanges();
}
三、删除数据操作
删除操作必须先让上下文知道要删的实体。常见做法是用主键查询出实体再Remove,适用于需要确认数据存在的场景。若仅按条件删除且不关心实体内容,可借助EF Core的批量删除扩展(如ExecuteDelete)减少查询开销。
传统跟踪删除代码如下。Remove会把状态置为Deleted,SaveChanges发出DELETE。注意若实体未被当前上下文跟踪,需先Attach再Remove,否则会报找不到主键跟踪项。批量删除扩展方法在Microsoft.EntityFrameworkCore.Relational中提供,能直接生成DELETE WHERE,不经过实体物化。
using (var db = new AppDbContext())
{
// 按主键查询后删除
var item = db.Products.Find(1);
if (item != null)
{
db.Products.Remove(item);
db.SaveChanges();
}
// 条件批量删除,无需先查询
db.Products.Where(p => p.Price < 10).ExecuteDelete();
}
四、修改数据操作
修改分为跟踪修改与非跟踪修改。跟踪修改最自然:查询出的实体被上下文监视,改属性后SaveChanges会自动比对快照生成UPDATE。非跟踪场景(如Web层传来的DTO)可用Update附加为Modified状态,但会更新所有列。
以下展示两种方式。第一种利用跟踪自动脏检查,只改变动字段;第二种显式Update,适合断开式环境。若只想更新部分字段,可在非跟踪时借助Entry().Property().IsModified控制,避免多余写列。
using (var db = new AppDbContext())
{
// 跟踪修改
var p = db.Products.Find(2);
if (p != null)
{
p.Price = 69.0m;
db.SaveChanges();
}
// 非跟踪修改
var detached = new Product { Id = 3, Name = "耳机", Price = 199.0m };
db.Products.Update(detached);
db.SaveChanges();
}
五、查询数据操作
查询是EF Core最常用能力。LINQ to Entities支持Where、OrderBy、Select、Include等。Include用于预先加载关联表,避免遍历时触发延迟加载导致N+1查询。尽量在数据库端用Select投影所需字段,减少传输量。
下面示例包含条件过滤、排序与关联包含。若Product有Category导航属性,Include会生成LEFT JOIN。AsNoTracking可在只读场景关闭跟踪提升性能。分页用Skip和Take,注意排序稳定后再分页,防止数据错乱。
using (var db = new AppDbContext())
{
var result = db.Products
.AsNoTracking()
.Where(p => p.Price > 50)
.OrderByDescending(p => p.Price)
.Skip(0)
.Take(10)
.ToList();
foreach (var p in result)
{
Console.WriteLine($"{p.Name} {p.Price}");
}
}
六、变更跟踪与SaveChanges细节
DbContext维护实体状态机,状态包括Added、Modified、Deleted、Unchanged、Detached。SaveChanges遍历跟踪项,按状态生成命令并批量提交。一次调用默认开启事务,若某条失败整体回滚,因此多步操作无需手动BeginTransaction也能保证原子性。
长生命周期上下文会累积跟踪项导致内存上涨,建议用using块短时持有。对于高并发更新,要注意跟踪冲突与并发令牌(ConcurrencyToken)。合理设计聚合边界,避免把大图全部加载再保存,可显著提升吞吐。
| 操作 | 推荐方式 | 注意点 |
|---|---|---|
| 新增 | Add / AddRange | 自增键提交后回填 |
| 删除 | Remove 或 ExecuteDelete | 批量删免查询 |
| 修改 | 跟踪改属性 / Update | Update更新全列 |
| 查询 | LINQ + AsNoTracking | 只读关跟踪省资源 |
七、常见误区与建议
一个典型误区是在循环里反复SaveChanges,导致大量短事务与网络往返。应尽量在业务边界统一提交一次。另一个误区是滥用延迟加载,造成隐藏的多次查询,建议显式Include或投影。
此外,不要将DbContext设为静态单例长期存活,它在多线程下非线程安全。Web应用通过依赖注入使用Scoped生命周期,每次请求一个实例,既安全又方便事务边界控制。掌握这些,就能用EF Core稳健地完成各类增删改查需求。