entityframeworkcore
song

ef core

1
2
3
NuGet\Install-Package Microsoft.EntityFrameworkCore     ##必须的核心包
NuGet\Install-Package Microsoft.EntityFrameworkCore.SqlServer ##数据库提供程序
NuGet\Install-Package Microsoft.EntityFrameworkCore.Tools ##包含 dotnet ef CLI 工具,用于数据库迁移、模型生成、脚本导出、逆向工程等。对于开发非常有用。
  1. 创建模型类,完成模型配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    //文章实体
    public class Article
    {
    public long Id { get; set; }
    public string Title { get; set; }
    public string Message { get; set; }
    public List<Comment> Comments { get; set; }=new List<Comment>(); //导航属性
    }
    //评论实体
    public class Comment
    {
    public long Id { get; set; }
    public string Message { get; set; }
    public long ArticleId {get;set;}
    public Article Article { get; set; } //导航属性
    }

    //文章和评论的数据库配置类
    public class ArticleConfig : IEntityTypeConfiguration<Article>
    {
    public void Configure(EntityTypeBuilder<Article> builder)
    {
    builder.ToTable("T_Article");
    builder.HasKey(x => x.Id);
    builder.Property(x => x.Title).HasMaxLength(100).IsUnicode().IsRequired();
    builder.Property(x => x.Message).IsUnicode().IsRequired();

    }
    }
    public class CommentConfig : IEntityTypeConfiguration<Comment>
    {
    public void Configure(EntityTypeBuilder<Comment> builder)
    {
    builder.ToTable("T_Comment");
    builder.HasKey(x => x.Id);
    builder.Property(x => x.Message).IsUnicode().IsRequired();
    builder.HasOne(x => x.Article).WithMany(x => x.Comments).HasForeignKey("ArticleId");
    }
    }
  2. 定义DbContext

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class MyDbContext: DbContext
    {
    public DbSet<Article> Articles { get; set; }

    public DbSet<Comment> Comments { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
    optionsBuilder.UseSqlServer("Data Source=127.0.0.1;Initial Catalog=Demo1; User ID=sa; Password=Password"); //配置使用的数据库
    }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
    base.OnModelCreating(modelBuilder);
    modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly); //使用IEntityTypeConfiguration配置
    }
    }
  3. 创建数据库迁移

    1
    2
    Add-Migration Init
    Update-Database
  4. 使用DbContext进行数据操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    internal class Program
    {
    static void Main(string[] args)
    {
    using MyDbContext db = new MyDbContext();
    //var a1 = new Article { Title = "trump当选美国总统", Message = "trump当选美国总统" };
    //var comment1 = new Comment { Message = "trump当选美国总统,这是事实" };
    //var comment2 = new Comment { Message = "trump当选美国总统,这是谣言" };
    //a1.Comments.Add(comment1);
    //a1.Comments.Add(comment2);
    //db.Articles.Add(a1);
    //db.SaveChanges();

    //查询数据
    var article1 = db.Articles
    .Include(r => r.Comments)//关联查询 包含Comments
    .Single(r => r.Id == 1);
    foreach (var comment in article1.Comments)
    {
    Console.WriteLine($"{comment.Message}");
    }

    var comments = db.Comments.Include(r => r.Article);
    foreach (var comment in comments)
    {
    Console.WriteLine($"{comment.Id}---{comment.Message}---------- {comment.Article.Title}");
    }

    //更新数据
    article1.Title = "2024年11/05选举日trump当选美国总统";
    db.SaveChanges();
    Console.WriteLine($"{article1.Title}");

    //删除数据
    var comment1 = db.Comments.Single(r => r.Id == 1);
    db.Comments.Remove(comment1);
    db.SaveChanges();
    foreach (var item in db.Comments)
    {
    Console.WriteLine($"{item.Id}---{item.Message}");
    }
    }
    }

Linq

  • Select 选择特定的字段、投影到新对象等 select * from table—> select a,b from table
  • Where 配置好实体关系可支持复杂关系查询

DBContext

IEntityTypeConfiguration

关系配置

一对一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class User
{
public int Id { get; set; }
public string Name { get; set; }

// 一对一关系的导航属性
public Address Address { get; set; }
}

public class Address
{
public int Id { get; set; }
public string City { get; set; }
public string Street { get; set; }

// 一对一关系的外键
public int UserId { get; set; }
public User User { get; set; } // 导航属性
}

