LazyLoading是EntityFramework受争议比较严重的特性,有些人爱它,没有它就活不下去了,有些人对它嗤之以鼻,由于这种不受控制的查询而感到焦虑。html
我我的以为若是要用EF那仍是尽可能要使用它尽量多的特性,否则,你还不如去找其它更轻量级的ORM。sql
本人对EF的理解仍是处于比较初级的阶段,可是CodeFirst的开发方式让我在三年前写MVC的时候为之惊叹。奈何各类搞Migration吐血,各类配置吐血,学习耗时太长,后来放弃,直到敬而远之。数据库
此次因为本身喜欢的油管主播AngelSix在WPF项目中使用了EFCore访问本地Sqlite数据库,和SQL Server数据库,决定参考从新学习。此次本着边作边学的态度,接触EFCore,碰到很多坑,如今记录以下,后续可能会有更新,毕竟EFCore目前的版本是2.1,项目也正在不断演进。c#
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);
这个实际上是一个不太容易发现问题缘由的异常,由于不少缘由能够致使这个异常,我此次的错误是把枚举类型以声明形式转换为数据库字段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(); } }
不少时候须要排错,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以为够用了
因为CodeFirst生成的关联关系,在查询的时候默认都是空的
例如:
var fs = Stock.FirstOrDefault(x=>x.StockID = 1);
即便fs为1的对象有关联的Valuation数据在数据库中,查询出来的对象Valuation这一属性将会为空
只有显式的声明Include、ThenInclude才能一并加载,这对某些一对多自关联的对象来讲很恐怖,因此LazyLoad能够说是省时省力的好工具
参考官方文档
一共有三种方式实现LazyLoad,都须要EFCore版本2.1以上
Domain对象确定不能侵入式注入,因此我尝试了方法1和方法3,均可以成功
实现细节参考文档,这里说下坑
首先全部关联属性必须用virtual,否则代理不能注入
其次代理注入将改变对象的类型
好比我注入了一个UserRole对象,那这个对象的GetType将会是UserRoleProxy
这就致使这个对象在和另外一个UserRole进行比较的时候可能出现,对象判等失败
obj.GetType() != GetType()
由于方案一实现过程当中出现了坑二的问题,致使我又尝试了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数据首先要使用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))