EF:使用延迟加载的必需属性时,更新验证失败


69

鉴于这个极其简单的模型:

public class MyContext : BaseContext
{
    public DbSet<Foo> Foos { get; set; }
    public DbSet<Bar> Bars { get; set; }
}

public class Foo
{
    public int Id { get; set; }
    public int Data { get; set; }
    [Required]
    public virtual Bar Bar { get; set; }
}

public class Bar
{
    public int Id { get; set; }
}

以下程序失败:

object id;
using (var context = new MyContext())
{
    var foo = new Foo { Bar = new Bar() };
    context.Foos.Add(foo);
    context.SaveChanges();
    id = foo.Id;
}
using (var context = new MyContext())
{
    var foo = context.Foos.Find(id);
    foo.Data = 2;
    context.SaveChanges(); //Crash here
}

DbEntityValidationException。中找到的信息EntityValidationErrors需要的酒吧现场。

但是,如果我Bar通过在以下行之前添加以下行来强制加载属性SaveChanges

var bar = foo.Bar;

一切正常。如果我删除该[Required]属性,这也适用。

这真的是预期的行为吗?是否有任何解决方法(每次我要更新实体时都加载每个必需的参考)


1
昨天我刚好反对这个,所以我可以确认你的观察。我正在寻找解决方法。这似乎非常不幸。
Xhalent

1
这不仅是导航特性的问题。我已经抱怨说在MSDN:social.msdn.microsoft.com/Forums/en-US/adodotnetentityframework/...
拉吉斯拉夫Mrnka

1
坦白地说,由于围绕可空性的所有这些问题,我认为EF代理简直就是断头且危险。请参阅此处的问题:entityframework.codeplex.com/workitem/1571还存在无法将已卸载引用设置为null的问题(因为它已经为null / unloaded)。基本上,代理在EF中不起作用,即使更改跟踪的代理也表现出相同的行为。这种情况令人震惊,每个人都必须编写骇客解决方案来解决基本的日常情况。
罗伯·肯特

Answers:


54

我发现以下帖子对相同问题有答案:

造成此问题的原因是,在RC和RTM验证中,懒惰不再加载任何属性。进行此更改的原因是,当一次保存大量具有延迟加载属性的实体时,验证将使它们一个接一个地潜在地导致大量意外交易和严重的性能下降。

解决方法是在使用.include()保存或验证之前显式加载所有已验证的属性,您可以在此处阅读有关如何执行此操作的更多信息:http : //blogs.msdn.com/b/adonet/archive/2011/01 / 31 /在ef-feature-ctp5-part-6-loading-related-entities.aspx中使用dbcontext

我的看法是,这是一个糟糕的代理实现。虽然自然地可以避免不必要地遍历对象图并检索延迟加载的属性(但是在Microsoft的EF的第一个化身中显然被忽略了),但您不必不必取消代理包装就可以验证其存在。再三考虑,我不确定为什么无论如何都要走对象图,因为ORM的变更跟踪器肯定知道哪些对象需要验证。

我不确定为什么会出现此问题,但是如果使用NHibernate,我肯定不会遇到这个问题。

我的“解决方法”-我所做的是在EntityTypeConfiguration类中定义关系的Required属性,并删除了Required属性。这应该使其工作正常。这意味着您将不验证关系,但是将使更新失败。并非理想的结果。


5
我最终写了一个通用LoadAllReferences方法。我对EF感到更加失望。
2011年

6
感谢你的回答。这是我很长一段时间以来见过的最愚蠢的错误。有人怎么认为这对于ORM可以接受?
黑暗猎鹰2012年

2
我对此感到失望。是否通过删除虚拟机使所有必需的导航属性变为非延迟状态,这不是另一种解决方法?
卡尔·G

2
@卡尔 如果使所有引用都不是惰性的,那么最终会从数据库中检索数量不确定的对象,对于特定的工作单元,实际上需要使用任意数量的对象。这就是为什么可以进行延迟加载。
Xhalent

2
是的,框架肯定知道Bar字段没有更改,因此不需要检查。EF是个玩笑。我希望我没有选择它,现在切换为时已晚,但是我永远不会再使用它。
Spongman 2013年

44

好的,这是真正的答案=)

首先一点解释

如果您有一个属性(例如Bar),并且注意到FK(ForeignKey),那么您也可以在模型中具有相应的FK字段,因此,如果我们只需要FK而不是实际的FK,Bar则不需要它去数据库:

[ForeignKey("BarId")]
public virtual Bar Bar { get; set; }
public int BarId { get; set; }

