ef core
1 | NuGet\Install-Package Microsoft.EntityFrameworkCore ##必须的核心包 |
创建模型类,完成模型配置
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");
}
}定义
DbContext1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public 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配置
}
}创建数据库迁移
1
2Add-Migration Init
Update-Database使用
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
43internal 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 | public class User |
一对多
典型的一对多关系
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
52public 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
12using (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 方法在查询时加载相关数据。
自引用一对多关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public 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 | public class Student |
UsingEntity(j => j.ToTable(“StudentCourses”)) 的作用是自定义连接表的名称为 StudentCourses,你可以根据需要设置连接表的其他属性。
以下是添加数据的示例,展示如何将学生与课程关联:
1 | using (var context = new ApplicationDbContext()) |
- 在 .NET Core EF Core 中,直接在两个实体类中定义集合导航属性即可实现多对多关系。
- EF Core 自动创建连接表,并通过外键维护多对多关系。
- 可以使用 .UsingEntity 方法自定义连接表的名称和配置。
Fluent api
DataAnnotation
ef core开启sql生成日志
IQueryable vs IEnumerable
1 | // 使用 IQueryable,数据库执行过滤操作 |
在这段代码中,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 中实现分页查询非常简单,使用 Skip 和 Take 方法即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public 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
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
31public 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);
}
} - 使用过滤器查询生成 上面的查询会自动生成以下 SQL(假设
1
2// 获取当前租户下所有未删除的产品
var products = dbContext.Products.ToList();_currentTenantId为1):1
2SELECT * FROM Products
WHERE IsDeleted = 0 AND TenantId = 1; - 暂时禁用全局过滤器
1
2// 获取所有产品,包括已删除和其他租户的 如果需要忽略全局过滤器,可以使用 `IgnoreQueryFilters`。
var allProducts = dbContext.Products.IgnoreQueryFilters().ToList();
ef 实体状态跟踪
EF Core 会监控实体的状态,以便能够正确地同步它们与数据库的变更。每个实体都有一个状态,这些状态包括以下几种
- Added:表示实体是新创建的,尚未存在于数据库中。EF Core 会在保存更改时插入一个新记录。
- Modified:表示实体已经加载,并且某些属性已被修改。EF Core 会在保存更改时生成更新 SQL 命令。
- Deleted:表示实体已经被标记为删除。EF Core 会在保存更改时生成删除 SQL 命令。
- Unchanged:表示实体的状态没有变化,EF Core 不会为其生成任何 SQL 命令。
- Detached:表示实体没有与任何 DbContext 关联,EF Core 不会跟踪该实体的状态。
EF Core 通过 Change Tracker 来监控实体的状态。ChangeTracker 是 DbContext 的一个组件,它跟踪所有实体的状态。每当对实体进行更改时,EF Core 会更新实体的状态,并记录哪些属性发生了变化。
以下是 EF Core 监控实体状态的基本过程:
- 实体的加载: 当你从数据库加载实体时,EF Core 会将这些实体的状态标记为 Unchanged。如果在查询期间或加载后修改了实体的属性,EF Core 会将它的状态更新为 Modified。
- 对实体的修改: 当你修改实体的属性(如 Name = “New Name”)时,EF Core 会自动将实体的状态更改为 Modified,并记录下哪些属性发生了变化。
- 添加新实体: 当你使用 DbContext.Add 方法添加一个新实体时,EF Core 会将其状态标记为 Added。
- 删除实体:当你使用 DbContext.Remove 或 DbSet.Remove 删除实体时,EF Core 会将其状态标记为 Deleted。
- 保存更改: 调用 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
32static 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 | static void Main(string[] args) |
默认情况下,当你从数据库中查询实体时,Entity Framework会跟踪这些实体的状态。这意味着EF Core会维护一个实体快照,以监控实体的属性是否被修改、删除或添加。
然而,在某些情况下,你可能不需要EF Core跟踪实体的状态。例如,当你从数据库中读取数据仅仅是为了显示给用户,而不打算对这些数据进行任何修改时,跟踪实体的状态是不必要的,而且可能会导致性能下降,因为EF Core需要额外的资源来维护这些实体的状态。
在这种情况下,使用AsNoTracking()方法来告诉EF Core不要跟踪查询返回的实体。这意味着这些实体将作为“断开的”或“非托管”实体存在,EF Core不会监测它们的更改。
1 | using (var context = new ApplicationDbContext()) |
ef 批量更新插入问题
为什么 EF Core 默认不批量更新?
- 变更追踪机制:EF Core 的变更追踪机制可以识别出每个实体对象的具体更改,并只针对修改的属性生成相应的更新 SQL。这种方式很适合单个实体或少量实体的 CRUD 操作,但不适合大量数据的批量操作。
- 关系和级联更新:EF Core 支持复杂的实体关系(如一对多、多对多等),在更新实体时还会考虑相关导航属性的变化。逐条更新可以确保这些关系的正确性和一致性。
- 通用兼容性:EF Core 需要兼容多种数据库,每种数据库对于批量更新的支持程度和实现方式不同。因此,EF Core 选择了更通用的逐条更新模式,以确保所有数据库提供一致的行为和结果。
efcore 7.0版本后提供的批量方法
批量添加
AddRange1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23static 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 来插入数据。批量更新
ExecuteUpdate1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17static 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();
}批量修改
ExecuteDelete1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22static 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如何兼容多种数据库

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,并管理与数据库交互的细节。
- SQL 生成和查询翻译 AST 转 SQL
- 数据库连接和事务管理
- 类型映射和特性适配,确保数据类型兼容。
- 支持数据库的特定特性,如分页、并发控制、存储过程等。
- 支持数据库的迁移、创建、删除和模式管理。
- 提供异步支持和延迟加载的实现。
常用的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,主要职责包括以下几个方面:
- 数据库连接 Connection ( SqlConnection、MySqlConnection 等)
- 数据库命令执行 Command (SqlCommand、MySqlCommand 等
- 数据读取 DataReader (SqlDataReader、MySqlDataReader)
- 参数化查询支持 Parameter
- 数据适配与缓存 DataAdapter
- 事务管理 Transaction
- 异常处理和错误管理 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 | Unable to create an object of type "MyDbContext'. For the different patterns supported at |
处理方案
1 | public class BloggingContextFactory : IDesignTimeDbContextFactory<BloggingContext> |
| 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 | ## 第一次执行 |
- 显式使用事务和锁级别
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
81class 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();
}
} - 运行结果
1
2
3
4
5
6
7
8tom----------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: 用来检测并发冲突的字段,可以是任何字段,比如逻辑字段(Price、Stock等)或特殊的版本字段. RowVersionRowVersion是数据库(如 SQL Server)中特殊的数据类型,常用来标记表中行的版本号。- 每次行被修改时,
RowVersion会自动生成一个新的值,通常是唯一且递增的二进制数据。 - 在 EF Core 中,
RowVersion可以直接用作 Concurrency Token,避免开发者手动管理并发标记字段。 RowVersion是一种更高效、更可靠的 Concurrency Token。
工作机制的核心
1. 原始值跟踪
- EF 会在数据加载时记录被标记为
并发标记(Concurrency Token)的属性值。 - 在保存更改时,这些值会作为“原始值”与数据库中当前的值进行对比。
2. 数据库层次的条件检查 - 在生成的
UPDATE或DELETE语句中,会在WHERE子句中加入并发检查的条件(通常是属性的原始值)。 - 如果
WHERE条件未匹配到任何行,EF 会认为发生了并发冲突。
3. 冲突处理 - 当数据库中的值与内存中的原始值不匹配时,EF 抛出
DbUpdateConcurrencyException。 - 开发者可以捕获此异常并采取适当的操作,例如提示用户或重试操作。
EF 执行的 SQL 示例
假设我们有一个实体类 Product,并在 Price 属性上使用了 [ConcurrencyCheck]:
1 | public class Product |
在修改 Price 时,EF 会生成类似以下的 SQL 查询:
- 数据加载
1
2
3SELECT [Id], [Name], [Price]
FROM [Products]
WHERE [Id] = 1; - 更新数据
1
2
3UPDATE [Products]
SET [Price] = 25.0
WHERE [Id] = 1 AND [Price] = 20.0;
WHERE [Price] = 20.0:- EF 在更新时会检查数据库中
Price的当前值是否与原始值(20.0)一致。 - 如果一致,则更新成功。
- 如果不一致,则不会更新任何行,并抛出并发异常。
- EF 在更新时会检查数据库中
实现方式
Concurrency Token
数据定义
1 | public class House |
- 并发控制
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
77class 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();
}
} - 运行结果
1
2
3
4
5
6
7
8jerry----------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
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
29public 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; }
} - 并发控制 同上面Concurrency Token用户用法一致,
RowVersion也是 Concurrency Token 的一种。