public class ApplicationDbContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Address> Addresses { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.HasOne(u => u.Address)
.WithOne(a => a.User)
.HasForeignKey<Address>(a => a.UserId); // 配置 Address.UserId 作为外键
}
}
一对多
  1. 典型的一对多关系

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    public class Blog
    {
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<Post> Posts { get; set; } // 一个博客可以包含多个文章
    }

    public class Post
    {
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public int BlogId { get; set; }
    public Blog Blog { get; set; }
    }

    public class ApplicationDbContext : DbContext
    {
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) {}

    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
    base.OnModelCreating(modelBuilder);

    //配置 Blog 和 Post 的多一关系 。 从post实体开始配置,当blog实体中 Posts属性被注释时, 单项导航
    //modelBuilder.Entity<Post>()
    // .HasOne(b => b.Blog) // 每个post有一个 blog
    // .WithMany() // Post 没有导航属性指向 Blog
    // .HasForeignKey(p => p.BlogId); // 外键设为 BlogId

    //配置 posts 和blog 一对多关系。 从blog 实体开始配置,当post实体中 Blog属性被注释时, 单项导航
    //modelBuilder.Entity<Blog>()
    // .HasMany(b => b.Posts)
    // .WithOne()
    // .HasForeignKey(p => p.BlogId);

    //配置 posts 和blog 一对多关系 双向导航
    modelBuilder.Entity<Blog>()
    .HasMany(b => b.Posts) // 每个blog有很多 posts
    .WithOne(o=>o.Blog) // Post 有一个 blog
    .HasForeignKey(p => p.BlogId); // 外键设为 BlogId

    // 双向导航
    modelBuilder.Entity<Post>()
    .HasOne(b => b.Blog) // 每个post有一个 blog
    .WithMany(o => o.Posts) // Blog 有一个或多个 post
    .HasForeignKey(p => p.BlogId); // 外键设为 BlogId
    }
    }

    在这里,HasOne 和 WithMany 方法分别表示一对多关系。HasForeignKey 指定 Post 实体中的 BlogId 字段作为外键。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    using (var context = new ApplicationDbContext(options))
    {
    var blog = context.Blogs
    .Include(b => b.Posts) // 加载 Blog 的 Posts
    .FirstOrDefault(b => b.Name == "Tech Blog");
    df
    Console.WriteLine($"Blog: {blog.Name}");
    foreach (var post in blog.Posts)
    {
    Console.WriteLine($"- Post Title: {post.Title}");
    }
    }

    使用 Include 方法在查询时加载相关数据。

  2. 自引用一对多关系

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class Category
    {
    public int Id { get; set; }
    public string Name { get; set; }

    // 自引用一对多关系
    public int? ParentCategoryId { get; set; } // 外键,允许为空
    public Category ParentCategory { get; set; } // 指向父类别的导航属性
    public ICollection<Category> SubCategories { get; set; } // 子类别集合
    }

    public class CategoryConfig : IEntityTypeConfiguration<Category>
    {
    public void Configure(EntityTypeBuilder<Category> builder)
    {
    builder.ToTable("T_Category");
    builder.HasKey(x => x.Id);
    builder.Property(x => x.Name).IsUnicode().IsRequired();
    builder.Property(x => x.ParentCategoryId).IsRequired(false);
    builder.HasOne(x => x.ParentCategory).WithMany(x => x.SubCategories).HasForeignKey("ParentCategoryId").OnDelete(DeleteBehavior.Restrict);
    }
    }
多对多

在 .NET Core 的 EF Core 中,从 EF Core 5.0 开始,已经支持直接配置 多对多关系,不需要再显式定义连接表(中间表)实体。可以使用简单的配置来实现多对多关系,并让 EF Core 自动创建连接表。
假设我们有两个实体:Student 和 Course。一个学生可以选修多门课程,而一门课程也可以有多个学生。这就是一个典型的多对多关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class Student
{
public int Id { get; set; }
public string Name { get; set; }

// 多对多关系的导航属性
public ICollection<Course> Courses { get; set; }
}

public class Course
{
public int Id { get; set; }
public string Title { get; set; }

// 多对多关系的导航属性
public ICollection<Student> Students { get; set; }
}

public class ApplicationDbContext : DbContext
{
public DbSet<Student> Students { get; set; }
public DbSet<Course> Courses { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>()
.HasMany(s => s.Courses)
.WithMany(c => c.Students)
.UsingEntity(j => j.ToTable("StudentCourses")); // 自定义连接表名称
}
}

UsingEntity(j => j.ToTable(“StudentCourses”)) 的作用是自定义连接表的名称为 StudentCourses,你可以根据需要设置连接表的其他属性。

以下是添加数据的示例,展示如何将学生与课程关联:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using (var context = new ApplicationDbContext())
{
// 创建课程
var course1 = new Course { Title = "Mathematics" };
var course2 = new Course { Title = "History" };

// 创建学生并添加课程
var student1 = new Student { Name = "Alice", Courses = new List<Course> { course1, course2 } };
var student2 = new Student { Name = "Bob", Courses = new List<Course> { course1 } };

context.Students.Add(student1);
context.Students.Add(student2);
context.SaveChanges();
}
  • 在 .NET Core EF Core 中,直接在两个实体类中定义集合导航属性即可实现多对多关系。
  • EF Core 自动创建连接表,并通过外键维护多对多关系。
  • 可以使用 .UsingEntity 方法自定义连接表的名称和配置。

Fluent api

DataAnnotation

ef core开启sql生成日志

IQueryable vs IEnumerable

1
2
3
4
5
// 使用 IQueryable,数据库执行过滤操作
IQueryable<Student> queryableStudents = context.Students.Where(s => s.Age > 18);

// 使用 IEnumerable,数据加载到内存后再执行过滤操作
IEnumerable<Student> enumerableStudents = context.Students.ToList().Where(s => s.Age > 18);

在这段代码中,queryableStudents 查询会在数据库中执行,而 enumerableStudents 查询会将所有学生数据加载到内存,再进行过滤。

