实体框架只会将相关实体属性设置为 "null" 如果我第一次获取该属性



编辑这似乎适用于在一个方向上引用另一个实体的任何实体属性。换句话说,对于下面的示例,Bar覆盖Equality这一事实似乎无关紧要。

假设我有以下类:

public class Foo
{
public int? Id { get; set; }
public virtual Bar { get; set; }
}
public class Bar : IEquatable<Bar>
{
public int Id { get; set; }
public override bool Equals(object obj)
{
var other = obj as Bar;
return Equals(other);
}
public bool Equals(Bar other)
{
if (object.Equals(other, null))
return false;
return this.Id == other.Id;
}
public static bool operator ==(Bar left, Bar right)
{
return object.Equals(left, right);
}
public static bool operator !=(Bar left, Bar right)
{
return !object.Equals(left, right);
}
public override int GetHashCode()
{
return Id.GetHashCode();
}
}

请注意,在这里,"Bar"有意具有"Id"相等,因为它或多或少代表了一个查找表——因此,具有相同Id的任何两个对象引用都应该始终被视为相同。

奇怪的是,当我将Foo.Bar设置为另一个Bar实例时,这一切都很好——一切都会按预期更新。

但是,如果foo在从DbContext检索时有一个现有的Bar,我会:

foo.Bar = null

那么财产实际上并没有改变!

如果我这样做:

var throwAway = foo.Bar;
foo.Bar = null;

然后该属性将实际设置并保存为null。

由于Foo.Bar属性只是一个虚拟的、自动实现的属性,我只能得出结论,这与延迟加载和实体框架代理有关——但我不知道为什么这种特定场景会导致问题。

为什么实体框架会这样做,我如何才能让它真正可靠地设置null

作为一种变通方法,我发现缓解这个问题的最简单方法是让setter在将backing字段设置为null之前调用getter,例如

public class Foo
{
public int? Id { get; set; }
private Bar _bar;
public virtual Bar 
{ 
get { return _bar; }
set
{
var entityFrameworkHack = this.Bar; //ensure the proxy has loaded
_bar = value;
}
}
}

这样,无论其他代码是否已经实际加载了该属性,该属性都可以工作,代价是可能不需要的实体加载。

你是对的,这是因为你在EF中使用了延迟加载(virtual属性)。您可以删除virtual(但这对您来说可能是不可能的)。您在问题调用属性中描述的其他方式,并将其设置为null。

你也可以在SO.上阅读关于这个问题的另一个主题

使其工作的一种方法是使用属性API:

var foo = context.Foos.Find(1);
context.Entry(foo).Reference(f => f.Bar).CurrentValue = null;
context.SaveChanges();

好处是,这在不通过延迟加载加载foo.Bar的情况下有效,而且它也适用于不支持延迟加载或更改跟踪代理(没有virtual属性)的纯POCO。缺点是需要在要将相关Bar设置为null的位置提供一个context实例。

我对官方的解决方法不满意:

context.Entry(foo).Reference(f => f.Bar).CurrentValue = null;

因为它涉及POCO对象的用户的太多上下文知识。我的解决方案是在将值设置为null时触发懒惰属性的加载,这样我们就不会从EF:中得到误报比较

public virtual User CheckoutUser
{
get { return checkoutUser; }
set
{
if (value != null || !LazyPropertyIsNull(CheckoutUser))
{
checkoutUser = value;
}
}
}

并且在我的基本DbEntity类中:

protected bool LazyPropertyIsNull<T>(T currentValue) where T : DbEntity
{
return (currentValue == null);
}

将属性传递给LazyPropertyIsNull函数会触发延迟加载并进行正确的比较。

请在EF问题日志上对此问题进行投票:

就我个人而言,我认为Nathan的答案(属性setter内部的惰性加载)是最稳健的。然而,它使您的域类(每个属性10行)激增,并使其可读性降低。

作为另一种变通方法,我将两种方法编译为扩展方法:

public static void SetToNull<TEntity, TProperty>(this TEntity entity, Expression<Func<TEntity, TProperty>> navigationProperty, DbContext context = null)
where TEntity : class
where TProperty : class
{
var pi = GetPropertyInfo(entity, navigationProperty);
if (context != null)
{
//If DB Context is supplied, use Entry/Reference method to null out current value
context.Entry(entity).Reference(navigationProperty).CurrentValue = null;
}
else
{
//If no DB Context, then lazy load first
var prevValue = (TProperty)pi.GetValue(entity);
}
pi.SetValue(entity, null);
}
static PropertyInfo GetPropertyInfo<TSource, TProperty>(    TSource source,    Expression<Func<TSource, TProperty>> propertyLambda)
{
Type type = typeof(TSource);
MemberExpression member = propertyLambda.Body as MemberExpression;
if (member == null)
throw new ArgumentException(string.Format(
"Expression '{0}' refers to a method, not a property.",
propertyLambda.ToString()));
PropertyInfo propInfo = member.Member as PropertyInfo;
if (propInfo == null)
throw new ArgumentException(string.Format(
"Expression '{0}' refers to a field, not a property.",
propertyLambda.ToString()));
if (type != propInfo.ReflectedType &&
!type.IsSubclassOf(propInfo.ReflectedType))
throw new ArgumentException(string.Format(
"Expression '{0}' refers to a property that is not from type {1}.",
propertyLambda.ToString(),
type));
return propInfo;
}

这允许您提供DbContext(如果有),在这种情况下,它将使用最有效的方法并将条目引用的CurrentValue设置为null。

entity.SetToNull(e => e.ReferenceProperty, dbContext);

如果没有提供DBContext,它将首先进行延迟加载。

entity.SetToNull(e => e.ReferenceProperty);

如果您想避免操作EntityEntry,可以通过在POCO中包含FK属性(如果您不希望用户访问,可以将其设为私有属性)来避免对数据库的延迟加载调用,如果settervalue为null,则让Nav属性setter将该FK值设置为null。示例:

public class InaccessibleFKDependent
{
[Key]
public int Id { get; set; }
private int? PrincipalId { get; set; }
private InaccessibleFKPrincipal _principal;
public virtual InaccessibleFKPrincipal Principal
{
get => _principal;
set
{
if( null == value )
{
PrincipalId = null;
}
_principal = value;
}
}
}
public class InaccessibleFKDependentConfiguration : IEntityTypeConfiguration<InaccessibleFKDependent>
{
public void Configure( EntityTypeBuilder<InaccessibleFKDependent> builder )
{
builder.HasOne( d => d.Principal )
.WithMany()
.HasForeignKey( "PrincipalId" );
}
}

测试:

public static void TestInaccessibleFKSetToNull( DbContextOptions options )
{
using( var dbContext = DeleteAndRecreateDatabase( options ) )
{
var p = new InaccessibleFKPrincipal();
dbContext.Add( p );
dbContext.SaveChanges();
var d = new InaccessibleFKDependent()
{
Principal = p,
};
dbContext.Add( d );
dbContext.SaveChanges();
}
using( var dbContext = new TestContext( options ) )
{
var d = dbContext.InaccessibleFKDependentEntities.Single();
d.Principal = null;
dbContext.SaveChanges();
}
using( var dbContext = new TestContext( options ) )
{
var d = dbContext.InaccessibleFKDependentEntities
.Include( dd => dd.Principal )
.Single();
System.Console.WriteLine( $"{nameof( d )}.{nameof( d.Principal )} is NULL: {null == d.Principal}" );
}
}

最新更新