【EF Core】增删改查
插入
using (TestDbContext ctx = new TestDbContext())
{
var b1 = new Book
{
AuthorName = "杨中科",
Title = "零基础趣学C语言",
Price = 59.8,
PubTime = new DateTime(2019, 3, 1)
};
var b2 = new Book
{
AuthorName = "Robert Sedgewick",
Title = "算法(第4版)",
Price = 99,
PubTime = new DateTime(2012, 10, 1)
};
ctx.Books.Add(b1);
ctx.Books.Add(b2);
await ctx.SaveChangesAsync();
}
批量添加:
var products = new List<Product>
{
new Product { Name = "Keyboard", Price = 49.99m },
new Product { Name = "Mouse", Price = 29.99m }
};
context.Products.AddRange(products); // 或 context.AddRange(products);
context.SaveChanges();
Attach+手动附加状态:
更灵活,可随时切换状态(如从 Unchanged 改为 Modified 或 Deleted)
var product = new Product { Id = 1, Name = "Updated Laptop" };
context.Attach(product); // 附加到上下文
context.Entry(product).State = EntityState.Added; // 标记为新增
context.SaveChanges();
查询
using (TestDbContext ctx = new TestDbContext())
{
//查询全部
foreach (var b in ctx.Books)
{
Console.WriteLine($"Id={b.Id},Title={b.Title},Price={b.Price}");
}
Console.WriteLine("----------------");
//主键查询
user = await ctx.Books.FindAsync(1);
//按条件查询,并且排序
foreach (var b in ctx.Books.Where(b => b.Price > 80).OrderByDescending(b => b.Id))
{
Console.WriteLine($"Id={b.Id},Title={b.Title},Price={b.Price}");
}
Console.WriteLine("----------------");
//Single
var book = ctx.Books.FirstOrDefault(b => b.Id == 1);
Console.WriteLine($"Id={book.Id},Title={book.Title},Price={book.Price}");
Console.WriteLine("----------------");
//分组查询
var groups = ctx.Books.GroupBy(b => b.AuthorName).Select(g => new { AuthorName = g.Key, BooksCount = g.Count(), MaxPrice = g.Max(b => b.Price) });
foreach (var g in groups)
{
Console.WriteLine($"作者:{g.AuthorName},图书数量:{g.BooksCount},最高价格:{g.MaxPrice}");
}
//查询指定字段
var book = ctx.Books.Select(b => new { b.Id, b.Title }).FirstOrDefault();
}
Find主键查询
Find方法是专门为基于主键的查询设计的优化方法,Find方法会首先检查当前DbContext的内存缓存中是否已存在具有指定主键的实体,如果实体已被跟踪,则直接返回内存中的实体,避免不必要的数据库查询,FirstOrDefault每次都会生成并执行SQL查询
var user = await ctx.Books.FindAsync(1);
非附加查询
非附加查询,当仅需查询数据而不进行修改时(如展示、统计、导出等),使用AsNoTracking()可避免EF Core维护实体状态,显著提升性能并减少内存占用
var user = identityContext.Set<UserInfo>().AsNoTracking().FirstOrDefault();
若使用 AsNoTracking()查询到的实体不会被跟踪,修改后调用 SaveChanges() 不会生效
解决方案:需手动附加实体并标记为修改状态:
var blog = context.Blogs.AsNoTracking().First(); // 无跟踪查询
blog.Title = "新标题";
context.Attach(blog); // 附加到上下文
context.Entry(blog).State = EntityState.Modified; // 标记为修改
context.SaveChanges(); // 保存到数据库
Include预加载
Include() 是一种用于预先加载的方法,主要用于在查询主实体时一次性加载关联的导航属性数据,避免后续访问时触发额外的数据库查询(即延迟加载的N+1问题)。
通过Include()显式指定需要加载的导航属性,生成包含JOIN操作的SQL查询,将主实体及其关联数据一并加载到内存。
var blogs = context.Blogs
.Include(b => b.Posts) // 加载Blog关联的Posts集合
.ToList();
多级关联加载:
ThenInclude()*:用于加载嵌套的导航属性(多级关联)。
var blogs = context.Blogs
.Include(b => b.Posts) // 第一级:Posts
.ThenInclude(p => p.Author) // 第二级:Posts关联的Author
.ToList();
避免N+1查询:若不使用Include,访问每个实体的导航属性会触发单独查询。例如:
var blogs = context.Blogs.ToList(); // 1次查询
foreach (var blog in blogs)
{
var posts = blog.Posts.ToList(); // N次查询(N为博客数量)
}
使用Include后,仅需1次查询即可获取所有数据
include过滤
Include过滤是指在加载关联数据时,对导航属性中的关联实体应用条件筛选,而非简单加载全部关联数据。
var blogs = context.Blogs
.Include(b => b.Posts.Where(p => p.IsPublished)) // 只加载已发布的Posts
.ToList();
自动预加载
//配置tag属性自动加载
modelBuilder.Entity<Comment>().Navigation(c => c.Tags).AutoInclude();
//不需要加Include方法了,此时comments中有tags了
var comments = identityContext.Comments.ToList();
//禁用预加载
var comments = identityContext.Comments.IgnoreAutoIncludes().ToList();
显式加载
显式加载是EF Core中一种精细控制关联数据加载的策略,它允许开发者明确指定何时以及如何加载导航属性数据。与延迟加载和预先加载相比,显式加载提供了更精确的控制能力,特别适合需要优化性能的复杂场景。
它的核心是通过DbContext的Entry方法获取实体条目,然后使用Reference(针对单个实体)或Collection(针对集合)方法指定要加载的导航属性,最后调用Load方法执行实际加载
// 获取主实体
var blog = context.Blogs.Single(b => b.BlogId == 1);
// 加载单个关联实体(Reference)
await context.Entry(blog)
.Reference(b => b.Owner)
.LoadAsync();
// 加载集合关联实体(Collection)
await context.Entry(blog)
.Collection(b => b.Posts)
.LoadAsync();
为什么说与延迟加载和预先加载相比,显式加载提供了更精确的控制能力?
显式加载允许开发者完全掌控关联数据的加载时机,而不是像其他两种方式那样由框架自动决定:
- 延迟加载:在访问导航属性时自动触发查询,开发者无法控制具体加载时间
- 预先加载:在查询主实体时立即加载所有指定关联数据,无论后续是否真正需要
- 显式加载:通过代码显式调用Load()或LoadAsync()方法,可以在业务逻辑的任意位置决定加载关联数据
显式加载支持在加载前添加复杂条件,而其他方式难以实现这种灵活性:
- 延迟加载:总是加载导航属性的全部数据
- 预先加载:虽然可以通过ThenInclude加载多层数据,但筛选条件有限
- 显式加载:可通过Query()方法自由添加Where、OrderBy等LINQ操作
实际应用示例:
var product = context.Products.Find(productId);
// 只加载评分4星以上的评论,并按时间排序
context.Entry(product)
.Collection(p => p.Reviews)
.Query()
.Where(r => r.Rating >= 4)
.OrderByDescending(r => r.CreatedDate)
.Load();
延迟加载
安装nuget包
Microsoft.EntityFrameworkCore.Proxies
开启延迟加载
builder.Services.AddDbContext<IdentityDbContext>(opt =>
{
string connStr = "Server=.;Database=IdentityDB;Integrated Security=True;";
opt.UseLazyLoadingProxies();//开启
opt.UseSqlServer(connStr);
});
所有导航属性设置为virtual
public class Comment
{
public int Id { get; set; }
public string Content { get; set; } = null!;
public int BId { get; set; }
public virtual Blog Blog { get; set; } = null!;
public virtual List<Tag> Tags { get; set; } = null!;
}
var comments = identityContext.Comments.ToList();
foreach (var comment in comments)
{
var tags = comment.Tags?.ToList() ?? [];
}
AddRange、ExecuteInsert适用场景对比
AddRange更适用:
- 需要立即获取数据库生成的值(如自增ID)
- 插入后需要对实体进行修改和保存
- 需要利用EF Core完整功能的工作单元模式
- 需要事务中包含其他跟踪操作的情况
ExecuteInsert更适用:
- 纯批量插入场景(如数据迁移、初始化)
- 大数据量插入(10万+记录)
- 对内存敏感的应用
- 不需要后续修改的只读数据插入
拆分查询
EF Core的拆分查询(Split Query)是一种性能优化策略,用于处理复杂关联查询可能导致的性能问题(如笛卡尔积爆炸)。以下是核心要点:
- 避免笛卡尔爆炸:当通过Include加载多个集合导航属性时,单一SQL查询会生成大量冗余数据(如主表数据重复)。拆分查询将其拆分为多个独立SQL查询,减少数据冗余。
- 降低内存消耗:拆分后每个查询结果集更小,减轻内存压力。
- 适用场景:多对多关系、嵌套集合导航属性或大数据量查询。
启用方式
局部启用
var orders = context.Orders
.Include(o => o.OrderItems)
.AsSplitQuery() // 启用拆分查询
.ToList();
全局启用
optionsBuilder.UseSqlServer(connectionString, o =>
o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
全局启用后,仍可通过AsSingleQuery()强制单查询模式
工作原理
拆分查询生成多条SQL语句,例如:
-- 查询1:加载主表数据
SELECT * FROM Orders;
-- 查询2:加载关联的OrderItems
SELECT * FROM OrderItems WHERE OrderId IN (...);
而单查询模式会生成包含LEFT JOIN的复杂SQL
AsQueryable 与 AsEnumerable的区别
**AsQueryable(): **
查询逻辑会被翻译成SQL,在数据库端执行(如Where、OrderBy等操作会生成SQL语句)。
延迟执行,直到实际遍历数据(如调用ToList())时才触发数据库查询。
AsEnumerable():
立即触发数据库查询,后续操作(如Where、Select)在内存中执行(Linq to Objects)。
若原始数据量大,可能导致性能问题(所有数据加载到内存)。
keyset分页
在EF Core中,Keyset分页(又称游标分页或基于搜索的分页)是一种高效的分页技术,相比传统的Skip/Take分页(偏移分页),它通过利用有序键值(如自增ID或时间戳)直接定位数据,避免数据库扫描大量无效行,显著提升性能。
Keyset分页的核心原理:
- 依赖有序键:使用可排序的列(如Id、CreatedAt)作为分页基准。
- 条件过滤:通过Where子句直接跳过已加载的数据(如Id > lastId),而非Skip。
- 高效索引利用:若键列有索引,数据库可直接定位目标数据,无需遍历前序行。
var lastId = 100; // 上一页最后一条记录的ID
var pageSize = 10;
var nextPage = await dbContext.Products
.Where(p => p.Id > lastId)
.OrderBy(p => p.Id)
.Take(pageSize)
.ToListAsync();
生成的SQL类似:
SELECT TOP(10) * FROM Products WHERE Id > 100 ORDER BY Id
| 特性 | Keyset分页 | 偏移分页 |
|---|---|---|
| 性能 | 高效(直接通过索引定位数据,避免全表扫描) | 低效(需扫描并跳过前序行,偏移量越大性能越差) |
| 并发问题 | 无(不受插入/删除影响,数据变动不影响分页) | 可能重复或遗漏数据(如中间数据插入/删除导致漂移) |
| 随机访问支持 | 仅支持顺序导航(上一页/下一页) | 支持跳转到任意页 |
| 适用场景 | 无限滚动、大数据量分页 | 小数据量或需随机访问的UI(如传统页码导航) |
| 实现复杂度 | 较高(需唯一有序键和多列排序逻辑) | 简单(直接使用Skip/Take) |
| 索引依赖 | 强依赖有序键的索引 | 依赖排序字段索引,但偏移量本身无索引优化 |
修改
using (TestDbContext ctx = new TestDbContext())
{
var book = ctx.Books.FirstOrDefault(b => b.Id == 1);
book.Price = 100;
ctx.SaveChanges();
}
删除
using (TestDbContext ctx = new TestDbContext())
{
var book = ctx.Books.FirstOrDefault(b => b.Id == 4);
ctx.Remove(book);
//ctx.Books.Remove(book);//这种方式也可以
ctx.SaveChanges();
}
执行sql
查询数据并映射到实体
FromSqlRaw:手动参数化,需防SQL注入。
FromSqlInterpolated:自动参数化,推荐使用。
// 使用FromSqlInterpolated(推荐)
var blogs = context.Blogs
.FromSqlInterpolated($"SELECT * FROM Blogs WHERE Rating > {3}")
.ToList();
// 使用FromSqlRaw(需手动参数化)
var blogsRaw = context.Blogs
.FromSqlRaw("SELECT * FROM Blogs WHERE Rating > {0}", 3)
.ToList();
限制:
必须返回实体所有字段,不能只查询部分列。
不支持多表JOIN结果直接映射。
结合LINQ
var result = context.Blogs
.FromSqlInterpolated("SELECT * FROM Blogs")
.Where(b => b.Url.Contains("dotnet"))
.ToList();
复杂查询,如多表join
using (var cmd = context.Database.GetDbConnection().CreateCommand()) {
cmd.CommandText = "SELECT Title, Content FROM Articles WHERE Title LIKE '%搜索%'";
context.Database.OpenConnection();
using (var reader = cmd.ExecuteReader()) {
while (reader.Read()) {
var title = reader.GetString(0);
var content = reader.GetString(1);
}
}
}
执行非查询SQL(增删改)
ExecuteSqlRaw / ExecuteSqlInterpolated
// 使用ExecuteSqlInterpolated(推荐)
var rows = await context.Database.ExecuteSqlInterpolatedAsync(
$"UPDATE Blogs SET Rating = 5 WHERE Name LIKE '%EF%'");
// 使用ExecuteSqlRaw
var rowsRaw = context.Database.ExecuteSqlRaw(
"DELETE FROM Blogs WHERE Rating < {0}", 3);
调用存储过程
查询存储过程
var result = context.Blogs
.FromSqlRaw("EXEC GetTopBlogs @p0", 5)
.ToList();
非查询存储过程
var rows = context.Database.ExecuteSqlRaw(
"EXEC ArchiveOldBlogs @p0", DateTime.Now.AddYears(-1));
4. 事务处理
using (var transaction = context.Database.BeginTransaction()) {
try {
context.Database.ExecuteSqlRaw("UPDATE Blogs SET Rating = 5 WHERE Id = 1");
context.Database.ExecuteSqlRaw("DELETE FROM Blogs WHERE Id = 2");
transaction.Commit();
} catch {
transaction.Rollback();
}
}
批量增删改
EF Core中不支持高效的删除、更新、插入数据,都是逐条操作。AddRange、DeleteRange等。
两种办法实现批量增删改:
- 执行sql
- 通过EF Core扩展包:
Zack.EFCore.Batch或Z.EntityFramework.Extensions
Zack.EFCore.Batch
GitHub:https://github.com/yangzhongke/Zack.EFCore.Batch
该程序集实现的批量更新、批量删除功能可以通过生成一条Update、Delete语句来实现,而不需要EFCore原始的写法先查询后操作了。
1、安装Nuget
Install-Package Zack.EFCore.Batch.MSSQL
2、
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseLoggerFactory(LoggerFactory.Create(build =>
{
build.AddDebug();
}));
optionsBuilder.UseBatchEF_MSSQL();// MSSQL Server 用户用这个
}
3、批量增删改
//批量删除
await ctx.DeleteRangeAsync<Book>(b => b.Price > n || b.AuthorName =="zack yang");
//批量修改
await ctx.BatchUpdate<Book>()
.Set(b => b.Price, b => b.Price + 3)
.Set(b => b.Title, b => s)
.Set(b =>b.AuthorName,b=>b.Title.Substring(3,2)+b.AuthorName.ToUpper())
.Set(b => b.PubTime, b => DateTime.Now)
.Where(b => b.Id > n || b.AuthorName.StartsWith("Zack"))
.ExecuteAsync();
//批量插入
ctx.BulkInsert(books);

浙公网安备 33010602011771号