特性 IQueryable IEnumerable
执行位置 数据库端 内存中
查询执行时机 延迟执行,调用 终结 操作时执行 立即执行
性能 更高,适合大数据集 较低,适合小数据集
用途 需要数据库端过滤、排序时 内存中操作数据,适合小数据集或已加载数据处理
终结方法: 遍历ToArray()ToList()Min()Max()Count()FirstOrDefault()等。
非终结方法: Take()Skip()Include()GroupBy()等

IQueryable可抽取公用的查询逻辑,靠终结方方法生成不同的sql语句,来实现逻辑代码复用。
在遍历IQueryable的过程中,由于IQueryable底层是ADO.NET DataReader数据流, 如果有耗时的业务逻辑,会长时间霸占数据库连接。这情况可让数据给提前执行终结方法,转成IEnumerable,在内存中逐条完成业务逻辑。
IQueryable和DbContext有强关联,DbContext被销毁后, 当前Context相关的Queryable不可用。
IQueryable底层是ADO.NET DataReader数据流, 各类型数据库都不支持同时开启多个DataReader数据流,如果嵌套遍历多个IQueryable会报错。

分页查询

在 EF Core 中实现分页查询非常简单,使用 SkipTake 方法即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public async Task<(List<Student> Students, int TotalPages)> GetPagedStudentsAsync(int pageNumber, int pageSize)
{
using (var context = new ApplicationDbContext())
{
int totalRecords = await context.Students.CountAsync(); // 总记录数
int totalPages = (int)Math.Ceiling(totalRecords / (double)pageSize); // 总页数

var students = await context.Students
.OrderBy(s => s.Id)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();

return (students, totalPages);
}
}

执行原生sql语句

用法 方法 参数化 内联查询 示例
查询返回实体数据 FromSqlRaw SqlParameter 不支持 context.Students.FromSqlRaw(…)
查询返回实体数据 FromSqlInterpolated 内插值 不支持、配置实体关系的实体支持 context.Students.FromSqlInterpolated($”SELECT * FROM Students WHERE Age > {age}”)
查询返回非实体数据 Set<T>().FromSqlRaw SqlParameter 支持 context.Set<StudentInfo>().FromSqlRaw(…)
查询返回非实体数据 Set<T().FromSqlInterpolated 内插值 支持 context.Set<StudentInfo>().($”SELECT Name, Age FROM Students WHERE Age > {minAge}”)
查询返回非实体数据 Database.SqlQuery<T> SqlParameter 支持 context.Database.SqlQuery<StudentInfo>(
$”SELECT Name, Age FROM Students WHERE Age > {ageLimit}”)
执行非查询 SQL 操作 ExecuteSqlRaw SqlParameter 支持 context.Database.ExecuteSqlRaw(…)
使用DbConnection Database.GetDbConnection() SqlParameter 支持 context.Database.GetDbConnection()
执行非查询 SQL 操作 ExecuteSqlInterpolated 内插值 支持 context.Database.ExecuteSqlInterpolated(…)
  • ExecuteSqlInterpolated执行自定义的sql语句使用内插值表达式 $"UPDATE Students SET Name = {newName} WHERE Age > {age}",对应的占位符会转换成参数sql语句。
  • FromSqlInterpolated执行自定义的sql语句使用内插值表达式 $"SELECT * FROM Students WHERE Age > {age}",对应的占位符会转换成参数sql语句

全局筛选器

允许对特定实体类型配置过滤逻辑,使得这些过滤条件在每次查询时自动应用。通常用于实现多租户系统、软删除(soft delete)或角色权限控制等场景。

  1. 配置全局过滤器
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    public class Product
    {
    public int Id { get; set; }
    public string Name { get; set; }
    public bool IsDeleted { get; set; } // 用于软删除的标志
    public int TenantId { get; set; } // 用于多租户
    }

    public class AppDbContext : DbContext
    {
    private readonly int _currentTenantId; // 当前租户 ID

    public AppDbContext(DbContextOptions<AppDbContext> options, int currentTenantId) : base(options)
    {
    _currentTenantId = currentTenantId; // 注入当前租户 ID
    }

    public DbSet<Product> Products { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
    base.OnModelCreating(modelBuilder);

    // 全局过滤器:软删除
    modelBuilder.Entity<Product>().HasQueryFilter(p => !p.IsDeleted);

    // 全局过滤器:多租户
    modelBuilder.Entity<Product>().HasQueryFilter(p => p.TenantId == _currentTenantId);
    }
    }

  2. 使用过滤器
    1
    2
    // 获取当前租户下所有未删除的产品
    var products = dbContext.Products.ToList();
    查询生成 上面的查询会自动生成以下 SQL(假设 _currentTenantId1):
    1
    2
    SELECT * FROM Products
    WHERE IsDeleted = 0 AND TenantId = 1;
  3. 暂时禁用全局过滤器
    1
    2
    // 获取所有产品,包括已删除和其他租户的  如果需要忽略全局过滤器,可以使用 `IgnoreQueryFilters`。
    var allProducts = dbContext.Products.IgnoreQueryFilters().ToList();

ef 实体状态跟踪