现在,为了回答你的问题,你能做些什么来让BarRequired是标志BarId财产的要求,而不是Bar本身:

[ForeignKey("BarId")]
public virtual Bar Bar { get; set; }
[Required] //this makes the trick
public int BarId { get; set; }

这就像一个魅力=)


1
不错的答案(已批准)。我的FK与属性的命名相同,所以我不得不这样做[Required, Column("Bar"), ForeignKey("Bar")] public int? BarId { get; set; },这很丑陋,因为我本质上是在破解我的域模型以满足EF的怪癖。
2011年

2
这样做的问题是,在创建新的Foo()时,您需要同时设置Bar和BarId属性,如果仅设置Bar属性,则将无法通过所需的BarId验证。另外,BarId必须为可空值,才能使所需属性起作用。
Xhalent 2011年

这对我有用。我认为BarId应该为空,以反映Bar尚未设置,此外,我认为[Required]在标量属性上毫无意义。@Xhalent,您可以在Bar属性中设置BarId。
卡尔·G

感谢您的回答!我不需要[Required]属性,但是我的模型中没有ForeignKey(Id)-现在它就像一个吊饰!(我使用的是EF5)
kapsiR 2013年

1
但是,如果删除Foo,它将不会级联删除到Bar。当您从上下文和SaveChanges中删除Foo时,Bar在删除之前被设置为null,然后您收到以下错误消息:“遇到无效数据。缺少必需的关系。检查StateEntries以确定约束冲突的来源。” 但是StateEntries中没有任何内容可以指示问题。
罗伯·肯特

9

透明的解决方法,可忽略卸载参考上的错误

在您的中DbContext,重写ValidateEntity方法可消除未加载的引用上的验证错误。

    private static bool IsReferenceAndNotLoaded(DbEntityEntry entry, string memberName)
    {
        var reference = entry.Member(memberName) as DbReferenceEntry;
        return reference != null && !reference.IsLoaded;
    }

    protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry,
                                                 IDictionary<object, object> items)
    {
        var result = base.ValidateEntity(entityEntry, items);
        if (result.IsValid || entityEntry.State != EntityState.Modified)
        {
            return result;
        }
        return new DbEntityValidationResult(entityEntry,
            result.ValidationErrors
                  .Where(e => !IsReferenceAndNotLoaded(entityEntry, e.PropertyName)));
    }

优点:

  • 透明,当您使用继承,复杂类型,不需要修改模型时不会崩溃...
  • 仅当验证失败时
  • 无反省
  • 仅对无效的卸载引用进行迭代
  • 没有无用的数据加载

3
我认为这是解决此问题的最佳方法。简单并且避免了往返数据库的开销。
mcsdwarken

这确实是一个很好的解决方案,应将其标记为答案。
bubi

5

这是一个半可接受的解决方法

var errors = this.context.GetValidationErrors();
foreach (DbEntityValidationResult result in errors) {
    Type baseType = result.Entry.Entity.GetType().BaseType;
    foreach (PropertyInfo property in result.Entry.Entity.GetType().GetProperties()) {
        if (baseType.GetProperty(property.Name).GetCustomAttributes(typeof(RequiredAttribute), true).Any()) {
            property.GetValue(result.Entry.Entity, null);
        }
    }
}

1
是的,这或多或少是我最近在做的事情。我什至用此功能创建了一个OSS项目Nuget包
Diego Mijelshon 2012年

1
这段代码可以继承吗?我有三个继承级别,并且我得到一个空引用,我认为这是因为property.Name不属于基本类型。
罗伯·肯特

@RobKent我当然也想知道,因为我遇到了与您完全相同的问题。有谁知道?
JensOlsen112

4

如果有人想采用一般方法来解决此问题,则这里有一个自定义DbContext,它根据这些约束条件找出属性:

  • 延迟加载已开启。
  • 具有的属性 virtual
  • 具有任何ValidationAttribute属性的属性。

检索此列表后,在任何SaveChanges需要修改的列表上,它将自动加载所有引用和集合,从而避免任何意外的异常。

public abstract class ExtendedDbContext : DbContext
{
    public ExtendedDbContext(string nameOrConnectionString)
        : base(nameOrConnectionString)
    {
    }

    public ExtendedDbContext(DbConnection existingConnection, bool contextOwnsConnection)
        : base(existingConnection, contextOwnsConnection)
    {
    }

    public ExtendedDbContext(ObjectContext objectContext, bool dbContextOwnsObjectContext)
        : base(objectContext, dbContextOwnsObjectContext)
    {
    }

