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))