EF Core 会监控实体的状态,以便能够正确地同步它们与数据库的变更。每个实体都有一个状态,这些状态包括以下几种

  1. Added:表示实体是新创建的,尚未存在于数据库中。EF Core 会在保存更改时插入一个新记录。
  2. Modified:表示实体已经加载,并且某些属性已被修改。EF Core 会在保存更改时生成更新 SQL 命令。
  3. Deleted:表示实体已经被标记为删除。EF Core 会在保存更改时生成删除 SQL 命令。
  4. Unchanged:表示实体的状态没有变化,EF Core 不会为其生成任何 SQL 命令。
  5. Detached:表示实体没有与任何 DbContext 关联,EF Core 不会跟踪该实体的状态。

EF Core 通过 Change Tracker 来监控实体的状态。ChangeTracker 是 DbContext 的一个组件,它跟踪所有实体的状态。每当对实体进行更改时,EF Core 会更新实体的状态,并记录哪些属性发生了变化。
以下是 EF Core 监控实体状态的基本过程:

  1. 实体的加载: 当你从数据库加载实体时,EF Core 会将这些实体的状态标记为 Unchanged。如果在查询期间或加载后修改了实体的属性,EF Core 会将它的状态更新为 Modified。
  2. 对实体的修改: 当你修改实体的属性(如 Name = “New Name”)时,EF Core 会自动将实体的状态更改为 Modified,并记录下哪些属性发生了变化。
  3. 添加新实体: 当你使用 DbContext.Add 方法添加一个新实体时,EF Core 会将其状态标记为 Added。
  4. 删除实体:当你使用 DbContext.Remove 或 DbSet.Remove 删除实体时,EF Core 会将其状态标记为 Deleted。
  5. 保存更改: 调用 DbContext.SaveChanges() 时,EF Core 会检查所有跟踪的实体的状态,并生成相应的 SQL 命令(插入、更新或删除)。EF Core 会将所有状态为 Added、Modified 或 Deleted 的实体对应的数据库操作批量提交。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    static void Main(string[] args)
    {
    using MyDbContext context = new MyDbContext();
    var student = context.Students.Single(r=>r.Id==3);

    var entityEntry = context.Entry(student);
    Console.WriteLine($"Student Status: {entityEntry.State}"); // 输出 "Unchanged"

    // 修改属性,EF Core 会标记该实体为 "Modified"
    student.Name = "Updated Name";

    // 查看该实体的状态
    entityEntry = context.Entry(student);
    Console.WriteLine($"Student Status: {entityEntry.State}"); // 输出 "Modified"

    // 添加新实体,EF Core 会标记该实体为 "Added"
    var newStudent = new Student { Name = "New Student", Age = 20 };
    entityEntry = context.Entry(newStudent);
    Console.WriteLine($"New Student Status: {entityEntry.State}"); // 输出 "Detached"

    context.Students.Add(newStudent);
    entityEntry = context.Entry(newStudent);
    Console.WriteLine($"New Student Status: {entityEntry.State}"); // 输出 "Added"

    // 删除实体,EF Core 会标记该实体为 "Deleted"
    context.Students.Remove(student);
    entityEntry = context.Entry(student);
    Console.WriteLine($"Deleted Student Status: {entityEntry.State}"); // 输出 "Deleted"

    // 保存更改到数据库
    context.SaveChanges();
    }

EF Core 会自动检测实体属性的变化。具体来说,当加载一个实体后,EF Core 会在内存中保存实体的原始值。当你修改实体的某个属性时,EF Core 会比较该属性的新值和原始值。如果新值不同,它会将该属性标记为已修改,且会在 SaveChanges() 时生成更新 SQL。

如果你手动修改实体的状态,可以通过 ChangeTracker 显式地指定实体的状态。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
static void Main(string[] args)
{
using MyDbContext context = new MyDbContext();
var student = context.Students.Single(r => r.Id == 4);
context.Entry(student).State = EntityState.Modified; // 手动标记为 "Modified"
context.SaveChanges(); //UPDATE [Students] SET [Age] = @p0, [Name] = @p1 WHERE [Id] = @p2;


var newstudent=new Student{Id=4,Age=21};
var entity1 = context.Entry(student);
entity1.Property("Age").IsModified = true;
context.SaveChanges(); //UPDATE [Students] SET [Age] = @p0 WHERE [Id] = @p1;
}

默认情况下,当你从数据库中查询实体时,Entity Framework会跟踪这些实体的状态。这意味着EF Core会维护一个实体快照,以监控实体的属性是否被修改、删除或添加。
然而,在某些情况下,你可能不需要EF Core跟踪实体的状态。例如,当你从数据库中读取数据仅仅是为了显示给用户,而不打算对这些数据进行任何修改时,跟踪实体的状态是不必要的,而且可能会导致性能下降,因为EF Core需要额外的资源来维护这些实体的状态。

在这种情况下,使用AsNoTracking()方法来告诉EF Core不要跟踪查询返回的实体。这意味着这些实体将作为“断开的”或“非托管”实体存在,EF Core不会监测它们的更改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using (var context = new ApplicationDbContext())
{
// 执行一个只读查询,不需要跟踪实体变化
var students = context.Students
.AsNoTracking() // 禁用实体跟踪
.Where(s => s.Age > 18)
.ToList();

foreach (var student in students)
{
Console.WriteLine(student.Name);
}

// 注意:如果你想对学生进行修改,EF Core 不会自动跟踪它们的变化
// 你需要手动将实体状态设置为 Modified 或重新附加它们
}

ef 批量更新插入问题