    public ExtendedDbContext(string nameOrConnectionString, DbCompiledModel model)
        : base(nameOrConnectionString, model)
    {
    }

    public ExtendedDbContext(DbConnection existingConnection, DbCompiledModel model, bool contextOwnsConnection)
        : base(existingConnection, model, contextOwnsConnection)
    {
    }

    #region Validation + Lazy Loading Hack

    /// <summary>
    /// Enumerator which identifies lazy loading types.
    /// </summary>
    private enum LazyEnum
    {
        COLLECTION,
        REFERENCE,
        PROPERTY,
        COMPLEX_PROPERTY
    }

    /// <summary>
    /// Defines a lazy load property
    /// </summary>
    private class LazyProperty
    {
        public string Name { get; private set; }
        public LazyEnum Type { get; private set; }

        public LazyProperty(string name, LazyEnum type)
        {
            this.Name = name;
            this.Type = type;
        }
    }

    /// <summary>
    /// Concurrenct dictinary which acts as a Cache.
    /// </summary>
    private ConcurrentDictionary<Type, IList<LazyProperty>> lazyPropertiesByType =
        new ConcurrentDictionary<Type, IList<LazyProperty>>();

    /// <summary>
    /// Obtiene por la caché y si no lo tuviese lo calcula, cachea y obtiene.
    /// </summary>
    private IList<LazyProperty> GetLazyProperties(Type entityType)
    {
        return
            lazyPropertiesByType.GetOrAdd(
                entityType,
                innerEntityType =>
                {
                    if (this.Configuration.LazyLoadingEnabled == false)
                        return new List<LazyProperty>();

                    return
                        innerEntityType
                            .GetProperties(BindingFlags.Public | BindingFlags.Instance)
                            .Where(pi => pi.CanRead)
                            .Where(pi => !(pi.GetIndexParameters().Length > 0))
                            .Where(pi => pi.GetGetMethod().IsVirtual)
                            .Where(pi => pi.GetCustomAttributes().Exists(attr => typeof(ValidationAttribute).IsAssignableFrom(attr.GetType())))
                            .Select(
                                pi =>
                                {
                                    Type propertyType = pi.PropertyType;
                                    if (propertyType.HasGenericInterface(typeof(ICollection<>)))
                                        return new LazyProperty(pi.Name, LazyEnum.COLLECTION);
                                    else if (propertyType.HasGenericInterface(typeof(IEntity<>)))
                                        return new LazyProperty(pi.Name, LazyEnum.REFERENCE);
                                    else
                                        return new LazyProperty(pi.Name, LazyEnum.PROPERTY);
                                }
                            )
                            .ToList();
                }
            );
    }

    #endregion

    #region DbContext

    public override int SaveChanges()
    {
        // Get all Modified entities
        var changedEntries =
            this
                .ChangeTracker
                .Entries()
                .Where(p => p.State == EntityState.Modified);

        foreach (var entry in changedEntries)
        {
            foreach (LazyProperty lazyProperty in GetLazyProperties(ObjectContext.GetObjectType(entry.Entity.GetType())))
            {
                switch (lazyProperty.Type)
                {
                    case LazyEnum.REFERENCE:
                        entry.Reference(lazyProperty.Name).Load();
                        break;
                    case LazyEnum.COLLECTION:
                        entry.Collection(lazyProperty.Name).Load();
                        break;
                }
            }
        }

        return base.SaveChanges();
    }

    #endregion
}

在哪里IEntity<T>

public interface IEntity<T>
{
    T Id { get; set; }
}

在这些代码中使用了这些扩展:

public static bool HasGenericInterface(this Type input, Type genericType)
{
    return
        input
            .GetInterfaces()
            .Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
}

public static bool Exists<T>(this IEnumerable<T> source, Predicate<T> predicate)
{
    foreach (T item in source)
    {
        if (predicate(item))
            return true;
    }

    return false;
} 

希望能帮助到你,


2

我知道有点晚了...但是,请把这个贴在这里。由于我也对此感到非常恼火。只需将EF告知Include必填字段即可。

注意变化

using (var context = new MyContext())
{
    var foo = context.Foos.Include("Bar").Find(id);
    foo.Data = 2;
    context.SaveChanges(); //Crash here
}

这对我有用,而其他人则没有。添加简单,易于理解。
赎金

0

由于这仍然是EF 6.1.1中的问题,因此我想根据他们的确切模型要求,提供另一个适合某些人的答案。总结问题:

  1. 您需要使用代理进行延迟加载。

  2. 延迟加载的属性标记为必需。

  3. 您想要修改和保存代理,而不必强制加载惰性引用。

