1. 程式人生 > >ASP.NET Core 中的 ORM 之 Entity Framework

ASP.NET Core 中的 ORM 之 Entity Framework

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)

  1. 新建一個 WebAPI 專案

  2. 通過 Nuget 安裝 EF Core 引用

    // SQL Server
    Install-Package Microsoft.EntityFrameworkCore.SqlServer

    其他資料庫請檢視:https://docs.microsoft.com/zh-cn/ef/core/providers/

  3. 新增實體

    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }
        public int Rating { get; set; }
    
        public List<Post> 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; }
    }
  4. 新增資料庫上下文

    public class BloggingContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }
    }

    有兩種方式配置資料庫連線,一種是註冊 Context 的時候提供 options。比較推薦這種方式。

    public class BloggingContext : DbContext
    {
        public BloggingContext(DbContextOptions<BloggingContext> options)
            : base(options)
        { }
    
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }
    }

    在 Startup 中配置

    public void ConfigureServices(IServiceCollection services)
    {
        var connectionString = @"Server=.;Database=Blogging;Trusted_Connection=True;";
        services.AddDbContext<BloggingContext>(o => o.UseSqlServer(connectionString));
    
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    }

    一種是過載 OnConfiguring 方法提供連線字串:

    public class BloggingContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }
    
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Server=.;Database=Blogging;Trusted_Connection=True;");
            base.OnConfiguring(optionsBuilder);
        }
    }
  5. 在Controller 中使用 Context

    public class BlogsController : ControllerBase
    {
        private readonly BloggingContext _context;
    
        public BlogsController(BloggingContext context)
        {
            _context = context;
        }
    
        // GET: api/Blogs
        [HttpGet]
        public IEnumerable<Blog> GetBlogs()
        {
            return _context.Blogs;
        }
    }

    遷移 Migration

  6. 通過 Nuget 引入EF Core Tool 的引用

    Install-Package Microsoft.EntityFrameworkCore.Tools

    如果需要使用 dotnet ef 命令, 請新增 Microsoft.EntityFrameworkCore.Tools.DotNet

  7. 生成遷移

    開啟Package Manager Console,執行命令 Add-Migration InitialCreate。 執行成功後會在專案下生成一個 Migrations目錄,包含兩個檔案:
    • BloggingContextModelSnapshot:當前Model的快照(狀態)。
    • 20180828074905_InitialCreate:這裡麵包含著migration builder需要的程式碼,用來遷移這個版本的資料庫。裡面有Up方法,就是從當前版本升級到下一個版本;還有Down方法,就是從下一個版本再退回到當前版本。
  8. 更新遷移到資料庫

    執行命令 Update-Database。 如果執行成功,資料庫應該已經建立成功了。現在可以測試剛才建立的WebAPI應用了。

    使用程式碼 Database.Migrate(); 可以達到同樣的目的

        public BloggingContext(DbContextOptions<BloggingContext> 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<Post> 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<Post> Posts { get; set; }
    }
    
    public class BlogConfiguration : IEntityTypeConfiguration<Blog>
    {
        public void Configure(EntityTypeBuilder<Blog> builder)
        {
            builder.HasKey(t => t.BlogId);
    
            builder.Property(t => t.Url).IsRequired().HasMaxLength(500);
        }
    }

    並在 Context 的 OnModelCreating 方法裡面應用:

    public class BloggingContext : DbContext
    {
        public BloggingContext(DbContextOptions<BloggingContext> options)
            : base(options)
        {}
    
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> 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<Post> 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<Post>
    {
        public void Configure(EntityTypeBuilder<Post> builder)
        {
            builder.HasOne<Blog>(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<PostExtension>
    {
        public PostExtensionConfiguration()
        {
    
        }
    
        public void Configure(EntityTypeBuilder<PostExtension> builder)
        {
            builder.HasKey(t => t.PostId);
    
            //builder.HasOne(e => e.Post)
            //    .WithOne(p => p.Extension)
            //    .HasForeignKey<PostExtension>(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<BlogTag> BlogTags { get; set; }
    }
    
    public class Tag
    {
        public int TagId { get; set; }
        public string TagName { get; set; }
    
        public IList<BlogTag> 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<BlogTag>
    {
        public void Configure(EntityTypeBuilder<BlogTag> builder)
        {
            builder.HasKey(bt => new { bt.BlogId, bt.TagId });
    
            builder.HasOne<Blog>(bt => bt.Blog)
                .WithMany(b => b.BlogTags)
                .HasForeignKey(bt => bt.BlogId);
    
            builder.HasOne<Tag>(bt => bt.Tag)
                .WithMany(t => t.BlogTags)
                .HasForeignKey(bt => bt.TagId);
        }
    }

種子資料

填充種子資料可以讓我們在首次使用應用之前向資料庫中插入一些初始化資料。有兩種方法:

  • 通過實體類配置實現 在配置實體的時候可以通過HasData方法預置資料,在執行Update-Database命令時候會寫入資料庫。

    public class BlogConfiguration : IEntityTypeConfiguration<Blog>
    {
        public void Configure(EntityTypeBuilder<Blog> 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<DbContextOptions<BloggingContext>>()))
            {
                if (context.Blogs.Any())
                    return; // DB has been seeded
    
                var blogs = new List<Blog>
                {
                    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<ILogger<Program>>();
                    logger.LogError(ex, "An error occurred seeding the DB.");
                }
            }
    
            host.Run();
        }
    
        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>();
    }

併發管理

資料庫併發指的是多個程序或使用者同時訪問或更改資料庫中的相同資料的情況。 併發控制指的是用於在發生併發更改時確保資料一致性的特定機制。

  • 樂觀併發:無論何時從資料庫請求資料,資料都會被讀取並儲存到應用記憶體中。資料庫級別沒有放置任何顯式鎖。資料操作會按照資料層接收到的順序執行。
  • 悲觀併發:無論何時從資料庫請求資料,資料都會被讀取,然後該資料上就會加鎖,因此沒有人能訪問該資料。這會降低併發相關問題的機會,缺點是加鎖是一個昂貴的操作,會降低整個應用程式的效能。

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<IActionResult> 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<TEntity>.FromSql() 返回值為IQueryable,可以與Linq擴充套件方法配合使用。注意:

    1. SQL 查詢必須返回實體或查詢型別的所有屬性的資料
    2. 結果集中的列名必須與屬性對映到的列名稱匹配。
    3. SQL 查詢不能包含相關資料。 但是可以使用 Include 運算子返回相關資料。
    4. 不要使用 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(r => r.Posts).ToList();

    通過 SQL Server Profiler 檢視 SQL 語句:

    SELECT [b].[BlogId], [b].[Rating], [b].[Timestamp], [b].[Url]
    FROM (
        select * from Blogs
    ) AS [b]
    ORDER BY [b].[BlogId]
    
    SELECT [b.Posts].[PostId], [b.Posts].[BlogId], [b.Posts].[Content], [b.Posts].[Title]
    FROM [Posts] AS [b.Posts]
    INNER JOIN (
        SELECT [b0].[BlogId]
        FROM (
            select * from Blogs
        ) AS [b0]
    ) AS [t] ON [b.Posts].[BlogId] = [t].[BlogId]
    ORDER BY [t].[BlogId]
  • DbContext.Database.ExecuteSqlCommand()

    ExecuteSqlCommand方法返回一個整數,表示執行的SQL語句影響的行數。有效的操作是 INSERT、UPDATE 和 DELETE,不能用於返回實體。

    測試一下 INSERT:

    int affectRows = _context.Database.ExecuteSqlCommand($"Insert into Blogs([Url],[Rating])Values({blog.Url}, {blog.Rating})");

    通過 SQL Server Profiler 檢視 SQL 語句:

    exec sp_executesql N'Insert into Blogs([Url],[Rating])Values(@p0, @p1)',N'@p0 nvarchar(4000),@p1 int',@p0=N'testurl',@p1=3

延遲載入和預先載入

EF Core 通過在模型中使用導航屬性來載入相關實體。 有三種常見模式可用於載入相關資料。

  • 預先載入 表示從資料庫中載入相關資料,作為初始查詢的一部分。使用 Include方法實現預載入,使用 ThenInclude 實現多級預載入。

    var blogs = _context.Blogs.Include(r => r.Posts).ToList();

    當需要 JSON 序列化 blogs 物件時候,ASP.NET Core 自帶的序列化庫 Newtonsoft.Json 可能會丟擲自引用迴圈異常。請在 Startup 的 ConfigureServices 方法中配置以下程式碼解決。

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc()
            .AddJsonOptions(options => options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore);
    }
  • 顯式載入 表示稍後從資料庫中顯式載入相關資料。

    var blog = await _context.Blogs.FindAsync(id);
    
    _context.Entry(blog)
        .Collection(b => b.Posts)
        .Load();
  • 延遲載入 表示在訪問導航屬性時,才從資料庫中載入相關資料。在 EF Core 2.1 中才引入此功能。
    1. Nuget 安裝 Microsoft.EntityFrameworkCore.Proxies
    2. 呼叫 UseLazyLoadingProxies 來啟用延遲載入。

      services.AddDbContext<BloggingContext>(option => option.UseLazyLoadingProxies().UseSqlServer(connectionString));
    3. 導航屬性新增 virtual 修飾符。

      public class Blog
      {
          public int BlogId { get; set; }
          public string Url { get; set; }
          public int Rating { get; set; }
      
          public virtual IList<Post> 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 virtual Blog Blog { get; set; }
      }
    4. 測試,當代碼執行到var posts = blog.Posts時候,會去資料庫裡面查詢Posts記錄。

      var blog = await _context.Blogs.FindAsync(id);
      var posts = blog.Posts;

      儘量避免在迴圈時候使用延遲載入,會導致每次迴圈都去訪問資料庫。

IQueryable 和 IEnumerable

直接通過一個例項測試一下:

var testIQueryable = _context.Blogs.Where(r => r.Rating > 10);
var testIEnumerable = _context.Blogs.AsEnumerable().Where(r => r.Rating > 10);

var testIQueryableList = testIQueryable.ToList();
var testIEnumerableList = testIEnumerable.ToList();

檢視生產的 SQL 語句

  • IQueryable

    SELECT [r].[BlogId], [r].[Rating], [r].[Timestamp], [r].[Url]
    FROM [Blogs] AS [r]
    WHERE [r].[Rating] > 10
  • IEnumerable

    SELECT [b].[BlogId], [b].[Rating], [b].[Timestamp], [b].[Url]
    FROM [Blogs] AS [b]

IQueryable 是將 Linq 表示式翻譯成 T-SQL 語句之後再向 SQL 伺服器傳送命令. IEnumerable 是在呼叫自己的 Linq 方法之前先從 SQL 伺服器取到資料並載入到本地記憶體中。

生成遷移 SQL 指令碼

EF Core 將遷移更新到生產環境可以使用 Script-Migration 命令生成sql指令碼,然後到生產資料庫執行.

此命令有幾個選項。

  • -From <String> 遷移應是執行該指令碼前應用到資料庫的最後一個遷移。 如果未應用任何遷移,請指定 0(預設值)。
  • -To <String> 遷移是執行該指令碼後應用到資料庫的最後一個遷移。 它預設為專案中的最後一個遷移。
  • -Idempotent 此指令碼僅會應用尚未應用到資料庫的遷移。 如果不確知應用到資料庫的最後一個遷移或需要部署到多個可能分別處於不同遷移的資料庫,此指令碼非常有用。

待補充...

SQL 監視工具

有幾種方法可以監視 EF Core 自動生成的 SQL 語句:

  • 內建日誌
  • 資料庫監視工具
  • Miniprofiler

  • 內建日誌: 在除錯模式下,EF Core 會使用 ASP.NET Core 的內建日誌記錄功能把生成的 SQL 語句顯示在輸出視窗,大概如下:

    Microsoft.EntityFrameworkCore.Database.Command:Information: Executed DbCommand (50ms) [Parameters=[@__get_Item_0='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']
    SELECT TOP(1) [e].[Id], [e].[Rating], [e].[Url]
    FROM [Blogs] AS [e]
    WHERE [e].[Id] = @__get_Item_0

    如果想檢視敏感資料比如@__get_Item_0='?',請在 Context 類的 OnConfiguring 方法裡面配置optionsBuilder.EnableSensitiveDataLogging();

  • 資料庫監視工具: 也可以通過資料庫的監視工具,比如用於監視 MS SQL 的工具 SQL Server Profiler 檢視執行的 SQL 語句,大概如下:

    exec sp_executesql N'SELECT TOP(1) [e].[Id], [e].[Rating], [e].[Url]
    FROM [Blogs] AS [e]
    WHERE [e].[Id] = @__get_Item_0',N'@__get_Item_0 int',@__get_Item_0=1
  • Miniprofiler: MiniProfiler/dotnet是一款簡單而有效的效能分析的輕量級程式,可以監控頁面,也可以監控 EF Core 執行的 SQL 語句。

    MiniProfiler 一般用於 MVC 專案,但也可以結合 Swagger 用於 Web API專案。Swagger 的安裝和使用在本篇不做討論,詳細請參考Swashbuckle.AspNetCore

    1. Nuget 安裝 MiniProfiler 引用

      Install-Package MiniProfiler.AspNetCore.Mvc
      Install-Package MiniProfiler.EntityFrameworkCore
    2. 修改 SwaggerUI/index.html 頁面: 在專案下面新建一個檔案 SwaggerIndex.html 並複製以下程式碼,設定編譯為 Embedded resource

      <script async="async" id="mini-profiler" src="/profiler/includes.min.js?v=4.0.138+gcc91adf599" data-version="4.0.138+gcc91adf599" data-path="/profiler/" data-current-id="4ec7c742-49d4-4eaf-8281-3c1e0efa748a" data-ids="" data-position="Left" data-authorized="true" data-max-traces="15" data-toggle-shortcut="Alt+P" data-trivial-milliseconds="2.0" data-ignored-duplicate-execute-types="Open,OpenAsync,Close,CloseAsync"></script>
      
      <!-- HTML for static distribution bundle build -->
      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>%(DocumentTitle)</title>
          <link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700" rel="stylesheet">
          <link rel="stylesheet" type="text/css" href="./swagger-ui.css">
          <link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
          <link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
          <style>
              html {
                  box-sizing: border-box;
                  overflow: -moz-scrollbars-vertical;
                  overflow-y: scroll;
              }
      
              *,
              *:before,
              *:after {
                  box-sizing: inherit;
              }
      
              body {
                  margin: 0;
                  background: #fafafa;
              }
          </style>
          %(HeadContent)
      </head>
      
      <body>
      
          <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position:absolute;width:0;height:0">
              <defs>
                  <symbol viewBox="0 0 20 20" id="unlocked">
                      <path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z"></path>
                  </symbol>
      
                  <symbol viewBox="0 0 20 20" id="locked">
                      <path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z" />
                  </symbol>
      
                  <symbol viewBox="0 0 20 20" id="close">
                      <path d="M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z" />
                  </symbol>
      
                  <symbol viewBox="0 0 20 20" id="large-arrow">
                      <path d="M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z" />
                  </symbol>
      
                  <symbol viewBox="0 0 20 20" id="large-arrow-down">
                      <path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z" />
                  </symbol>
      
      
                  <symbol viewBox="0 0 24 24" id="jump-to">
                      <path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z" />
                  </symbol>
      
                  <symbol viewBox="0 0 24 24" id="expand">
                      <path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z" />
                  </symbol>
      
              </defs>
          </svg>
      
          <div id="swagger-ui"></div>
      
          <!-- Workaround for https://github.com/swagger-api/swagger-editor/issues/1371 -->
          <script>
              if (window.navigator.userAgent.indexOf("Edge") > -1) {
                  console.log("Removing native Edge fetch in favor of swagger-ui's polyfill")
                  window.fetch = undefined;
              }
          </script>
      
          <script src="./swagger-ui-bundle.js"></script>
          <script src="./swagger-ui-standalone-preset.js"></script>
          <script>
              window.onload = function () {
                  var configObject = JSON.parse('%(ConfigObject)');
                  var oauthConfigObject = JSON.parse('%(OAuthConfigObject)');
                  // Apply mandatory parameters
                  configObject.dom_id = "#swagger-ui";
                  configObject.presets = [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset];
                  configObject.layout = "StandaloneLayout";
                  // If oauth2RedirectUrl isn't specified, use the built-in default
                  if (!configObject.hasOwnProperty("oauth2RedirectUrl"))
                      configObject.oauth2RedirectUrl = window.location.href.replace("index.html", "oauth2-redirect.html");
                  // Build a system
                  const ui = SwaggerUIBundle(configObject);
                  // Apply OAuth config
                  ui.initOAuth(oauthConfigObject);
              }
          </script>
      </body>
      
      </html>
      
        <ItemGroup>
          <EmbeddedResource Include="SwaggerIndex.html" />
        </ItemGroup>
    3. 在 Startup 中配置 MiniProfiler: 在 ConfigureServices 裡面新增services.AddMiniProfiler().AddEntityFramework(), 在 Configure 裡面新增app.UseMiniProfiler(); 並配置 Swagger 的 IndexStream.

      public void ConfigureServices(IServiceCollection services)
      {
          services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
      
          //Swagger
          services.AddSwaggerGen(options =>
          {
              options.DescribeAllEnumsAsStrings();
              options.SwaggerDoc("v1", new Swashbuckle.AspNetCore.Swagger.Info
              {
                  Title = "API Docs",
                  Version = "v1",
              });
          });
      
          //Profiling
          services.AddMiniProfiler(options =>
              options.RouteBasePath = "/profiler"
          ).AddEntityFramework();
      }
      
      // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
      public void Configure(IApplicationBuilder app, IHostingEnvironment env)
      {
          if (env.IsDevelopment())
          {
              app.UseDeveloperExceptionPage();
      
              // profiling, url to see last profile check: http://localhost:56775/profiler/results
              app.UseMiniProfiler();
          }
      
          app.UseSwagger();
      
          app.UseSwagger().UseSwaggerUI(c =>
          {
              c.SwaggerEndpoint("/swagger/v1/swagger.json", "API V1");
              // index.html customizable downloadable here: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/master/src/Swashbuckle.AspNetCore.SwaggerUI/index.html
              // this custom html has miniprofiler integration
              c.IndexStream = () => GetType().GetTypeInfo().Assembly.GetManifestResourceStream("ORMDemo.EFWithRepository.SwaggerIndex.html");
          });
      
          app.UseMvc();
      }
    4. 執行專案,MiniProfiler 監控頁面應該已經出現在 Swagger UI 頁面的左上角了。

倉儲模式和工作單元模式

倉儲模式(Repository)是用來解耦的(通過在資料訪問層和業務邏輯層之間建立抽象層)。 但倉儲只關注於單一聚合的持久化,而業務用例卻常常會涉及多個聚合的更改,為了確保業務用例的一致型,我們需要引入工作單元來管理多個聚合。

工作單元模式(unit of work)的作用就是在業務用例的操作中跟蹤物件的所有更改(增加、刪除和更新),並將所有更改的物件儲存在其維護的列表中。在業務用例的終點,通過事務,一次性提交所有更改,以確保資料的完整性和有效性。總而言之,UOW協調這些物件的持久化及併發問題。

在 EF Core 中 DBContext 已經實現了工作單元模式,同時也比較容易更換統一的資料儲存介質(通過支援的資料庫驅動)。那麼還有沒有必要在 EF Core 上面再封裝一層實現自己的倉儲和工作單元呢?

  • 如果專案比較簡單,業務邏輯並不複雜。特別是在實現一些微服務的時候,每個專案(服務)都只負責一部分小的並且功能內聚的業務。這個時候或許保持程式碼簡單最好,沒有必要過度設計。
  • 當然,如果專案比較複雜,沒有采用微服務架構而是多個模組都在一起的單體架構,可能同時需要多種資料儲存介質和途徑,用到了多種的資料訪問和持久化技術,那麼可能就需要好好設計一個適合專案的倉儲和工作單元模式了。

下面實現一個簡單的倉儲和工作單元模式:

  • 定義實體基類

    public abstract class BaseEntity<TKey>
    {
        public virtual TKey Id { get; set; }
    }
  • 定義倉儲基類

    public interface IRepository<TDbContext, TEntity, TKey> where TEntity : BaseEntity<TKey> where TDbContext : DbContext
    {
        Task<TEntity> GetByKeyAsync(TKey id);
    
        Task<IList<TEntity>> GetAsync(
            Expression<Func<TEntity, bool>> predicate = null,
            Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
            List<Expression<Func<TEntity, object>>> includes = null);
    
        Task<TEntity> AddAsync(TEntity entity);
    
        TEntity Update(TEntity entity);
    
        void Delete(TKey id);
    
        void Delete(TEntity entity);
    }
    
    public class EFRepository<TDbContext, TEntity, TKey> : IRepository<TDbContext, TEntity, TKey> where TEntity : BaseEntity<TKey> where TDbContext : DbContext
    {
        protected readonly TDbContext _context;
        protected readonly DbSet<TEntity> dbSet;
    
        public EFRepository(TDbContext context)
        {
            this._context = context;
            this.dbSet = context.Set<TEntity>();
        }
    
        public virtual async Task<TEntity> GetByKeyAsync(TKey id)
        {
            return await dbSet.FindAsync(id);
        }
    
        public virtual async Task<IList<TEntity>> GetAsync(
            Expression<Func<TEntity, bool>> predicate = null,
            Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
            List<Expression<Func<TEntity, object>>> includes = null)
        {
            IQueryable<TEntity> query = dbSet;
    
            if (includes != null)
            {
                query = includes.Aggregate(query, (current, include) => current.Include(include));
            }
            if (orderBy != null)
            {
                query = orderBy(query);
            }
            if (predicate != null)
            {
                query = query.Where(predicate);
            }
    
            return await query.ToListAsync();
        }
    
        public virtual async Task<TEntity> AddAsync(TEntity entity)
        {
            var result = await dbSet.AddAsync(entity);
            return result.Entity;
        }
    
        public virtual TEntity Update(TEntity entity)
        {
            AttachIfNot(entity);
            this._context.Entry(entity).State = EntityState.Modified;
            return entity;
        }
    
        public virtual void Delete(TKey id)
        {
            TEntity entity = dbSet.Find(id);
            Delete(entity);
        }
    
        public virtual void Delete(TEntity entity)
        {
            AttachIfNot(entity);
            dbSet.Remove(entity);
        }
    
        protected virtual void AttachIfNot(TEntity entity)
        {
            if (this._context.Entry(entity).State == EntityState.Detached)
            {
                dbSet.Attach(entity);
            }
        }
    }

    可以根據需求擴充套件更多的方法。

  • 定義工作單元基類

    public interface IUnitOfWork<TDbContext> where TDbContext : DbContext
    {
        Task<int> SaveChangesAsync();
    }
    
    public class UnitOfWork<TDbContext> : IUnitOfWork<TDbContext> where TDbContext : DbContext
    {
        private readonly TDbContext _dbContext;
    
        public UnitOfWork(TDbContext context)
        {
            _dbContext = context ?? throw new ArgumentNullException(nameof(context));
        }
    
        public async Task<int> SaveChangesAsync()
        {
            return await _dbContext.SaveChangesAsync();
        }
    }
  • 定義 BloggingContext 並定義基於 BloggingContext 的倉儲基類和工作單元基類

    public class BloggingContext : DbContext
    {
        public BloggingContext(DbContextOptions<BloggingContext> options)
            : base(options)
        {
        }
    
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
    
            modelBuilder.ApplyConfiguration(new BlogConfiguration());
            modelBuilder.ApplyConfiguration(new PostConfiguration());
        }
    }
    
    public interface IBlogggingRepositoryBase<TEntity, TKey> : IRepository<BloggingContext, TEntity, TKey> where TEntity : BaseEntity<TKey>
    {
    }
    
    public class BlogggingRepositoryBase<TEntity, TKey> : EFRepository<BloggingContext, TEntity, TKey>, IBlogggingRepositoryBase<TEntity, TKey> where TEntity : BaseEntity<TKey>
    {
        public BlogggingRepositoryBase(BloggingContext dbContext) : base(dbContext)
        {
        }
    }
    
    public class BloggingUnitOfWork : UnitOfWork<BloggingContext>
    {
        public BloggingUnitOfWork(BloggingContext dbContext) : base(dbContext)
        {
        }
    }
  • 在 Startup 的 ConfigureServices 裡面註冊相關服務

    public void ConfigureServices(IServiceCollection services)
    {
        var connectionString = @"Server=.;Database=BloggingWithRepository;Trusted_Connection=True;";
        services.AddDbContext<BloggingContext>(option => option.UseSqlServer(connectionString));
        services.AddScoped<BloggingUnitOfWork>();
        services.AddTransient(typeof(IBlogggingRepositoryBase<,>), typeof(BlogggingRepositoryBase<,>));
    }

    這裡 BloggingContext 和 UnitOfWork 的生命週期為 Scoped。

  • 在 Controller 裡面呼叫並測試

    public class BlogsController : ControllerBase
    {
        private readonly IBlogggingRepositoryBase<Blog, int> _blogRepository;
        private readonly IBlogggingRepositoryBase<Post, int> _postRepository;
        private readonly BloggingUnitOfWork _unitOfWork;
    
        public BlogsController(IBlogggingRepositoryBase<Blog, int> blogRepository, IBlogggingRepositoryBase<Post, int> postRepository, BloggingUnitOfWork unitOfWork)
        {
            _blogRepository = blogRepository;
            _postRepository = postRepository;
            _unitOfWork = unitOfWork;
        }
    
        [HttpGet]
        public async Task<IActionResult> GetBlogs()
        {
            var blogs = await _blogRepository.GetAsync();
            return Ok(blogs);
        }
    
        [HttpPost]
        public async Task<IActionResult> PostBlog([FromBody] Blog blog)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
    
            //await _blogRepository.AddAsync(new Blog { Url = "http://sample.com/4", Rating = 0 });
            //await _postRepository.AddAsync(new Post { Title = "Title4", Content = "BlogId_1 Post_3", BlogId = 1 });
    
            var result = await _blogRepository.AddAsync(blog);
            await _unitOfWork.SaveChangesAsync();
    
            return CreatedAtAction("GetBlog", new { id = blog.Id }, blog);
        }
    }

使用 EF Core(DB First)

EF Core 的 DB First 是通過 Scaffold-DbContext 命令根據已經存在的資料庫建立實體類和context類。

可以通過PM> get-help scaffold-dbcontext –detailed檢視命令的詳細引數

Scaffold-DbContext [-Connection] <String> [-Provider] <String> [-OutputDir <String>] [-ContextDir <String>] [-Context <String>] [-Schemas <String[]>] [-Tables <String[]>] [-DataAnnotations] [-UseDatabaseNames] [-Force] 
[-Project <String>] [-StartupProject <String>] [<CommonParameters>]

使用之前建立的 blogging 資料庫簡單的測試一下:

  1. 新建一個專案,然後通過 Nuget 安裝 EF Core 引用

    Install-Package Microsoft.EntityFrameworkCore.SqlServer
    Install-Package Microsoft.EntityFrameworkCore.Tools
  2. 執行命令建立實體

    Scaffold-DbContext "Server=CD02SZV3600503\SQLEXPRESS;Database=BloggingWithRepository;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models

執行成功後可以看到在 Models 資料夾下面建立的實體類和 Context 類。

原始碼