为什么 EF Core 默认不批量更新?

  1. 变更追踪机制:EF Core 的变更追踪机制可以识别出每个实体对象的具体更改,并只针对修改的属性生成相应的更新 SQL。这种方式很适合单个实体或少量实体的 CRUD 操作,但不适合大量数据的批量操作。
  2. 关系和级联更新:EF Core 支持复杂的实体关系(如一对多、多对多等),在更新实体时还会考虑相关导航属性的变化。逐条更新可以确保这些关系的正确性和一致性。
  3. 通用兼容性:EF Core 需要兼容多种数据库,每种数据库对于批量更新的支持程度和实现方式不同。因此,EF Core 选择了更通用的逐条更新模式,以确保所有数据库提供一致的行为和结果。

efcore 7.0版本后提供的批量方法

  1. 批量添加AddRange

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
            static async Task Main(string[] args)
    {
    using var context = new ApplicationDbContext();
    var s1 = new Student { Name = "Student1" };
    var s2 = new Student { Name = "Student2" };
    var s3 = new Student { Name = "Student3" };

    await context.Students.AddRangeAsync(s1, s2, s3);
    await context.SaveChangesAsync();
    /* 生成的sql
    * exec sp_executesql N'SET IMPLICIT_TRANSACTIONS OFF;
    SET NOCOUNT ON;
    MERGE [Students] USING (
    VALUES (@p0, 0),
    (@p1, 1),
    (@p2, 2)) AS i ([Name], _Position) ON 1=0
    WHEN NOT MATCHED THEN
    INSERT ([Name])
    VALUES (i.[Name])
    OUTPUT INSERTED.[Id], i._Position;
    ',N'@p0 nvarchar(4000),@p1 nvarchar(4000),@p2 nvarchar(4000)',@p0=N'Student1',@p1=N'Student2',@p2=N'Student3'
    * */
    }

    虽然 AddRange 比逐个调用 Add 更高效(因为它减少了多次数据库往返),但它并不是完全的“批量操作”。每个实体的插入操作仍然是由 EF Core 分别处理,并且每个实体会占用一定的内存。
    对于非常大的批量插入(例如,数千个或更多实体),如果性能成为问题,可能需要考虑使用第三方库(如 EFCore.BulkExtensions)来执行更高效的批量插入,或者直接使用原生 SQL 来插入数据。

  2. 批量更新ExecuteUpdate

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    static async Task Main(string[] args)
    {
    using var context = new ApplicationDbContext();
    var s1 = new Student { Name = "Student1" };
    var s2 = new Student { Name = "Student2" };
    var s3 = new Student { Name = "Student3" };

    await context.Students.AddRangeAsync(s1, s2, s3);
    await context.SaveChangesAsync();
    //`ExecuteUpdate` 允许你一次性更新符合条件的多个记录。
    await context.Students.ExecuteUpdateAsync(p => p.SetProperty(p => p.Name, p => "update"));
    /*
    * UPDATE [s] SET [s].[Name] = N'update' FROM [Students] AS [s]
    */

    // await context.Students.Where(r => r.Name == "update").ExecuteDeleteAsync();
    }
  3. 批量修改ExecuteDelete

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
        static async Task Main(string[] args)
    {
    using var context = new ApplicationDbContext();
    var s1 = new Student { Name = "Student1" };
    var s2 = new Student { Name = "Student2" };
    var s3 = new Student { Name = "Student3" };

    await context.Students.AddRangeAsync(s1, s2, s3);
    await context.SaveChangesAsync();
    await context.Students.Where(r => r.Age == 0).ExecuteUpdateAsync(p => p
    .SetProperty(p => p.Name, "update")
    .SetProperty(p => p.Age, 18)
    );
    /*
    * UPDATE [s] SET [s].[Age] = 18,[s].[Name] = N'update' FROM [Students] AS [s] WHERE [s].[Age] = 0
    */
    await context.Students.Where(r => r.Name == "update").ExecuteDeleteAsync();
    /*
    * DELETE FROM [s] WHERE [s].[Name] = N'update' FROM [Students] AS [s]
    */
    }
    }

使用第三方类库执行批量方法EFCore.BulkExtensions

efcore使用原生sql执行批量方法

数据迁移Migration

add-migration

update-datebase

script-migration

scaffold-dbcontext 反向工程

ef如何兼容多种数据库

image

ef core 核心

AST

EF Core 在处理 LINQ 查询时,会将查询表达式解析为表达式树,并在此基础上生成查询的抽象语法树(AST , Abstract Syntax Tree,抽象语法树),AST 是一个高度抽象的、数据库无关的表示,它描述了查询的逻辑结构,比如过滤、投影、排序等。

  • 创建表达式树:当编写 LINQ 查询时,C# 编译器会将查询表达式转换为表达式树。例如,Where、Select、OrderBy 等操作会被表达为表达式树的节点。
  • 解析表达式树:EF Core 查询管道会分析表达式树,并将它转换成内部的 AST 结构。这个过程会解析查询的结构和操作符,包括过滤、排序、投影、聚合等。
  • 优化和重写:在生成 AST 之后,EF Core 会对 AST 进行一些常见的优化和重写,例如合并 Where 过滤条件、消除不必要的查询等。
  • 交给对用的 ef core provide 等待对应provider解析ast生成sql,返回具体结果

