我比较喜欢安静,大概和我喜欢研究和琢磨技术原因相关吧,刚好到了元旦节,这几天可以好好学习下EF Core,同时在项目当中用到EF Core,借此机会给予比较深入的理解,这里我们只讲解和EF 6.x中不同,相同的则不再叙述。
当我们利用EF Core查询数据库时如果我们不显式关闭变更追踪的话,此时实体是被追踪的,关于变更追踪我们下节再叙。就像我们之前在EF 6.x中讨论的那样,不建议手动关闭变更追踪,对于有些特殊情况下,关闭变更追踪可能会导致许多问题的发生。
对于EF Core 1.1中依然有四种状态,有的人说不是有五种状态么,UnChanged、Added、Modified、Deleted、Detached。如果我们按照变更追踪来划分的话,实际上只有四种,将Detached排除在外,Detached不会被上下文所追踪。那么状态如何改变的呢?内部有一个IStateManager接口,通过此接口来对实体状态进行管理,此时再取决于SaveChanges被调用后背后是如何进行处理,我也就稍微看了下源码,深入的东西没去过多研究。
Added:实体还未插入到数据库当中,当调用SaveChanges后将修改其状态并将实体插入到数据库。
UnChanged:实体存在数据库中,但是在客户端未进行修改,当调用SaveChanges后将忽略。
Modified:实体存在数据库中,同时实体在客户端也进行了修改,当调用SaveChanges后将更改其状态并更新数据持久化到数据库。
Deleted:实体存在数据库中,当调用SaveChanges方法后将删除实体。
在EF Core 1.1中依然存在Add、Attach、Update方法,我们通过上下文或者DbSet<TEntity>能够看到,当将实体传递到这些方法中时,它们与实体追踪可达图紧密联系在一起,比如说我们之前讨论的博客的导航属性文章的发表,当我们添加文章的发表的这个实体时,然后调用Add方法后此时文章的发表这个实体也就被添加。在EF 6.x中我们说过当我们调用Add等方法时EF内部机制将会自动调用DetectChanges,但是在EF Core 1.1中则不再调用DetectChanges方法。空口无凭,我下载了源码,如下:
public virtual void Add(TEntity item) { var entry = _stateManager.GetOrCreateEntry(item); if (entry.EntityState == EntityState.Deleted || entry.EntityState == EntityState.Detached) { OnCountPropertyChanging(); entry.SetEntityState(EntityState.Added); _count++; OnCollectionChanged(NotifyCollectionChangedAction.Add, item); OnCountPropertyChanged(); } }
上述我们没有看到任何自动调用DetectChanges的逻辑,在EF 6.x中我们讲到当调用SaveChanges时此时会回调DetectChanges,而在EF Core 1.1中同样也是如此,所以相对于EF 6.x而言,EF Core 1.1只是在SaveChanges时回调DetectChanges,在Add、Attacth、Update等方法则不再回调DetectChanges,这样的话性能就会好很多。我们看到源代码中调用SaveChanges时逻辑如下:
public virtual int SaveChanges(bool acceptAllChangesOnSuccess) { CheckDisposed(); TryDetectChanges(); try { return StateManager.SaveChanges(acceptAllChangesOnSuccess); } catch (Exception exception) {..} }
接下来我们再来看看当调用Add、Update等方法时到底发生了什么。
Add:当调用Add方法时就没什么可说的了,此时将在图中的对应的所有实体推入到Added状态,也就说在调用SaveChanges时将会插入到数据库中去。
Attach:当调用Attach方法时将在图中的所有实体推入到UnChanged状态,但是有一个额外情况,比如我们在一个类中添加导航属性数据时,此时Attach的话将会使用混合模式,将此实体的状态为UnChanged而导航属性的状态则是Added状态,所以当插入到数据库中时,这个已存在的数据将不会被保存,只有新添加的导航属性数据才会被插入到数据库中去。
Update:Update方法和Attach方法一样只是将其状态修改为Modified,而将新添加的实体的修改将进行插入。
Remove:当调用Remove方法时此时它只会影响传递给该方法的实体,不会去遍历实体的可到达图。如果一个实体的状态是UnChanged或者Modified,说明该实体已存在数据库中,此时只需将其状态修改为Deleted。如果实体的状态为Added,此时说明该实体在数据库中不存在,此时会脱离上下文而不被跟踪。所以Remove方法侧重强调实体要被追踪,否则的话需要首先被Attach然后将其推入到Deleted状态。
在EF Core 1.1中多了AddRanges、UpdateRanges等方法,它们和实际调用多次调用非Range方法其实是一样的,它内部也会去遍历实体集合并更新其状态,如下:
public virtual void UpdateRange([NotNull] IEnumerable<object> entities) => SetEntityStates(Check.NotNull(entities, nameof(entities)), EntityState.Modified);
我们再看SetEntityStates这个方法的实现。
private void SetEntityStates(IEnumerable<object> entities, EntityState entityState) { var stateManager = StateManager; foreach (var entity in entities) { SetEntityState(stateManager.GetOrCreateEntry(entity), entityState); } }
EF Core内部机制的处理肯定比我们之前手动去遍历添加实体集合性能要高,意外看到一篇文章上有说仅仅只高效一点,因为Range方法自动调用DetectChanges方法,找了半天也没看见在哪里调用DetectChanges,郁闷,算是一点疑惑吧。
【注意】EF团队之前一直在承诺EF Core会更高效和更高可扩展,但是我阅读源码发现内部还是自动调用了DetectChanges,性能方面的话还是不算太高效,但是,但是源码中已经明确给出,关于DetectChanges方法,未来对于这个api会进行更改或者彻底移除,源码注释如下:
/// <summary> /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// </summary> void DetectChanges([NotNull] IStateManager stateManager);
对于变更追踪也好,默认启用变更追踪也好,我们都是通过ChangeTracker属性来获取到,如下:
EFCoreContext efCoreContext;
efCoreContext.ChangeTracker.AutoDetectChangesEnabled;
efCoreContext.ChangeTracker.DetectChanges;
在ChangeTracker中也有一个重要的方法那就是如下:
efCoreContext.ChangeTracker.TrackGraph;
我们暂且起名为跟踪图吧,它是对实体状态的完全控制,比如我们在将数据插入到数据库之前想设置其某一个值为临时值,我们就可以通过该方法来实现。
Blog blog; using (var efCoreContext = new EFCoreContext(options)) { efCoreContext.ChangeTracker.TrackGraph(blog, node => { var entry = node.Entry; if ((int)entry.Property("Id").CurrentValue < 0) { entry.State = EntityState.Added; entry.Property("Id").IsTemporary = true; } else { entry.State = EntityState.Modified; } }); }
在EF Core 1.1其余的就是关于Add、Update等方法的异步操作了,对于操作数据库不至于阻塞的情况也还是挺好的。
关于EF Core 1.1中一些基本的知识我们过了一遍,下面我们来看看这些方法到底该如何高效使用呢?
关于这个方法就没有太多叙述的了,对应的则是异步方法。我们重点看看其他的方法。
当我们根据主键去更新所有实体这个so easy了,我们在Blog表添加如下数据。
(1)更新方式一
现在我们查出Id=1的实体,然后将Name进行修改如下:
IBlogRepository _blogRepository; public HomeController(IBlogRepository blogRepository) { _blogRepository = blogRepository; } public IActionResult Index() { var blog = _blogRepository.GetSingle(d => d.Id == 1); blog.Name = "EntityFramework Core 1.1"; _blogRepository.Commit(); return View(); }
上述我们直接查询出来主键对应的实体然后修改其值,最后提交更新其实体的对应修改的属性。最后顺理成章的数据字段进行了修改
我们知道因为查询出来的实体在未关闭变更追踪的情况下始终都是被追踪的,所以必须进行对应修改,但是要是下面的情况呢。
public IActionResult Index(int Id,Blog blog) { return Ok(); }
在客户端对数据进行了修改,我们需要根据主键Id进行对应属性修改,当然不希望多此一举的话,我们可以根据主键Id去查询对应的实体,然后将属性进行赋值最后提交修改保存到数据库中,大概就演变成如下情况。
public IActionResult Index(int Id,Blog blog) { var oldBlog = _blogRepository.GetSingle(d => d.Id == Id); oldBlog.Name = blog.Name; oldBlog.Url = blog.Url; _blogRepository.Commit(); return Ok(); }
诚然上述方法能达到我的目的,其实还有简便的方法,如下:
(2)更新方式二
既然有简单的方法为何我们不用呢,这样的场景就是更新指定属性,以往的情况都是自己封装一个Update方法,然后利用反射去包含需要修改的属性接着更改其属性的状态为修改,最后提交修改即可。是的,这就是我们说的方法,但是,但是在EF Core 1.1中完全不需要我们去封装,我们需要做的只是封装成一个通用方法即可,内置实现EF Core已经帮我们实现,我们来看看。
void Update(T entity, params Expression<Func<T, object>>[] properties);
很熟悉吧,我们在基仓储接口给出这样一个接口,接着我们来实现此接口,如下:
public void Update(T entity, params Expression<Func<T, object>>[] properties) { _context.Entry(entity).State = EntityState.Unchanged; foreach (var property in properties) { var propertyName = ExpressionHelper.GetExpressionText(property); _context.Entry(entity).Property(propertyName).IsModified = true; } }
是不是够简单粗暴,开源就是好啊,查找资料时发现老外已经给出了具体实现,当直接调用时居然发现已经给我们封装了,接下来我们再来修改指定的属性就变成了如下:
public IActionResult Index() { var blog = new Blog() { Id = 1, Name = "EntityFramework Core 1.1" }; _blogRepository.Update(blog, d => d.Name); _blogRepository.Commit(); return Ok(); }
上述只是演示,实际项目当中时我们只需给出我们修改的主键和实体即可。如果是修改实体集合的话,再重载一个遍历就ok。到这里你是不是发现已经非常完美了,还有更完美的解决方案,请继续往下看。
(3)更新方式三
其实在ASP.NET Core MVC中有比上面进一步还爽的方式通过利用TryUpdateModelAsync方法来实现,此方法有多个重载来实现,完全不需要我们去封装。如下:
public async Task<IActionResult> Index() { var blog = _blogRepository.GetSingle(d => d.Id == 1); blog.Name = "EntityFramework Core 1.1"; await TryUpdateModelAsync(blog, "", d => d.Name); _blogRepository.Commit(); return Ok(); }
上述三种更新方式各有其应用场景,如果必须要总结的话就主要是第二种方式和第三种方式该如何取舍,第二种方式通过我们手动封装的方式不需要再进行查询,直接更改其状态进行提交更新即可,而第三种方式需要进行查询才会被追踪最终提交更新,看个人觉得哪种方式更加合适就取哪种吧。关于EF Core 1.1中对于数据更新我们就讲解完了,我们再来看看删除。
对于上述和更新一样如果该实体已经被变更追踪,直接调用内置的方法Delete方法即可,大部分场景下是根据主键去删除数据。这里有两种方式供我们选择,请往下看。
(1)删除方式一
public IActionResult Index() { var blog = _blogRepository.GetSingle(d => d.Id == 1); _blogRepository.Delete(blog); _blogRepository.Commit(); return Ok(); }
我们查询出需要删除的实体,然后通过调用Remove(这里我封装了)方法将其标识为Deleted状态进行删除,当查询数据我们可以关闭变更追踪,一来数据量大的话对内存压力不会太大,二来因为调用Remove方法会将其标识为Deleted状态也会被追踪,不会有任何问题。
(2)删除方式二【推荐】
为了尽量减少请求时间,我们能一步完成的何必要用两步呢,我们完全可以直接实例化一个实体,将其主键赋值,最后修改其状态为Deleted,最终将持久化到数据库中删除对应的数据。如下:
public IActionResult Index() { var blog = new Blog() { Id = 1 }; _blogRepository.Delete(blog); _blogRepository.Commit(); return Ok(); }
最后还剩下一个查询没有讲述,这个和添加方法一样,比较简单我么基本过一下即可。由于在EF Core中不再支持延迟加载,所以我们需要通过Include显式获取我们需要的导航属性,比如如下:
DbContext dbContext;
dbContext.Set<Blog>().Include(d => d.Posts);
如果有多个导航属性,我们接着进行ThenInclude,如下:
DbContext dbContext;
dbContext.Set<Blog>().AsNoTracking().Include(d => d.Posts).ThenInclude(d => d....).
为了避免这样多次ThenInclude,方便调用我们进行如下封装即可:
public T GetSingle(Expression<Func<T, bool>> predicate, params Expression<Func<T, object>>[] includeProperties) { IQueryable<T> query = _context.Set<T>(); foreach (var includeProperty in includeProperties) { query = query.Include(includeProperty); } return query.Where(predicate).FirstOrDefault(); }
此时我们只需要进行对应调用即可,大概如下:
_blogRepository.GetSingle( d=>d.Id == 1, p=>p.Posts, p=>....)
本节我们比较详细的叙述了EntityFramework Core 1.1中一些方法的正确使用以及相关理论知识,下节再讲讲其他理论知识,EF Core 1.1现在非常稳定,不要担心再出太多坑,是时候学学EF Core了,come on。