前言(其实是复习)
通过之前的学习,
我们了解到DbContext实际上是一个"半手工"的Session
学过Hibernate的都知道,
它的难度很大程度上在于了解
Session对对象状态的跟踪(或者说是管理)会始终与数据库中表的记录同步,
这其中的原理则是通过Hibernate中对象的三种状态实现的
1.瞬时态
2.持久态
3.离线态
先不说Session是如何改变对象的状态,以及其复杂的内部结构(其实是快忘了)
一.EFCore的实体状态
EFCore的实体状态有五种,分别是
- Detached离线的
- Unchanged未改变的
- Deleted被删除
- Modified被更改的
- Added别添加的
在EntityState枚举中能找到它们
// 摘要:
// The state in which an entity is being tracked by a context.
public enum EntityState
{
Detached = 0,
Unchanged = 1,
Deleted = 2,
Modified = 3,
Added = 4
}
二.实体状态对应的场景
1.Detached没有设置主键,且没有和当前数据库上下文建立关联
static void Main(string[] args)
{
SGDbContext context = new SGDbContext();
//一个新建的对象,没有设置主键,没有放入DbContext对象中
var detachedObject = new StudentSec();
detachedObject.StudentSecName = "張三";
Console.WriteLine(context.Entry<StudentSec>(detachedObject).State);
}
结果
Detached
2.Unchanged从数据库中查询出的没有做任何变更的对象,
static void Main(string[] args)
{
//此处的DbcONTEXT用例可以是任何一个DbContext
PPDbContext context = new PPDbContext();
Pet pet = context.pets.First();
Person p1 = context.persons.First();
DisplayEntityState(context.ChangeTracker.Entries());
}
//查询实体状态的方法
private static void DisplayEntityState(IEnumerable<EntityEntry> entities)
{
foreach(var entry in entities)
{
//循环输出DbContext类实体中对象的状态
Console.WriteLine($"Entity{entry.Entity.GetType().Name},EntityState:{entry.State}");
}
}
结果:
EntityPet,EntityState:Unchanged
EntityPerson,EntityState:Unchanged
3.Added状态,被托管给DbContext(由EFCore)生成主键,但没有savechanges()
static void Main(string[] args)
{
PPDbContext context = new PPDbContext();
Pet pet = new Pet();
pet.name = "kksk";
pet.owner = null;
context.Add(pet);
DisplayEntityState(context.ChangeTracker.Entries());
}
结果:
EntityPet,EntityState:Added
4.Deleted状态,当(已存在的)实体被纳入数据库上下文的Remove方法后
static void Main(string[] args)
{
//此处的DbcONTEXT用例可以是任何一个DbContext
PPDbContext context = new PPDbContext();
Pet pet = context.pets.First();
context.Remove(pet);
DisplayEntityState(context.ChangeTracker.Entries());
}
结果:
EntityPet,EntityState:Deleted
5.Modified存在的实体属性被修改后的状态
static void Main(string[] args)
{
//此处的DbcONTEXT用例可以是任何一个DbContext
PPDbContext context = new PPDbContext();
Pet pet = context.pets.First();
pet.name = "55kai";
DisplayEntityState(context.ChangeTracker.Entries());
}
结果:
EntityPet,EntityState:Modified
三.访问跟踪的实体(官方文档写是访问,这不明摆着的是查询吗)
DbContext.Entry | 为给定的实体实例返回 EntityEntry< TEntity > 实例。 |
ChangeTracker.Entries | 为所有跟踪的实体或某种指定类型的所有跟踪的实体返回 EntityEntry< TEntity > 实例。 |
DbContext.Find、DbContext.FindAsync、DbSetTEntity.Find 和 DbSetTEntity.FindAsync | 按主键查找单个实体,首先查找跟踪的实体,然后根据需要查询数据库。 |
DbSetTEntity>.Local | 就是DbContext的集合 |
PS:后面两种方式就是纯粹的查询方式了,不知道官网文档为什么把它放到访问跟踪的实体中,这样恐怕会造成文档的内容重叠,不方便读者阅读(机翻很多已经不好阅读了)
这里只需要介绍一下这个EntityEntry即可,
在上面的代码中,我们就通过了ChangeTrackers.Entities获取了所有的EntityEntry< TEntity > 进行判断
下面是EntityEntry的的源码(不需要仔细看,快速的浏览即可),
[DebuggerDisplay("{InternalEntry,nq}")]
public class EntityEntry : IInfrastructure<InternalEntityEntry>
{
public virtual PropertyValues CurrentValues { get; }
//该是个是否已经有键值
public virtual bool IsKeySet { get; }
//实体类中的集合属性
public virtual IEnumerable<CollectionEntry> Collections { get; }
//实体类中的引用类型
public virtual IEnumerable<ReferenceEntry> References { get; }
//实体类的属性
public virtual IEnumerable<PropertyEntry> Properties { get; }
//实体类中的导航属性
public virtual IEnumerable<NavigationEntry> Navigations { get; }
//实体类中的成员(方法),
public virtual IEnumerable<MemberEntry> Members { get; }
//实体类型的 IEntityType 元数据。
public virtual IEntityType Metadata { get; }
//当前的DbContext数据库上下文
public virtual DbContext Context { get; }
//当前实体的状态
public virtual EntityState State { get; set; }
//当前实体的本体(其实整个类就像一个Wrapper)
public virtual object Entity { get; }
public virtual PropertyValues OriginalValues { get; }
public virtual CollectionEntry Collection([NotNullAttribute] string propertyName);
//仅强制检测此实体的更改
public virtual void DetectChanges();
[EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals(object obj);
public virtual PropertyValues GetDatabaseValues();
[AsyncStateMachine(typeof(<GetDatabaseValuesAsync>d__40))]
public virtual Task<PropertyValues> GetDatabaseValuesAsync(CancellationToken cancellationToken = default);
[EditorBrowsable(EditorBrowsableState.Never)]
public override int GetHashCode();
//再获取MemberEntry
public virtual MemberEntry Member([NotNullAttribute] string propertyName);
//再获取NavigatEntry
public virtual NavigationEntry Navigation([NotNullAttribute] string propertyName);
//再获取属性Entry
public virtual PropertyEntry Property([NotNullAttribute] string propertyName);
//再获取引用Entry
public virtual ReferenceEntry Reference([NotNullAttribute] string propertyName);
[AsyncStateMachine(typeof(<ReloadAsync>d__42))]
public virtual Task ReloadAsync(CancellationToken cancellationToken = default);
public override string ToString();
}
其实这个EntityEntry就是一个对实体对象的大的包装类
主要有这几个属性
- EntityEntry.State 获取并设置实体的 EntityState。
- EntityEntry.Entity 实体本体
- EntityEntry.Context 正在跟踪此实体的 DbContext。
- EntityEntry.Metadata 实体类型的 IEntityType 元数据。
- EntityEntry.DetectChanges() 仅强制检测此实体的更改
四.快照更改跟踪(这里源文档说的很晦涩,我仅以我的理解判断一下)
原文文档链接:
https://docs.microsoft.com/zh-cn/ef/core/change-tracking/change-detection
默认情况下,DbContext 实例首次跟踪每个实体时,EF Core 会创建这些实体的属性值的快照。 然后将此快照中存储的值与实体的当前值进行比较,以确定哪些属性值已更改。
在调用 SaveChanges 时,会进行更改检测,以确保在将更新发送到数据库之前检测到所有更改的值。 但是,更改检测也会发生在其他时间,以确保应用程序使用最新的跟踪信息。 可以通过调用
ChangeTracker.DetectChanges() 随时强制执行更改检测。
(简而言之,就是EFCore会创建数据库上下文跟踪对象的快照,在你调用SaveChange()之时,检查实体的更改(包括实体的状态,属性值等等。。),发送SQL使得跟踪的实体和数据库的表的数据和关系一致),可以使用ChangeTracker.DetectChanges()提前检查上述的更改
这里以上两天文章的多对多的关系举例
static void Main(string[] args)
{
MMDbContext context = new MMDbContext();
var stuOne = context.studentCopies.First();
stuOne.courses.Add(new Course() { course_name = "張三說刑法",teacher_name = "羅翔" });
stuOne.age = 999;
//第一次打印跟改跟踪器视图,此时还没有检查实体关系的改变
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
Console.WriteLine("分割線---------------------強行啟動修改檢查");
//这里我们启动强制关系检查,更新跟改
context.ChangeTracker.DetectChanges();
//再次打印跟改跟踪器视图,查看此时的变化情况
Console.WriteLine(context.ChangeTracker.DebugView.LongView);
}
结果:
StudentCopy {id: 1} Unchanged
id: 1 PK
age: 999 Originally 21
name: '羅翔'
courses: [<not found>]
//在调用 ChangeTracker.DetectChanges() 之前查看更改跟踪器调试视图表明未检测到所做的更改,因此这些更改不会反映在实体状态和修改的属性数据中:
分割線---------------------強行啟動修改檢查
CourseStudentCopy (Dictionary<string, object>) {coursesid: -2147482647, studentsid: 1} Added
coursesid: -2147482647 PK FK Temporary
studentsid: 1 PK FK
Course {id: -2147482647} Added //检查到了新加的关联对象
id: -2147482647 PK Temporary
course_name: '張三說刑法'
teacher_name: '羅翔'
students: [{id: 1}]
StudentCopy {id: 1} Modified //修改Unchanged状态为Modified
id: 1 PK
age: 999 Modified Originally 21
name: '羅翔'
courses: [{id: -2147482647}]
DetectChanges()方法不仅可以显式的调用,也被以下方法隐式的调用
- DbContext.SaveChanges 和 DbContext.SaveChangesAsync
- ChangeTracker.Entries() 和 ChangeTracker.EntriesTEntity(),
- ChangeTracker.HasChanges()
- DbSetTEntity>.Local
- ChangeTracker.CascadeChanges(),
在某些情况下,更改检测仅发生在单个实体实例上,而不是整个跟踪的实体图上。 具体情况如下:
1.使用 DbContext.Entry 时,以确保实体的状态和修改的属性处于最新状态。
2.使用 EntityEntry 方法(如 、Collection、Reference 或 Member)时,以确保属性修改、当前值等处于最新状态。
3.由于已提供所需的关系,将要删除依赖/子实体时。 这会检测何时不应删除实体,因为它已重新成为父级。
可以通过调用 EntityEntry.DetectChanges() 来显式触发单个实体的本地更改检测。
(PS:可以调用ChangeTracker.AutoDetectChangesEnabled来关闭自动的跟改检测,这在批量处理实体时可以会有一点性能上的提升,但不检查数据的一致性)
五.(实体更改后)实体的通知
https://docs.microsoft.com/zh-cn/ef/core/change-tracking/change-detection
通知实体是不使用实体快照的一种检测一致性的方式,(不常用)
需要实现
INotifyPropertyChanging 和 INotifyPropertyChanged 接口,这些接口是 .NET 基类库 (BCL) 的一部分。 这些接口定义在更改属性值之前和之后必须触发的事件。
例如:
public class Blog : INotifyPropertyChanging, INotifyPropertyChanged
{
public event PropertyChangingEventHandler PropertyChanging;
public event PropertyChangedEventHandler PropertyChanged;
private int _id;
public int Id
{
get => _id;
set
{
PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(nameof(Id)));
_id = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Id)));
}
}
private string _name;
public string Name
{
get => _name;
set
{
PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(nameof(Name)));
_name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
public IList<Post> Posts { get; } = new ObservableCollection<Post>();
}