EF core中的ast节点类型

  • QueryRootExpression:代表查询的根节点(即数据源)。
  • ProjectionExpression:代表查询的投影部分,通常对应于 Select 子句。
  • FilterExpression:代表 Where 子句。
  • OrderingExpression:代表 OrderBy 或 OrderByDescending 子句。
  • AggregateExpression:代表聚合操作,如 Count、Sum 等。
  • JoinExpression:代表连接操作。
  • ConstantExpression:代表常量值,例如 true、1、”test” 等。
  • BinaryExpression:代表二元运算符,例如 ==、>、< 等。

ef core provider

数据库提供程序(Database Providers)是实现跨数据库兼容的关键模块。它负责将数据库无关的查询(如 LINQ)翻译成特定数据库所需的 SQL,并管理与数据库交互的细节。

  1. SQL 生成和查询翻译 AST 转 SQL
  2. 数据库连接和事务管理
  3. 类型映射和特性适配,确保数据类型兼容。
  4. 支持数据库的特定特性,如分页、并发控制、存储过程等。
  5. 支持数据库的迁移、创建、删除和模式管理。
  6. 提供异步支持和延迟加载的实现。

常用的Database Providers

  • SQL Server:Microsoft.EntityFrameworkCore.SqlServer
  • SQLite:Microsoft.EntityFrameworkCore.Sqlite
  • MySQL:Pomelo.EntityFrameworkCore.MySql 或 MySql.EntityFrameworkCore
  • PostgreSQL:Npgsql.EntityFrameworkCore.PostgreSQL
  • Oracle :

ado.net provider

在 .NET 中,ADO.NET Provider(ADO.NET 数据提供程序)是负责在 .NET 应用程序与数据库之间进行数据通信的组件。它提供了一组 API 和类,用于执行数据库连接、查询、更新和事务管理等操作。

ADO.NET Provider 是一个数据访问接口层,为不同的数据库提供一致的 API,主要职责包括以下几个方面:

  1. 数据库连接 Connection ( SqlConnection、MySqlConnection 等)
  2. 数据库命令执行 Command (SqlCommand、MySqlCommand 等
  3. 数据读取 DataReader (SqlDataReader、MySqlDataReader)
  4. 参数化查询支持 Parameter
  5. 数据适配与缓存 DataAdapter
  6. 事务管理 Transaction
  7. 异常处理和错误管理 SqlException

常见的 ADO.NET Provider

  • SQL Server:Microsoft.Data.SqlClient 或 System.Data.SqlClient
  • SQLite: Microsoft.Data.Sqlite 或 System.Data.SQLite
  • MySQL: MySqlConnector 或 MySql.Data.MySqlClient
  • PostgreSQL: Npgsql
  • Oracle: Oracle.ManagedDataAccess.Client

nuget查询后,通过包管理器控制台安装

核心包

1
NuGet\Install-Package Microsoft.EntityFrameworkCore -Version 6.0.35

ms sqlserver依赖包

1
NuGet\Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 6.0.35

数据库迁移工具包

1
NuGet\Install-Package Microsoft.EntityFrameworkCore.Tools -Version 6.0.35

迁移数据库报错:

1
2
3
Unable to create an object of type "MyDbContext'. For the different patterns supported at
8g
design time, see https://go.microsoft.com/fwlink/?linkid=851728

处理方案

1
2
3
4
5
6
7
8
9
10
public class BloggingContextFactory : IDesignTimeDbContextFactory<BloggingContext>
{
public BloggingContext CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder<BloggingContext>();
optionsBuilder.UseSqlite("Data Source=blog.db");

return new BloggingContext(optionsBuilder.Options);
}
}
Command Usage
Add-Migration Adds a new migration.
Bundle-Migratio Creates an executable to update the database.
Drop-Database Drops the database.
Get-DbContext Gets information about a DbContext type.
Get-Help EntityFramework Displays information about Entity Framework commands.
Get-Migration Lists available migrations.
Optimize-DbContext Generates a compiled version of the model used by the DbContext.
Remove-Migration Removes the last migration.
Scaffold-DbContext Generates a DbContext and entity type classes for a specified database.
Script-DbContext Generates a SQL script from the DbContext. Bypasses any migrations.
Script-Migration Generates a SQL script from the migrations.
Update-Database Updates the database to the last migration or to a specified migration.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
## 第一次执行
Add-Migration Init
Update-Database

## 添加删除字段后
Add-Migration Add xxxx
Update-Database

````
## ef并发控制

### 悲观并发

