1. 程式人生 > >EFCore2.1的安裝使用和其中遇到的那些坑

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

錯誤 異常 factory 實體映射 task emp clas xxx 單元

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

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

我個人覺得如果要用EF那還是盡量要使用它盡可能多的特性,不然,你還不如去找其它更輕量級的ORM。

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

這次由於自己喜歡的油管主播AngelSix在WPF項目中使用了EFCore訪問本地Sqlite數據庫,和SQL Server數據庫,決定參考重新學習。這次本著邊做邊學的態度,接觸EFCore,碰到不少坑,現在記錄如下,後續可能會有更新,畢竟EFCore目前的版本是2.1,項目也正在不斷演進。

  • EFCore的安裝使用
  • 坑一:實體與實體間的關聯關系,外鍵如何生成和映射
  • 坑二:System.InvalidCastException: 指定的轉換無效
  • 坑三:數據存取集成測試如何不創建實體文件數據庫進行測試
  • 坑四:怎樣顯示EFCore執行的Sql日誌
  • 坑五:為何使用LazyLoad,如何使用
  • 方案一:使用Microsoft.EntityFrameworkCore.Proxies
  • 方案二:侵入式使用ILazyLoader註入Domain對象
  • 坑六:怎樣實現一個完整的Clone數據庫對象

EFCore的安裝使用

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

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

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

DBSet定義數據庫表

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

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

再通過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))

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