小窍门
本文档中的代码可以作为 可运行的示例在 GitHub 上找到。
背景
更改跟踪 意味着 EF Core 会自动确定应用程序在加载的实体实例上执行了哪些更改,以便在调用时 SaveChanges 将这些更改保存回数据库。 EF Core 通常通过在从数据库加载实例时拍摄实例的 快照 ,并将该快照与分发给应用程序的实例 进行比较 来执行此作。
EF Core 附带了内置逻辑,用于对数据库中使用的大多数标准类型进行快照和比较,因此用户通常不需要担心本主题。 但是,当属性通过 值转换器映射时,EF Core 需要对可能很复杂的任意用户类型执行比较。 默认情况下,EF Core 使用类型定义的默认相等比较(例如 Equals
方法);在快照过程中,值类型会被复制以生成快照,而对于 引用类型 则不进行复制,直接使用同一实例作为快照。
如果内置比较行为不合适,用户可以提供 值比较器,其中包含用于快照、比较和计算哈希代码的逻辑。 例如,下面为属性List<int>
设置值转换,使其被转换为数据库中的 JSON 字符串,同时定义适当的值比较器。
modelBuilder
.Entity<EntityType>()
.Property(e => e.MyListProperty)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<List<int>>(v, (JsonSerializerOptions)null),
new ValueComparer<List<int>>(
(c1, c2) => c1.SequenceEqual(c2),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
c => c.ToList()));
有关更多详细信息,请参阅下面的 可变类 。
请注意,在确定两个键值在解析关系时是否相同时,也会使用值比较器;下面对此进行了说明。
浅表与深度比较
对于诸如int
之类的小型不可变值类型,EF Core 的默认逻辑运作良好:在快照时,该值会被复制 as-is ,并与该类型的内置相等比较进行对比。 实现自己的值比较器时,请务必考虑使用深度或浅表比较(以及创建快照)逻辑是否合适。
考虑字节数组,它可以任意大。 可以比较这些值:
- 通过引用,只有在使用新的字节数组时才会检测到差异
- 通过深入比较,可以检测到数组中字节的突变
默认情况下,EF Core 对非键字节数组使用这些方法中的第一种方法。 也就是说,仅比较引用,并且仅当现有字节数组替换为新字节数组时,才会检测到更改。 这是一个务实的决定,避免复制整个数组,并在执行 SaveChanges时比较字节到字节。 这意味着,像替换一个图像为另一个图像这样的常见场景是通过高效的方式处理的。
另一方面,当字节数组用于表示二进制键时,引用相等性将不起作用,因为 FK 属性不太可能设置为与需要比较的 PK 属性 相同的实例 。 因此,EF Core 对用作键的字节数组进行深入比较;由于二进制键通常较短,这不太可能对性能产生大的影响。
请注意,所选的比较和快照逻辑必须彼此对应:深度比较需要深度快照才能正常运行。
简单不可变类
请考虑使用值转换器映射简单不可变类的属性。
public sealed class ImmutableClass
{
public ImmutableClass(int value)
{
Value = value;
}
public int Value { get; }
private bool Equals(ImmutableClass other)
=> Value == other.Value;
public override bool Equals(object obj)
=> ReferenceEquals(this, obj) || obj is ImmutableClass other && Equals(other);
public override int GetHashCode()
=> Value.GetHashCode();
}
modelBuilder
.Entity<MyEntityType>()
.Property(e => e.MyProperty)
.HasConversion(
v => v.Value,
v => new ImmutableClass(v));
此类属性无需进行特殊的比较处理或创建快照,因为:
- 已重载相等性,以确保不同实例可以正确比较。
- 类型是不可变的,因此不可能改变快照值
因此,在这种情况下,EF Core 的默认行为是正常的。
简单不可变结构
简单结构的映射也很简单,不需要特殊的比较器或快照。
public readonly struct ImmutableStruct
{
public ImmutableStruct(int value)
{
Value = value;
}
public int Value { get; }
}
modelBuilder
.Entity<EntityType>()
.Property(e => e.MyProperty)
.HasConversion(
v => v.Value,
v => new ImmutableStruct(v));
EF Core 内置支持生成已编译的、按成员逐一比较的结构属性比较。 这意味着结构体不需要为了 EF Core 重写等值判断,但您仍可能出于 其他原因选择这样做。 此外,不需要特殊快照,因为结构体是不可变的,总是以成员方式复制。 (对于可变结构也是如此,但 一般应避免可变结构。
可变类
建议尽可能将不可变类型(类或结构)与值转换器一起使用。 这通常更高效,并且具有比使用可变类型的更简洁的语义。 但是,也就是说,通常使用应用程序无法更改的类型属性。 例如,映射一个包含数字列表的属性:
public List<int> MyListProperty { get; set; }
List<T> 类:
- 具有引用相等性;包含相同值的两个列表被视为不同的列表。
- 可变;可以添加和删除列表中的值。
在列表属性上进行典型值转换时,可能会在 JSON 和列表之间进行双向转换。
modelBuilder
.Entity<EntityType>()
.Property(e => e.MyListProperty)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<List<int>>(v, (JsonSerializerOptions)null),
new ValueComparer<List<int>>(
(c1, c2) => c1.SequenceEqual(c2),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
c => c.ToList()));
构造函数 ValueComparer<T> 接受三个表达式:
- 用于检查相等性的表达式
- 用于生成哈希代码的表达式
- 用于创建值的快照的表达式
在这种情况下,通过检查数字序列是否相同来完成比较。
同样,哈希代码是从同一序列生成的。 (请注意,这是可变值的哈希代码,因此 可能会导致问题。如果可以,请改为不可变。
快照是通过克隆列表来创建的 ToList
。 同样,仅当列表将发生变异时,才需要这样做。 如果可以,请保持不可变。
注释
值转换器和比较器是使用表达式而不是简单的委托构造的。 这是因为 EF Core 将这些表达式插入到一个更复杂的表达式树中,然后编译为实体塑造器委托。 从概念上讲,这类似于编译器内联。 例如,简单的转换可能只是在强制转换中编译的,而不是调用另一种方法来执行转换。
关键比较器
背景部分介绍为何关键比较可能需要特殊语义的缘由。 请确保在设置主键、主导键或外键属性时,为键创建一个适当的比较器。
在极少数情况下,在同一属性上需要不同的语义时使用 SetKeyValueComparer 。
注释
重写默认比较器
有时,EF Core 使用的默认比较可能不适用。 例如,在默认情况下,EF Core 不能检测到字节数组的突变。 可以通过为属性指定不同的比较器来覆盖此项。
modelBuilder
.Entity<EntityType>()
.Property(e => e.MyBytes)
.Metadata
.SetValueComparer(
new ValueComparer<byte[]>(
(c1, c2) => c1.SequenceEqual(c2),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
c => c.ToArray()));
EF Core 现在将比较字节序列,因此将检测字节数组突变。