EF : 지연로드 된 필수 속성을 사용할 때 업데이트시 유효성 검사 실패
이 매우 간단한 모델을 고려하면 :
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
}
A를 DbEntityValidationException
. 에서 찾은 메시지 EntityValidationErrors
는 Bar 필드가 필요합니다. .
그러나, 나는이로드 강제 경우 Bar
전에 다음 줄을 추가하여 속성을 SaveChanges
:
var bar = foo.Bar;
모든 것이 잘 작동합니다. [Required]
속성을 제거해도 작동 합니다.
이것이 실제로 예상되는 동작입니까? 해결 방법이 있습니까 (엔티티를 업데이트 할 때마다 모든 필수 참조를로드하는 것 외에)
동일한 문제에 대한 답변이있는 다음 게시물 을 찾았습니다 .
이 문제의 원인은 RC 및 RTM 유효성 검사에서 더 이상 속성을 지연로드하지 않기 때문입니다. 이렇게 변경된 이유는 지연로드 된 속성 유효성 검사가있는 많은 엔터티를 한 번에 저장할 때 하나씩 가져 와서 예상치 못한 많은 트랜잭션과 성능 저하를 유발할 수 있기 때문입니다.
해결 방법은 .Include ()를 사용하여 저장하거나 유효성을 검사하기 전에 유효성이 검사 된 모든 속성을 명시 적으로로드하는 것입니다. 자세한 방법은 http://blogs.msdn.com/b/adonet/archive/2011/01 에서 확인할 수 있습니다. /31/using-dbcontext-in-ef-feature-ctp5-part-6-loading-related-entities.aspx
이것에 대한 나의 견해는 꽤 형편없는 프록시 구현입니다. 불필요하게 개체 그래프를 살펴보고 지연로드 된 속성을 검색하는 것은 당연히 피해야 할 일이지만 (하지만 Microsoft의 첫 번째 EF 구현에서는 간과 된 것임) 래퍼가 존재하는지 확인하기 위해 프록시를 해제 할 필요는 없습니다. 다시 생각해 보면, 어차피 개체 그래프를 살펴 봐야하는 이유를 잘 모르겠습니다. ORM의 변경 추적기는 어떤 개체에 유효성 검사가 필요한지 알고 있습니다.
문제가 왜 존재하는지 잘 모르겠지만 NHibernate를 사용하면이 문제가 발생하지 않을 것이라고 확신합니다.
내 '해결 방법'-내가 한 일은 EntityTypeConfiguration 클래스에서 관계의 필수 특성을 정의하고 필수 특성을 제거하는 것입니다. 이것은 잘 작동합니다. 이는 관계의 유효성을 검사하지 않지만 업데이트에 실패 함을 의미합니다. 이상적인 결과가 아닙니다.
좋아, 여기에 진짜 대답이 있습니다 =)
먼저 약간의 설명
Bar
FK ( ForeignKey
)를 표시하는 속성 (예 :)이있는 경우 모델에 해당 FK 필드도있을 수 있으므로 FK 만 필요하고 실제 FK 만 필요한 경우 Bar
데이터베이스로 이동하는 데 필요하지 않습니다.
[ForeignKey("BarId")]
public virtual Bar Bar { get; set; }
public int BarId { get; set; }
이제 질문에 답하기 위해 속성 자체가 아닌 필수 속성 에 플래그 를 지정하는 방법을있는 Bar
그대로 만들 수 있습니다.Required
BarId
Bar
[ForeignKey("BarId")]
public virtual Bar Bar { get; set; }
[Required] //this makes the trick
public int BarId { get; set; }
이것은 매력처럼 작동합니다 =)
언로드 된 참조에서 오류를 무시하는 투명한 해결 방법
에서 메서드를 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)));
}
장점 :
- 투명 하고 상속, 복잡한 유형을 사용할 때 충돌하지 않으며 모델을 수정할 필요가 없습니다.
- 유효성 검사가 실패한 경우에만
- 반사 없음
- 유효하지 않은 언로드 된 참조에서만 반복
- 쓸모없는 데이터 로딩 없음
다음은 반 허용 가능한 해결 방법입니다 .
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);
}
}
}
누구든지이 문제를 해결하기위한 일반적인 접근 방식을 원하는 경우 여기에 이러한 제약 조건을 기반으로 속성을 찾는 사용자 지정 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;
}
도움이 되었기를 바랍니다.
조금 늦었다는 것을 알고 있습니다 ... 그러나 여기에 게시 할 수 없습니다. 나도 이것으로 끔찍하게 짜증이 났기 때문에. EF에게 Include
필수 필드 를 알려 주십시오.
작은 변화를 주목하십시오
using (var context = new MyContext())
{
var foo = context.Foos.Include("Bar").Find(id);
foo.Data = 2;
context.SaveChanges(); //Crash here
}
이것이 EF 6.1.1에서 여전히 문제이기 때문에 정확한 모델 요구 사항에 따라 일부 사람들에게 적합한 다른 답변을 제공 할 것이라고 생각했습니다. 문제를 요약하려면 :
지연로드에는 프록시를 사용해야합니다.
지연로드중인 속성은 필수로 표시됩니다.
지연 참조를 강제로드하지 않고 프록시를 수정하고 저장하려고합니다.
3은 현재 EF 프록시 (둘 중 하나)에서는 불가능하며 이는 제 생각에 심각한 단점입니다.
제 경우에는 lazy 속성이 값 유형처럼 동작하므로 엔티티를 추가 할 때 값이 제공되고 변경되지 않습니다. setter를 보호하고 업데이트하는 방법을 제공하지 않음으로써이를 시행 할 수 있습니다. 즉, 생성자를 통해 생성되어야합니다. 예 :
var myEntity = new MyEntity(myOtherEntity);
MyEntity에는 다음 속성이 있습니다.
public virtual MyOtherEntity Other { get; protected set; }
따라서 EF는이 속성에 대한 유효성 검사를 수행하지 않지만 생성자에서 null이 아닌지 확인할 수 있습니다. 그것은 하나의 시나리오입니다.
그런 방식으로 생성자를 사용하지 않는다고 가정해도 다음과 같은 사용자 지정 특성을 사용하여 유효성 검사를 확인할 수 있습니다.
[RequiredForAdd]
public virtual MyOtherEntity Other { get; set; }
RequiredForAdd 속성은 RequiredAttribute가 아닌 Attribute에서 상속되는 사용자 지정 속성입니다 . 기본 속성과 별도로 속성이나 메서드가 없습니다.
In my DB Context class I have a static constructor which finds all the properties with those attributes:
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)));
}
}
Now that we have a list of properties we need to check manually, we can override validation and manually validate them, adding any errors to the collection returned from the base validator:
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;
}
Note that I am only interested in validating for an Add; if you wanted to check during Modify as well, you would need to either do the force-load for the property or use a Sql command to check the foreign key value (shouldn't that already be somewhere in the context)?
Because the Required attribute has been removed, EF will create a nullable FK; to ensure you DB integrity you could alter the FKs manually in a Sql script that you run against your database after it has been created. This will at least catch the Modify with null issues.
Just had the same problem in EF 6.1.2. To solve this your class should be like the following:
public class Foo {
public int Id { get; set; }
public int Data { get; set; }
public int BarId { get; set; }
public virtual Bar Bar { get; set; }
}
As you can see, the "Required" attribute is not needed, because the Bar property is already required since the BarId property is not nullable.
So if you wanted the Bar property to be nullable, you would have to write:
public class Foo {
public int Id { get; set; }
public int Data { get; set; }
public int? BarId { get; set; }
public virtual Bar Bar { get; set; }
}
'Programing' 카테고리의 다른 글
knitr / Rmd : n 줄 / n 거리 이후 페이지 나누기 (0) | 2020.11.06 |
---|---|
DotNetOpenAuth ServiceProvider에서 PLAINTEXT 서명을 사용할 수 없습니다. (0) | 2020.11.06 |
큰 집합에서 해밍 거리가 낮은 이진 문자열을 효율적으로 찾습니다. (0) | 2020.11.05 |
TypeScript에서 jQuery 플러그인 사용 (0) | 2020.11.05 |
Android 명명 규칙 (0) | 2020.11.05 |