EntityFrameworkCore2.1的安裝使用和其中遇到的那些坑

LazyLoading是EntityFramework受爭議比較嚴重的特性,有些人愛它,沒有它就活不下去了,有些人對它嗤之以鼻,由於這種不受控制的查詢而感到焦慮。html

我我的以爲若是要用EF那仍是儘可能要使用它儘量多的特性,否則,你還不如去找其它更輕量級的ORM。sql

本人對EF的理解仍是處於比較初級的階段,可是CodeFirst的開發方式讓我在三年前寫MVC的時候爲之驚歎。奈何各類搞Migration吐血,各類配置吐血,學習耗時太長,後來放棄,直到敬而遠之。數據庫

此次因爲本身喜歡的油管主播AngelSix在WPF項目中使用了EFCore訪問本地Sqlite數據庫,和SQL Server數據庫,決定參考從新學習。此次本着邊作邊學的態度,接觸EFCore,碰到很多坑,如今記錄以下,後續可能會有更新,畢竟EFCore目前的版本是2.1,項目也正在不斷演進。c#

EFCore的安裝使用

EFCore同時支持傳統.net framework和.net core架構,相關的架構依賴能夠參考nuget上的說明文檔。api

安裝Nuget包Microsoft.EntityFrameworkCore.Sqlite版本2.1.0架構

EFCore的主要配置代碼都集中在DBContext繼承類上框架

DBSet定義數據庫表dom

OnConfiguring用來配置DBContext行爲,好比下面代碼就是使用本地testing.db文件數據庫async

OnModelCreating用來配置數據庫的映射,這裏沒有吧映射加到Domain實體,由於這樣Domain實體代碼就要引用EF,所有映射都在ModelCreating完成ide

再經過DbContext.Database.EnsureCreatedAsync();建立數據庫實例。

public class StockDbContext:DbContext
{
    #region DbSets
    public DbSet<Stock> Stocks { get; set; }
    public DbSet<Valuation> Valuations { get; set; }
    #endregion
    #region Constructor
    public StockDbContext(DbContextOptions<StockDbContext> options):base(options)
    {
    }
    #endregion
    #region Configure the path
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite("Data Source=testing.db");
    }
    #endregion
    #region Model Creating
    /// <summary>
    /// Configures the database structure and relationships
    /// </summary>
    /// <param name="modelBuilder"></param>
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        //設置數據庫主鍵
        modelBuilder.Entity<Stock>().HasKey(a => a.Id);
        //主鍵自增
        modelBuilder.Entity<Stock>().Property(x => x.Id).ValueGeneratedOnAdd();
        modelBuilder.Entity<Valuation>().HasKey(a => a.Id);
        modelBuilder.Entity<Valuation>().Property(x => x.Id).ValueGeneratedOnAd();
        //設置默認值
        modelBuilder.Entity<Valuation>().Property(x => x.Time).HasDefaultValueSq("strftime(\'%Y-%m-%d %H:%M:%f\',\'now\',\'localtime\')");
    }
}

其中ModelCreating的各類數據庫屬性怎麼映射能夠參考這裏

新增實體

public async Task<int> AddStock(Stock stock)
{
    mDbContext.Stocks.Add(stock);
    return await mDbContext.SaveChangesAsync();
}

更新實體

public async Task<int> UpdateStock(Stock stock)
{
    mDbContext.Stocks.Update(stock);
    // Save changes
    return await mDbContext.SaveChangesAsync();
}

刪除實體

public async Task<int> Remove(Stock stock)
{
    mDbContext.Stocks.Remove(stock);
    // Save changes
    return await mDbContext.SaveChangesAsync();
}

查詢實體

public Task<IQueryable<Stock>> GetStockAsync()
{
    return Task.FromResult(mDbContext.Stocks.AsQueryable());
}

坑一:實體與實體間的關聯關係,外鍵如何生成和映射

EntityFrameWork實體之間的關係映射這篇文章已經講的很清楚了,包括一對多、多對多關係。

