【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中不支持高效的删除、更新、插入数据,都是逐条操作。AddRangeDeleteRange等。
两种办法实现批量增删改:

  • 执行sql
  • 通过EF Core扩展包:Zack.EFCore.BatchZ.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);

批量操作详细:https://www.cnblogs.com/yaopengfei/p/14441115.html

posted @ 2022-04-16 07:39  .Neterr  阅读(503)  评论(0)    收藏  举报