**悲观并发控制**(Pessimistic Concurrency Control)依赖于数据库锁机制,在操作数据时锁定特定的行或表,确保其他事务无法修改数据直到当前事务完成。EF Core 本身没有直接的 API 提供悲观锁功能,但可以通过原生 SQL 或事务完成。
1. 数据定义
```C#
public class House
{
public int Id { get; set; }
public string RoomNo { get; set; }
public string Owner { get; set; }
}

public class HouseContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.LogTo(Console.WriteLine);
optionsBuilder.UseSqlServer("Data Source=(localdb)\\mssqllocaldb;database=House;TrustServerCertificate=true;integrated security=True;MultipleActiveResultSets=true");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<House>(entity =>
{
entity.ToTable("T_House");
entity.HasKey(e => e.Id);
entity.Property(e => e.RoomNo).IsRequired();
entity.Property(e => e.Owner).IsRequired(false).IsUnicode(false).HasMaxLength(20);
});
}

public DbSet<House> Houses { get; set; }
}
  1. 显式使用事务和锁级别
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    class Program
    {
    private readonly static Dictionary<string, ConsoleColor> _buyer = new Dictionary<string, ConsoleColor> { { "tom", ConsoleColor.Red }, { "jerry", ConsoleColor.Green } };
    static async Task Main(string[] args)
    {
    using var db = new HouseContext();
    var house = db.Houses.Single(r => r.Id == 1);
    house.Owner = null;
    db.SaveChanges();

    foreach (var item in _buyer)
    {
    _ = Task.Run(async () =>
    {
    var name = item.Key;
    try
    {
    ConsoleWrite($"{Thread.CurrentThread.ManagedThreadId}", name);
    await SnapUp2(item.Key);
    }
    catch (Exception ex)
    {
    ConsoleWrite($"{ex.Message}", name);
    }
    });
    }
    Console.ReadLine();

    //无锁版本
    async Task SnapUp(string name)
    {
    using var db = new HouseContext();
    ConsoleWrite(DateTime.Now + "准备开始select", name);
    var house = await db.Houses.SingleAsync(r => r.Id == 1);
    ConsoleWrite(DateTime.Now + "已完成select", name);
    if (!string.IsNullOrEmpty(house.Owner))
    {
    ConsoleWrite($"----无效房源,房子已经被【{house.Owner}】抢购", name);
    return;
    }

    house.Owner = name;
    await Task.Delay(5000);
    await db.SaveChangesAsync();
    ConsoleWrite(DateTime.Now + $"房源被【{name}】抢购成功", name);
    }


    async Task SnapUp2(string name)
    {
    using var db = new HouseContext();
    ConsoleWrite(DateTime.Now + "准备开始select", name);
    await db.Database.BeginTransactionAsync();
    var house = db.Houses.FromSqlInterpolated($"select * from T_House with(ROWLOCK,UPDLOCK) where Id=1").FirstOrDefault();
    ConsoleWrite(DateTime.Now + "已完成select", name);
    if (house == null)
    {
    ConsoleWrite($"----无效房源", name);
    return;
    }
    if (!string.IsNullOrWhiteSpace(house.Owner))
    {
    ConsoleWrite($"----无效房源,房子已经被【{house.Owner}】抢购", name);
    return;
    }

    house.Owner = name;
    await Task.Delay(5000);
    await db.SaveChangesAsync();
    await db.Database.CommitTransactionAsync();
    ConsoleWrite(DateTime.Now + $"房源被【{name}】抢购成功", name);
    }
    }
    static void ConsoleWrite(string message, string name)
    {
    _buyer.TryGetValue(name, out var color);
    Console.ForegroundColor = color;
    Console.WriteLine($"{name}----------{message}");
    Console.ResetColor();
    }
    }
  2. 运行结果
    1
    2
    3
    4
    5
    6
    7
    8
    tom----------9
    jerry----------4
    jerry----------2024/11/27 15:28:10准备开始select
    tom----------2024/11/27 15:28:10准备开始select
    tom----------2024/11/27 15:28:11已完成select
    jerry----------2024/11/27 15:28:16已完成select
    tom----------2024/11/27 15:28:16房源被【tom】抢购成功
    jerry--------------无效房源,房子已经被【tom】抢购

乐观控制

  • **Concurrency Token : 用来检测并发冲突的字段,可以是任何字段,比如逻辑字段(PriceStock 等)或特殊的版本字段.
  • RowVersion
    • RowVersion 是数据库(如 SQL Server)中特殊的数据类型,常用来标记表中行的版本号。
    • 每次行被修改时,RowVersion 会自动生成一个新的值,通常是唯一且递增的二进制数据。
    • 在 EF Core 中,RowVersion 可以直接用作 Concurrency Token,避免开发者手动管理并发标记字段。
    • RowVersion 是一种更高效、更可靠的 Concurrency Token。

工作机制的核心

1. 原始值跟踪

  • EF 会在数据加载时记录被标记为 并发标记(Concurrency Token) 的属性值。
  • 在保存更改时,这些值会作为“原始值”与数据库中当前的值进行对比。
    2. 数据库层次的条件检查
  • 在生成的 UPDATEDELETE 语句中,会在 WHERE 子句中加入并发检查的条件(通常是属性的原始值)。
  • 如果 WHERE 条件未匹配到任何行,EF 会认为发生了并发冲突。
    3. 冲突处理
  • 当数据库中的值与内存中的原始值不匹配时,EF 抛出 DbUpdateConcurrencyException
  • 开发者可以捕获此异常并采取适当的操作,例如提示用户或重试操作。

EF 执行的 SQL 示例

假设我们有一个实体类 Product,并在 Price 属性上使用了 [ConcurrencyCheck]

1
2
3
4
5
6
7
8
public class Product
{
public int Id { get; set; }
public string Name { get; set; }

[ConcurrencyCheck]
public decimal Price { get; set; }
}

在修改 Price 时,EF 会生成类似以下的 SQL 查询:

  1. 数据加载
    1
    2
    3
    SELECT [Id], [Name], [Price]
    FROM [Products]
    WHERE [Id] = 1;
  2. 更新数据
    1
    2
    3
    UPDATE [Products]
    SET [Price] = 25.0
    WHERE [Id] = 1 AND [Price] = 20.0;
  • WHERE [Price] = 20.0
    • EF 在更新时会检查数据库中 Price 的当前值是否与原始值(20.0)一致。
    • 如果一致,则更新成功。
    • 如果不一致,则不会更新任何行,并抛出并发异常。

实现方式

Concurrency Token

数据定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class House
{
public int Id { get; set; }
public string RoomNo { get; set; }
public string Owner { get; set; }
}

public class HouseContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
//optionsBuilder.LogTo(Console.WriteLine);
optionsBuilder.UseSqlServer("Data Source=(localdb)\\mssqllocaldb;database=House;TrustServerCertificate=true;integrated security=True;MultipleActiveResultSets=true");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<House>(entity =>
{
entity.ToTable("T_House");
entity.HasKey(e => e.Id);
entity.Property(e => e.RoomNo).IsRequired();
entity.Property(e => e.Owner).IsConcurrencyToken().IsRequired(false).IsUnicode(true).HasMaxLength(20);
});
}
public DbSet<House> Houses { get; set; }
}
  1. 并发控制
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    class Program
    {
    private readonly static Dictionary<string, ConsoleColor> _buyer = new Dictionary<string, ConsoleColor> { { "tom", ConsoleColor.Red }, { "jerry", ConsoleColor.Green } };
    static async Task Main(string[] args)
    {
    using var db = new HouseContext();
    var house = db.Houses.Single(r => r.Id == 1);
    house.Owner = null;
    db.SaveChanges();

    foreach (var item in _buyer)
    {
    _ = Task.Run(async () =>
    {
    var name = item.Key;
    try
    {
    ConsoleWrite($"{Thread.CurrentThread.ManagedThreadId}", name);
    await SnapUp(item.Key);
    }
    catch (Exception ex)
    {
    ConsoleWrite($"{ex.Message}", name);
    }
    });
    }
    Console.ReadLine();

    //Concurrency Token
    async Task SnapUp(string name)
    {
    using var db = new HouseContext();
    ConsoleWrite(DateTime.Now + "准备开始select", name);
    var house = await db.Houses.SingleAsync(r => r.Id == 1);
    ConsoleWrite(DateTime.Now + "已完成select", name);
    if (!string.IsNullOrEmpty(house.Owner))
    {
    ConsoleWrite($"----无效房源,房子已经被【{house.Owner}】抢购", name);
    return;
    }

    house.Owner = name;
    await Task.Delay(5000);
    try
    {
    await db.SaveChangesAsync();
    ConsoleWrite(DateTime.Now + $"房源被【{name}】抢购成功", name);
    }
    catch (DbUpdateConcurrencyException ex)
    {
    foreach (var entry in ex.Entries)
    {
    // 获取数据库中的最新值
    var dbValues = entry.GetDatabaseValues();
    var clientValues = entry.CurrentValues;
    //foreach (var property in dbValues.Properties)
    //{
    // Console.WriteLine($"Database: {property.Name} = {dbValues[property]}");
    // Console.WriteLine($"Client: {property.Name} = {clientValues[property]}");
    //}
    var owner = dbValues["Owner"].ToString();
    ConsoleWrite(DateTime.Now + $"抢购失败 ,房源已被【{owner}】抢购", name);
    return;
    }
    }

    }

    }
    static void ConsoleWrite(string message, string name)
    {
    _buyer.TryGetValue(name, out var color);
    Console.ForegroundColor = color;
    Console.WriteLine($"{name}----------{message}");
    Console.ResetColor();
    }
    }
  2. 运行结果
    1
    2
    3
    4
    5
    6
    7
    8
    jerry----------6
    tom----------10
    tom----------2024/11/27 15:22:13准备开始select
    jerry----------2024/11/27 15:22:13准备开始select
    tom----------2024/11/27 15:22:13已完成select
    jerry----------2024/11/27 15:22:13已完成select
    jerry----------2024/11/27 15:22:18房源被【jerry】抢购成功
    tom----------2024/11/27 15:22:18抢购失败 ,房源已被【jerry】抢购
RowVersion
  1. 数据定义
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    public class House
    {
    public int Id { get; set; }
    public string RoomNo { get; set; }
    public string Owner { get; set; }
    // [Timestamp] // 定义为 RowVersion 类型
    public byte[] RowVersion { get; set; }
    }

    public class HouseContext : DbContext
    {
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
    //optionsBuilder.LogTo(Console.WriteLine);
    optionsBuilder.UseSqlServer("Data Source=(localdb)\\mssqllocaldb;database=House;TrustServerCertificate=true;integrated security=True;MultipleActiveResultSets=true");
    }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
    modelBuilder.Entity<House>(entity =>
    {
    entity.ToTable("T_House");
    entity.HasKey(e => e.Id);
    entity.Property(e => e.RoomNo).IsRequired();
    entity.Property(e => e.Owner).IsRequired(false).IsUnicode(true).HasMaxLength(20);
    entity.Property(e => e.RowVersion).IsRowVersion();
    });
    }
    public DbSet<House> Houses { get; set; }
    }
  2. 并发控制 同上面Concurrency Token用户用法一致,RowVersion 也是 Concurrency Token 的一种。
由 Hexo 驱动 & 主题 Keep