ASP.NET Core 中的 ORM 之 Entity Framework

EF Core 簡介

Entity Framework Core 是微軟自家的 ORM 框架。做爲 .Net Core 生態中的一個重要組成部分,它是一個支持跨平臺的全新版本,用三個詞來概況 EF Core 的特色:輕量級、可擴展、跨平臺。html

目前 EF Core 支持的數據庫:git

  • 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 項目github

  2. 經過 Nuget 安裝 EF Core 引用web

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

    其餘數據庫請查看:https://docs.microsoft.com/zh-cn/ef/core/providers/sql

  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. 添加數據庫上下文json

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

    有兩種方式配置數據庫鏈接,一種是註冊 Context 的時候提供 options。比較推薦這種方式。api

    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 或者 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 ExtensionPost 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 .FromSql()

    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
  • MiniprofilerMiniProfiler/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 類。

源代碼

Github

參考

相關文章
相關標籤/搜索