ASP.NET Core 中的 ORM 之 Entity Framework

目录 EF Core 简介 使用 EF Core(Code First) EF Core 中的一些常用知识点 实体建模 实体关系 种子数据 并发管理 执行 SQL 语句和存储过程 延迟加载和预先加载 IQueryable 和 IEnumerable 生成迁移 SQL 脚本 待补充... SQL 监视工具 仓储模式和工作单元模式 使用 EF Core(DB First) 源代码 EF Core 简介 Entity Framework Core 是微软自家的 ORM 框架。作为 .Net Core 生态中的一个重要组成部分,它是一个支持跨平台的全新版本,用三个词来概况 EF Core 的特点:轻量级、可扩展、跨平台。 目前 EF Core 支持的数据库: Microsoft SQL Server SQLite Postgres (Npgsql) SQL Server Compact Edition InMemory (for testing purposes) MySQL IBM DB2 Oracle Firebird 使用 EF Core(Code First) 新建一个 WebAPI 项目 通过 Nuget 安装 EF Core 引用 // SQL Server Install-Package Microsoft.EntityFrameworkCore.SqlServer 其他数据库请查看:https://docs.microsoft.com/zh-cn/ef/core/providers/ 添加实体 public class Blog { public int BlogId { get; set; } public string Url { get; set; } public int Rating { get; set; } public List Posts { get; set; } } public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public int BlogId { get; set; } public Blog Blog { get; set; } } 添加数据库上下文 public class BloggingContext : DbContext { public DbSet Blogs { get; set; } public DbSet Posts { get; set; } } 有两种方式配置数据库连接,一种是注册 Context 的时候提供 options。比较推荐这种方式。 public class BloggingContext : DbContext { public BloggingContext(DbContextOptions options) : base(options) { } public DbSet Blogs { get; set; } public DbSet Posts { get; set; } } 在 Startup 中配置 public void ConfigureServices(IServiceCollection services) { var connectionString = @"Server=.;Database=Blogging;Trusted_Connection=True;"; services.AddDbContext(o => o.UseSqlServer(connectionString)); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); } 一种是重载 OnConfiguring 方法提供连接字符串: public class BloggingContext : DbContext { public DbSet Blogs { get; set; } public DbSet Posts { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Server=.;Database=Blogging;Trusted_Connection=True;"); base.OnConfiguring(optionsBuilder); } } 在Controller 中使用 Context public class BlogsController : ControllerBase { private readonly BloggingContext _context; public BlogsController(BloggingContext context) { _context = context; } // GET: api/Blogs [HttpGet] public IEnumerable GetBlogs() { return _context.Blogs; } } 迁移 Migration 通过 Nuget 引入EF Core Tool 的引用 Install-Package Microsoft.EntityFrameworkCore.Tools 如果需要使用 dotnet ef 命令, 请添加 Microsoft.EntityFrameworkCore.Tools.DotNet 生成迁移 打开Package Manager Console,执行命令 Add-Migration InitialCreate。 执行成功后会在项目下生成一个 Migrations目录,包含两个文件: BloggingContextModelSnapshot:当前Model的快照(状态)。 20180828074905_InitialCreate:这里面包含着migration builder需要的代码,用来迁移这个版本的数据库。里面有Up方法,就是从当前版本升级到下一个版本;还有Down方法,就是从下一个版本再退回到当前版本。 更新迁移到数据库 执行命令 Update-Database。 如果执行成功,数据库应该已经创建成功了。现在可以测试刚才创建的WebAPI应用了。 使用代码 Database.Migrate(); 可以达到同样的目的 public BloggingContext(DbContextOptions options) : base(options) { Database.Migrate(); } EF Core 中的一些常用知识点 实体建模 EF 根据对 Model 的配置生成表和字段,主要有三种配置方式: 约定 根据约定(Id 或者 Data Annotation 数据注解 using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; public class Blog { [Key] [Column("BlogId")] public int BlogId { get; set; } [Required] [MaxLength(500)] public string Url { get; set; } public int Rating { get; set; } public List Posts { get; set; } } Key: 主键 Required:不能为空 MinLength:字符串最小长度 MaxLength:字符串最大长度 StringLength:字符串最大长度 Timestamp:rowversion,时间戳列 ConcurrencyCheck 乐观并发检查列 Table 表名 Column 字段名 Index 索引 ForeignKey 外键 NotMapped 不映射数据库中的任何列 InverseProperty 指定导航属性和实体关系的对应,用于实体中有多个关系映射。 Fluent API 通过 Fluent API 在 IEntityTypeConfiguration 实现类里面配置实体: using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; public class Blog { public int BlogId { get; set; } public string Url { get; set; } public int Rating { get; set; } public List Posts { get; set; } } public class BlogConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.HasKey(t => t.BlogId); builder.Property(t => t.Url).IsRequired().HasMaxLength(500); } } 并在 Context 的 OnModelCreating 方法里面应用: public class BloggingContext : DbContext { public BloggingContext(DbContextOptions options) : base(options) {} public DbSet Blogs { get; set; } public DbSet Posts { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.ApplyConfiguration(new BlogConfiguration()); } } Fluent API 比数据注解有更高的优先级。 实体关系 一对多关系 Blog 和 Post 是一对多关系,在 PostConfiguration 里面添加如下配置: public class Blog { public int BlogId { get; set; } public string Url { get; set; } public int Rating { get; set; } public List Posts { get; set; } } public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public int BlogId { get; set; } public Blog Blog { get; set; } } public class PostConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.HasOne(p => p.Blog) .WithMany(b => b.Posts) .HasForeignKey(p => p.BlogId) .OnDelete(DeleteBehavior.Cascade); } } 一对一关系 创建一个实体类 PostExtension 做为 Post 的扩展表,它们之间是一对一关系。 如果两个实体相互包括了对方的引用导航属性(本例中是 PostExtension Extension 和 Post Post)和外键属性 (本例中是 PostExtension 中的 PostId),那 EF Core 会默认配置一对一关系的,当然也可以手动写语句(如注释的部分)。 public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public PostExtension Extension { get; set; } } public class PostExtension { public int PostId { get; set; } public string ExtensionField1 { get; set; } public Post Post { get; set; } } public class PostExtensionConfiguration : IEntityTypeConfiguration { public PostExtensionConfiguration() { } public void Configure(EntityTypeBuilder builder) { builder.HasKey(t => t.PostId); //builder.HasOne(e => e.Post) // .WithOne(p => p.Extension) // .HasForeignKey(e => e.PostId) // .OnDelete(DeleteBehavior.Cascade); } } 多对多关系 创建一个实体类 Tag, 和 Blog 是多对多关系。一个 Blog 可以有多个不同 Tag,同时一个 Tag 可以用多个 Blog。 EF Core 中创建多对多关系必须要声明一个映射的关系实体,所以我们创建 BlogTag 实体,并在 BlogTagConfiguration 配置了多对多关系。 public class Blog { public int BlogId { get; set; } public string Url { get; set; } public int Rating { get; set; } public IList BlogTags { get; set; } } public class Tag { public int TagId { get; set; } public string TagName { get; set; } public IList BlogTags { get; set; } } public class BlogTag { public int BlogId { get; set; } public Blog Blog { get; set; } public int TagId { get; set; } public Tag Tag { get; set; } } public class BlogTagConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.HasKey(bt => new { bt.BlogId, bt.TagId }); builder.HasOne(bt => bt.Blog) .WithMany(b => b.BlogTags) .HasForeignKey(bt => bt.BlogId); builder.HasOne(bt => bt.Tag) .WithMany(t => t.BlogTags) .HasForeignKey(bt => bt.TagId); } } 种子数据 填充种子数据可以让我们在首次使用应用之前向数据库中插入一些初始化数据。有两种方法: 通过实体类配置实现 在配置实体的时候可以通过HasData方法预置数据,在执行Update-Database命令时候会写入数据库。 public class BlogConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { //Data Seeding builder.HasData(new Blog { BlogId = 1, Url = "http://sample.com/1", Rating = 0 }); } } 统一配置 创建一个统一配置 SeedData 类, 然后在 Program.cs 中的 Main 中调用它。 public static class SeedData { public static void Initialize(IServiceProvider serviceProvider) { using (var context = new BloggingContext( serviceProvider.GetRequiredService>())) { if (context.Blogs.Any()) return; // DB has been seeded var blogs = new List { new Blog { Url = "http://sample.com/2", Rating = 0 }, new Blog { Url = "http://sample.com/3", Rating = 0 }, new Blog { Url = "http://sample.com/4", Rating = 0 } }; context.Blogs.AddRange(blogs); context.SaveChanges(); } } } public class Program { public static void Main(string[] args) { //CreateWebHostBuilder(args).Build().Run(); var host = CreateWebHostBuilder(args).Build(); using (var scope = host.Services.CreateScope()) { var services = scope.ServiceProvider; try { SeedData.Initialize(services); } catch (Exception ex) { var logger = services.GetRequiredService>(); logger.LogError(ex, "An error occurred seeding the DB."); } } host.Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup(); } 并发管理 数据库并发指的是多个进程或用户同时访问或更改数据库中的相同数据的情况。 并发控制指的是用于在发生并发更改时确保数据一致性的特定机制。 乐观并发:无论何时从数据库请求数据,数据都会被读取并保存到应用内存中。数据库级别没有放置任何显式锁。数据操作会按照数据层接收到的顺序执行。 悲观并发:无论何时从数据库请求数据,数据都会被读取,然后该数据上就会加锁,因此没有人能访问该数据。这会降低并发相关问题的机会,缺点是加锁是一个昂贵的操作,会降低整个应用程序的性能。 EF Core 默认支持乐观并发控制,这意味着它将允许多个进程或用户独立进行更改而不产生同步或锁定的开销。 在理想情况下,这些更改将不会相互影响,因此能够成功。 在最坏的情况下,两个或更多进程将尝试进行冲突更改,其中只有一个进程应该成功。 ConcurrencyCheck / IsConcurrencyToken ConcurrencyCheck 特性可以应用到领域类的属性中。当EF执行更新或删除操作时,EF Core 会将配置的列放在 where 条件语句中。执行这些语句后,EF Core 会读取受影响的行数。如果未影响任何行,将检测到并发冲突引发 DbUpdateConcurrencyException。 public class Blog { public int BlogId { get; set; } public string Url { get; set; } [ConcurrencyCheck] public int Rating { get; set; } } [HttpPut("{id}")] public async Task PutBlog([FromRoute] int id, [FromBody] Blog blog) { if (!ModelState.IsValid) { return BadRequest(ModelState); } var dbModel = await _context.Blogs.FindAsync(id); dbModel.Url = blog.Url; dbModel.Rating = blog.Rating; try { await _context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException ex) { //todo: handle DbUpdateConcurrencyException throw ex; } return NoContent(); } 通过 SQL Server Profiler 查看生成的 SQL Update 语句。 exec sp_executesql N'SET NOCOUNT ON; UPDATE [Blogs] SET [Rating] = @p0, [Url] = @p1 WHERE [BlogId] = @p2 AND [Rating] = @p3; SELECT @@ROWCOUNT; ',N'@p2 int,@p0 int,@p3 int,@p1 nvarchar(500)',@p2=1,@p0=999,@p3=20,@p1=N'http://sample.com/1' Timestamp / IsRowVersion TimeStamp特性可以应用到领域类中,只有一个字节数组的属性上面。每次插入或更新行时,由数据库生成一个新的值做为并发标记。 public class Blog { public int BlogId { get; set; } public string Url { get; set; } public int Rating { get; set; } [Timestamp] public byte[] Timestamp { get; set; } } 通过 SQL Server Profiler 查看生成的 SQL Update 语句。 exec sp_executesql N'SET NOCOUNT ON; UPDATE [Blogs] SET [Rating] = @p0 WHERE [BlogId] = @p1 AND [Timestamp] = @p2; SELECT [Timestamp] FROM [Blogs] WHERE @@ROWCOUNT = 1 AND [BlogId] = @p1; ',N'@p1 int,@p0 int,@p2 varbinary(8)',@p1=1,@p0=8888,@p2=0x00000000000007D1 处理冲突的策略: 忽略冲突并强制更新:这种策略是让所有的用户更改相同的数据集,然后所有的修改都会经过数据库,这就意味着数据库会显示最后一次更新的值。这种策略会导致潜在的数据丢失,因为许多用户的更改都丢失了,只有最后一个用户的更改是可见的。 部分更新:在这种情况中,我们也允许所有的更改,但是不会更新完整的行,只有特定用户拥有的列更新了。这就意味着,如果两个用户更新相同的记录但却不同的列,那么这两个更新都会成功,而且来自这两个用户的更改都是可见的。 拒绝更改:当一个用户尝试更新一个记录时,但是该记录自从他读取之后已经被别人修改了,此时告诉该用户不允许更新该数据,因为数据已经被某人更新了。 警告询问用户:当一个用户尝试更新一个记录时,但是该记录自从他读取之后已经被别人修改了,这时应用程序就会警告该用户该数据已经被某人更改了,然后询问他是否仍然要重写该数据还是首先检查已经更新的数据。 执行 SQL 语句和存储过程 EF Core 使用以下方法执行 SQL 语句和存储过程: DbSet DbSet.FromSql() 返回值为IQueryable,可以与Linq扩展方法配合使用。注意: SQL 查询必须返回实体或查询类型的所有属性的数据 结果集中的列名必须与属性映射到的列名称匹配。 SQL 查询不能包含相关数据。 但是可以使用 Include 运算符返回相关数据。 不要使用 TOP 100 PERCENT 或 ORDER BY 等子句。可以通过 Linq 在代码里面编写。 基本 SQL 查询 var blogs = _context.Blogs.FromSql($"select * from Blogs").ToList(); 带有参数的查询: var blog = _context.Blogs.FromSql($"select * from Blogs where BlogId = {id}"); 使用 LINQ: var blogs = _context.Blogs.FromSql($"select * from Blogs") .OrderByDescending(r => r.Rating) .Take(2) .ToList(); 通过 SQL Server Profiler 查看 SQL 语句,可以发现 EF Core 是把手工写的 SQL 语句和 Linq 合并生成了一条语句: exec sp_executesql N'SELECT TOP(@__p_1) [r].[BlogId], [r].[Rating], [r].[Timestamp], [r].[Url] FROM ( select * from Blogs ) AS [r] ORDER BY [r].[Rating] DESC',N'@__p_1 int',@__p_1=2 使用 Include 包括相关数据 var blogs = _context.Blogs.FromSql($"select * from Blogs").Include(
50000+
5万行代码练就真实本领
17年
创办于2008年老牌培训机构
1000+
合作企业
98%
就业率

联系我们

电话咨询

0532-85025005

扫码添加微信