可是EFCore的多對多映射和EF略有不一樣

EF中:

this.HasMany(t => t.Users)
    .WithMany(t => t.Roles)
    .Map(m =>
        {
            m.ToTable("UserRole");
            m.MapLeftKey("RoleID");
            m.MapRightKey("UserID");
        });

EFCore中沒有HasMany+WithMany這個API怎麼辦?

答案是手動建立關聯實體,經過引入UserRole這個實體,來映射

public class UserRole(){
    public int UserID { get; set; }
    public virtual User User { get; set; }
    public int RoleID { get; set; }
    public virtual Role Role { get; set; }
}
public partial class User(){
    public virtual ICollection<UserRole> UserRoles { get; set;}
}
public partial class Role(){
    public virtual ICollection<UserRole> UserRoles { get; set;}
}

Map的時候使用UserRole進行兩次一對多映射便可!

modelBuilder.Entity<UserRole>()
    .HasOne(x => x.Role)
    .WithMany(y => y.UserRoles)
    .HasForeignKey(z => z.RoleID)
    .IsRequired()
    .OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<UserRole>()
    .HasOne(x => x.User)
    .WithMany(y => y.UserRoles)
    .HasForeignKey(z => z.UserID)
    .IsRequired()
    .OnDelete(DeleteBehavior.Cascade);

坑二:System.InvalidCastException: 指定的轉換無效

這個實際上是一個不太容易發現問題緣由的異常,由於不少緣由能夠致使這個異常,我此次的錯誤是把枚舉類型以聲明形式轉換爲數據庫字段INTEGER致使

public enum Urgency : short{/*...*/}
//...OnModelCreating
modelBuilder.Entity<Task>().Property(x => x.Urgency).HasColumnType("INTEGER");

在建立數據庫時無問題,可是在添加或查詢數據時報錯

其實若是不顯式標註INTEGER的類型,在建立數據庫時仍是INTEGER類型,區別是一個可空一個不可空

估計在這裏作實體映射的時候出錯了,而後這個問題在EFCore2.0.3是沒有的,汗。。。

本人在調試這個問題的時候猜想問題出在OnModelCreating上,而後不停的註釋取注跑單元測試,最終定位問題出在這裏。

坑三:數據存取集成測試如何不建立實體文件數據庫進行測試

單元測試跑文件數據庫須要每次都刪除重來,搞起Setup、TearDown都是異常麻煩。

還好Sqlite有內存數據庫,可是內存數據庫的效用只在一次鏈接內。

也就是說,若是鏈接關閉了,你的表就都沒了,即便dbcontext已經執行過了EnsureDBCreate方法

參考文獻

public static StockDbContext GetMemorySqlDatabase()
{
    var connectionStringBuilder =
        new SqliteConnectionStringBuilder { DataSource = ":memory:" };
    var connectionString = connectionStringBuilder.ToString();
    var connection = new SqliteConnection(connectionString);
    var builder = new DbContextOptionsBuilder<StockDbContext>();
    builder.UseSqlite(connection);
    DbContextOptions<StockDbContext> options = builder.Options;
    return new StockDbContext(options);
}
public async Task UseMemoryContextRun(Func<StockDbContext, Task> function)
{
    //In-Memory sqlite db will vanish per connection
    using (var context = StockDbContext.GetMemorySqlDatabase())
    {
        if (context == null) return;
        context.Database.OpenConnection();
        context.Database.EnsureCreated();
        //Do that task
        await function.Invoke(context);
        context.Database.CloseConnection();
    }
}

坑四:怎樣顯示EFCore執行的Sql日誌

不少時候須要排錯,EF的最大問題是,我都不知道框架幫我生成的語句是什麼

這個時候能夠藉助

public static readonly LoggerFactory MyLoggerFactory
    = new LoggerFactory(new[] { new DebugLoggerProvider((_, __) => true) });
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseLoggerFactory(MyLoggerFactory);
}

將詳細日誌打印到Debug日誌裏,參考官方文檔