当前的EF代理(都不是3个)不可能实现3,这在我看来是一个严重的缺陷。

在我的情况下,惰性属性的行为类似于值类型,因此当我们添加实体且从未更改时会提供其值。我可以通过保护其设置器而不提供更新方法来强制执行此操作,即必须通过构造函数创建它,例如:

var myEntity = new MyEntity(myOtherEntity);

MyEntity具有此属性:

public virtual MyOtherEntity Other { get; protected set; }

因此,EF将不会对此属性执行验证,但我可以确保它在构造函数中不为null。那是一种情况。

假设您不希望以这种方式使用构造函数,则仍可以使用自定义属性来确保验证,例如:

[RequiredForAdd]
public virtual MyOtherEntity Other { get; set; }

RequiredForAdd属性是一个自定义属性,继承自Attribute而不是RequiredAttribute。除了基本属性外,它没有其他属性或方法。

在我的数据库上下文类中,我有一个静态构造函数,该构造函数查找具有那些属性的所有属性:

private static readonly List<Tuple<Type, string>> validateOnAddList = new List<Tuple<Type, string>>();

static MyContext()
{
    FindValidateOnAdd();
}

private static void FindValidateOnAdd()
{
    validateOnAddList.Clear();

    var modelType = typeof (MyEntity);
    var typeList = modelType.Assembly.GetExportedTypes()
        .Where(t => t.Namespace.NotNull().StartsWith(modelType.Namespace.NotNull()))
        .Where(t => t.IsClass && !t.IsAbstract);

    foreach (var type in typeList)
    {
        validateOnAddList.AddRange(type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
            .Where(pi => pi.CanRead)
            .Where(pi => !(pi.GetIndexParameters().Length > 0))
            .Where(pi => pi.GetGetMethod().IsVirtual)
            .Where(pi => pi.GetCustomAttributes().Any(attr => attr is RequiredForAddAttribute))
            .Where(pi => pi.PropertyType.IsClass && pi.PropertyType != typeof (string))
            .Select(pi => new Tuple<Type, string>(type, pi.Name)));
    }
}

现在我们有了需要手动检查的属性列表,我们可以覆盖验证并手动验证它们,将任何错误添加到从基本验证器返回的集合中:

protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items)
{
    return CustomValidateEntity(entityEntry, items);
}

private DbEntityValidationResult CustomValidateEntity(DbEntityEntry entry, IDictionary<object, object> items)
{
    var type = ObjectContext.GetObjectType(entry.Entity.GetType());

    // Always use the default validator.    
    var result = base.ValidateEntity(entry, items);

    // In our case, we only wanted to validate on Add and our known properties.
    if (entry.State != EntityState.Added || !validateOnAddList.Any(t => t.Item1 == type))
        return result;

    var propertiesToCheck = validateOnAddList.Where(t => t.Item1 == type).Select(t => t.Item2);

    foreach (var name in propertiesToCheck)
    {
        var realProperty = type.GetProperty(name);
        var value = realProperty.GetValue(entry.Entity, null);
        if (value == null)
        {
            logger.ErrorFormat("Custom validation for RequiredForAdd attribute validation exception. {0}.{1} is null", type.Name, name);
            result.ValidationErrors.Add(new DbValidationError(name, string.Format("RequiredForAdd validation exception. {0}.{1} is required.", type.Name, name)));
        }
    }

    return result;
}

请注意,我只对验证Add感兴趣。如果您还想在Modify期间进行检查,则需要对属性进行强制加载,还是需要使用Sql命令检查外键值(难道该内容已不在上下文中)?

由于Required属性已被删除,因此EF将创建可为空的FK。为确保数据库完整性,可以在创建数据库后针对数据库运行的Sql脚本中手动更改FK。这将至少捕获具有空问题的“修改”。


0

只是在EF 6.1.2中有同样的问题。为了解决这个问题,您的班级应如下所示:

public class Foo {
    public int Id { get; set; }
    public int Data { get; set; }

    public int BarId { get; set; }

    public virtual Bar Bar { get; set; }

}

如您所见,不需要“ Required”属性,因为Bar属性已经为必需,因为BarId属性不可为空。

因此,如果您希望Bar属性为可为空,则必须编写:

public class Foo {
    public int Id { get; set; }
    public int Data { get; set; }

    public int? BarId { get; set; }

    public virtual Bar Bar { get; set; }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.