須要去nuget上安裝對應的LogProvider,也可使用本身的logprovider,我本身安裝Microsoft.Extensions.Logging.Debug以爲夠用了

坑五:爲什麼使用LazyLoad,如何使用

因爲CodeFirst生成的關聯關係,在查詢的時候默認都是空的

例如:

var fs = Stock.FirstOrDefault(x=>x.StockID = 1);

即便fs爲1的對象有關聯的Valuation數據在數據庫中,查詢出來的對象Valuation這一屬性將會爲空

只有顯式的聲明Include、ThenInclude才能一併加載,這對某些一對多自關聯的對象來講很恐怖,因此LazyLoad能夠說是省時省力的好工具

參考官方文檔

一共有三種方式實現LazyLoad,都須要EFCore版本2.1以上

  • Nuget安裝使用Microsoft.EntityFrameworkCore.Proxies
  • 使用ILazyLoader注入Domain對象
  • 非侵入式使用ILazyLoader注入Domain對象

Domain對象確定不能侵入式注入,因此我嘗試了方法1和方法3,均可以成功

方案一:使用Microsoft.EntityFrameworkCore.Proxies

實現細節參考文檔,這裏說下坑

首先全部關聯屬性必須用virtual,否則代理不能注入

其次代理注入將改變對象的類型

好比我注入了一個UserRole對象,那這個對象的GetType將會是UserRoleProxy

這就致使這個對象在和另外一個UserRole進行比較的時候可能出現,對象判等失敗

obj.GetType() != GetType()

方案二:侵入式使用ILazyLoader注入Domain對象

由於方案一實現過程當中出現了坑二的問題,致使我又嘗試了ILazyLoader注入

No field was found backing property 'xxxxx' of entity type 'xxxxx'. Lazy-loaded navigation properties must have backing fields. Either name the backing field so that it is picked up by convention or configure the backing field to use.

只有一個關聯屬性xxxx報了這個錯,關聯屬性這麼多,怎麼恰恰你報錯呢?

仔細看了下,是拼寫問題,private field的拼寫要和public property的拼寫一致。雖然Intelisense沒有錯誤表明編譯是能夠經過的,汗。。。

坑六:怎樣實現一個完整的Clone數據庫對象

要Clone數據首先要使用AsNoTracking方法

var originalEntity = mDbContext.Memos.AsNoTracking()
    .Include(r => r.MemoTaggers)
    .Include(x => x.TaskMemos)
    .FirstOrDefault(e => string.Equals(e.MemoId, memoid, StringComparison.Ordinal));
if (originalEntity != null)
{
    originalEntity.MemoId = null;
    foreach (var originalEntityMemoTagger in originalEntity.MemoTaggers)
    {
        originalEntityMemoTagger.MemoId = null;
        originalEntityMemoTagger.MemoTaggerId = null;
    }
    foreach (var originalEntityTaskMemo in originalEntity.TaskMemos)
    {
        originalEntityTaskMemo.MemoId = null;
        originalEntityTaskMemo.TaskMemoId = null;
    }
    mDbContext.Memos.Add(originalEntity);
    await mDbContext.SaveChangesAsync();
    return originalEntity;
}

問題來了,LazyLoad引入後調用關聯屬性會報錯

Error generated for warning 'Microsoft.EntityFrameworkCore.Infrastructure.DetachedLazyLoadingWarning: An attempt was made to lazy-load navigation property 'MemoTaggers' on detached entity of type 'CNMemoProxy'. Lazy-loading is not supported for detached entities or entities that are loaded with 'AsNoTracking()'.'. This exception can be suppressed or logged by passing event ID 'CoreEventId.DetachedLazyLoadingWarning' to the 'ConfigureWarnings' method in 'DbContext.OnConfiguring' or 'AddDbContext'.

根據提示OnConfiguration中加入這段後,就能夠Suppress這個報錯。

optionsBuilder
.ConfigureWarnings(warnnings=>warnnings.Lo(CoreEventId.DetachedLazyLoadingWarning))
相關文章
相關標籤/搜索