entity-framework entity-framework-4.1

entity framework - La relación no se pudo cambiar porque una o más de las propiedades de clave externa no admiten nulos



entity-framework entity-framework-4.1 (16)

Recibo este error cuando GetById () en una entidad y luego establezco la colección de entidades secundarias en mi nueva lista que proviene de la vista de MVC.

La operación falló: la relación no se pudo cambiar porque una o más de las propiedades de clave externa no admiten nulos. Cuando se realiza un cambio en una relación, la propiedad de clave foránea relacionada se establece en un valor nulo. Si la clave externa no admite valores nulos, se debe definir una nueva relación, se debe asignar a la propiedad de clave externa otro valor no nulo o se debe eliminar el objeto no relacionado.

No entiendo muy bien esta línea:

La relación no se pudo modificar porque una o más de las propiedades de clave externa no admiten nulos.

¿Por qué cambiaría la relación entre 2 entidades? Debería permanecer igual durante toda la vida de toda la aplicación.

El código en el que se produce la excepción es simple asignación de clases secundarias modificadas en una colección a la clase padre existente. Es de esperar que esto sirva para eliminar las clases de niños, agregar nuevas y modificaciones. Hubiera pensado que Entity Framework maneja esto.

Las líneas de código se pueden destilar a:

var thisParent = _repo.GetById(1); thisParent.ChildItems = modifiedParent.ChildItems(); _repo.Save();


¡No tengo idea de por qué las otras dos respuestas son tan populares!

Creo que tenía usted razón al suponer que el marco ORM debería manejarlo; después de todo, eso es lo que promete ofrecer. De lo contrario, su modelo de dominio se corrompe por problemas de persistencia. NHibernate lo gestiona felizmente si configura los ajustes de cascada correctamente. En Entity Framework también es posible, solo esperan que siga mejores estándares al configurar su modelo de base de datos, especialmente cuando tienen que inferir qué tipo de conexión se debe hacer en cascada:

Debe definir la relación padre - hijo correctamente utilizando una " relación de identificación ".

Si hace esto, Entity Framework sabe que el objeto hijo está identificado por el padre y, por lo tanto, debe ser una situación "en cascada-eliminar-huérfanos".

Aparte de lo anterior, es posible que deba (a partir de la experiencia de NHibernate)

thisParent.ChildItems.Clear(); thisParent.ChildItems.AddRange(modifiedParent.ChildItems);

en lugar de reemplazar la lista por completo.

ACTUALIZAR

El comentario de @Sumauma me recordó que las entidades separadas son otra parte del problema general. Para resolverlo, puede adoptar el enfoque de utilizar un archivador de modelo personalizado que construya sus modelos intentando cargarlo desde el contexto. Esta publicación de blog muestra un ejemplo de lo que quiero decir.


Acabo de tener el mismo error. Tengo dos tablas con una relación padre-hijo, pero configuré un "en cascada de eliminación" en la columna clave externa en la definición de tabla de la tabla hija. Por lo tanto, cuando elimine manualmente la fila principal (a través de SQL) en la base de datos, eliminará automáticamente las filas secundarias.

Sin embargo, esto no funcionó en EF, apareció el error descrito en este hilo. La razón de esto fue que en mi modelo de datos de entidad (archivo edmx) las propiedades de la asociación entre el padre y la tabla secundaria no eran correctas. La opción End1 OnDelete se configuró para ser none ("End1" en mi modelo es el final que tiene una multiplicidad de 1).

Cambié manualmente la opción End1 OnDelete a Cascade y de lo que funcionó. No sé por qué EF no puede entender esto cuando actualizo el modelo de la base de datos (tengo un primer modelo de base de datos).

Para completar, así es como se ve mi código para eliminar:

public void Delete(int id) { MyType myObject = _context.MyTypes.Find(id); _context.MyTypes.Remove(myObject); _context.SaveChanges(); }

Si no hubiera definido la eliminación de cascada, tendría que eliminar las filas secundarias manualmente antes de eliminar la fila principal.


Debe borrar manualmente la colección ChildItems y agregar nuevos elementos en ella:

thisParent.ChildItems.Clear(); thisParent.ChildItems.AddRange(modifiedParent.ChildItems);

Después de eso puede llamar al método de extensión DeleteOrphans que manejará con entidades huérfanas (debe llamarse entre los métodos DetectChanges y SaveChanges).

public static class DbContextExtensions { private static readonly ConcurrentDictionary< EntityType, ReadOnlyDictionary< string, NavigationProperty>> s_navPropMappings = new ConcurrentDictionary< EntityType, ReadOnlyDictionary< string, NavigationProperty>>(); public static void DeleteOrphans( this DbContext source ) { var context = ((IObjectContextAdapter)source).ObjectContext; foreach (var entry in context.ObjectStateManager.GetObjectStateEntries(EntityState.Modified)) { var entityType = entry.EntitySet.ElementType as EntityType; if (entityType == null) continue; var navPropMap = s_navPropMappings.GetOrAdd(entityType, CreateNavigationPropertyMap); var props = entry.GetModifiedProperties().ToArray(); foreach (var prop in props) { NavigationProperty navProp; if (!navPropMap.TryGetValue(prop, out navProp)) continue; var related = entry.RelationshipManager.GetRelatedEnd(navProp.RelationshipType.FullName, navProp.ToEndMember.Name); var enumerator = related.GetEnumerator(); if (enumerator.MoveNext() && enumerator.Current != null) continue; entry.Delete(); break; } } } private static ReadOnlyDictionary<string, NavigationProperty> CreateNavigationPropertyMap( EntityType type ) { var result = type.NavigationProperties .Where(v => v.FromEndMember.RelationshipMultiplicity == RelationshipMultiplicity.Many) .Where(v => v.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One || (v.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.ZeroOrOne && v.FromEndMember.GetEntityType() == v.ToEndMember.GetEntityType())) .Select(v => new { NavigationProperty = v, DependentProperties = v.GetDependentProperties().Take(2).ToArray() }) .Where(v => v.DependentProperties.Length == 1) .ToDictionary(v => v.DependentProperties[0].Name, v => v.NavigationProperty); return new ReadOnlyDictionary<string, NavigationProperty>(result); } }


Debe eliminar los elementos secundarios antiguos de thisParent.ChildItems uno por uno de forma manual. Entity Framework no hace eso por usted. Finalmente, no puede decidir qué desea hacer con los elementos del elemento anterior, si desea desecharlos o si desea conservarlos y asignarlos a otras entidades principales. Debe decirle a Entity Framework su decisión. Pero una de estas dos decisiones TIENE que tomarse ya que las entidades secundarias no pueden vivir solas sin una referencia a ningún padre en la base de datos (debido a la restricción de clave externa). Eso es básicamente lo que dice la excepción.

Editar

Qué haría si los elementos secundarios pudieran agregarse, actualizarse y eliminarse:

public void UpdateEntity(ParentItem parent) { // Load original parent including the child item collection var originalParent = _dbContext.ParentItems .Where(p => p.ID == parent.ID) .Include(p => p.ChildItems) .SingleOrDefault(); // We assume that the parent is still in the DB and don''t check for null // Update scalar properties of parent, // can be omitted if we don''t expect changes of the scalar properties var parentEntry = _dbContext.Entry(originalParent); parentEntry.CurrentValues.SetValues(parent); foreach (var childItem in parent.ChildItems) { var originalChildItem = originalParent.ChildItems .Where(c => c.ID == childItem.ID && c.ID != 0) .SingleOrDefault(); // Is original child item with same ID in DB? if (originalChildItem != null) { // Yes -> Update scalar properties of child item var childEntry = _dbContext.Entry(originalChildItem); childEntry.CurrentValues.SetValues(childItem); } else { // No -> It''s a new child item -> Insert childItem.ID = 0; originalParent.ChildItems.Add(childItem); } } // Don''t consider the child items we have just added above. // (We need to make a copy of the list by using .ToList() because // _dbContext.ChildItems.Remove in this loop does not only delete // from the context but also from the child collection. Without making // the copy we would modify the collection we are just interating // through - which is forbidden and would lead to an exception.) foreach (var originalChildItem in originalParent.ChildItems.Where(c => c.ID != 0).ToList()) { // Are there child items in the DB which are NOT in the // new child item collection anymore? if (!parent.ChildItems.Any(c => c.ID == originalChildItem.ID)) // Yes -> It''s a deleted child item -> Delete _dbContext.ChildItems.Remove(originalChildItem); } _dbContext.SaveChanges(); }

Nota: Esto no está probado. Se asume que la colección de elementos secundarios es del tipo ICollection . (Normalmente tengo IList y luego el código se ve un poco diferente). También eliminé todas las abstracciones del repositorio para hacerlo simple.

No sé si esa es una buena solución, pero creo que se debe hacer algún tipo de trabajo duro en esta línea para encargarse de todo tipo de cambios en la colección de navegación. También me gustaría ver una manera más fácil de hacerlo.


Encontré this respuesta mucho más útil para el mismo error. Parece que a EF no le gusta cuando lo quitas, prefiere Eliminar.

Puede eliminar una colección de registros adjunta a un registro como este.

order.OrderDetails.ToList().ForEach(s => db.Entry(s).State = EntityState.Deleted);

En el ejemplo, todos los registros de Detalle adjuntos a un Pedido tienen su Estado configurado para Eliminar. (En preparación para agregar detalles actualizados, como parte de una actualización de pedido)


Este es un gran problema. Lo que sucede realmente en tu código es esto:

  • Usted carga Parent de la base de datos y obtiene una entidad adjunta
  • Reemplace su colección de niños con una nueva colección de niños separados
  • Guarda los cambios pero durante esta operación todos los niños se consideran agregados porque EF no los conocía hasta este momento. Por lo tanto, EF intenta establecer nulo en clave externa de los hijos mayores e inserta todos los hijos nuevos => filas duplicadas.

Ahora la solución realmente depende de lo que quiere hacer y cómo le gustaría hacerlo?

Si está utilizando ASP.NET MVC, puede intentar usar UpdateModel o TryUpdateModel .

Si solo desea actualizar los elementos secundarios existentes de forma manual, simplemente puede hacer algo como:

foreach (var child in modifiedParent.ChildItems) { context.Childs.Attach(child); context.Entry(child).State = EntityState.Modified; } context.SaveChanges();

No es necesario adjuntar (establecer el estado en Modified también adjuntará la entidad) pero me gusta porque hace que el proceso sea más obvio.

Si desea modificar existentes, eliminar existentes e insertar nuevos hijos, debe hacer algo como:

var parent = context.Parents.GetById(1); // Make sure that childs are loaded as well foreach(var child in modifiedParent.ChildItems) { var attachedChild = FindChild(parent, child.Id); if (attachedChild != null) { // Existing child - apply new values context.Entry(attachedChild).CurrentValues.SetValues(child); } else { // New child // Don''t insert original object. It will attach whole detached graph parent.ChildItems.Add(child.Clone()); } } // Now you must delete all entities present in parent.ChildItems but missing // in modifiedParent.ChildItems // ToList should make copy of the collection because we can''t modify collection // iterated by foreach foreach(var child in parent.ChildItems.ToList()) { var detachedChild = FindChild(modifiedParent, child.Id); if (detachedChild == null) { parent.ChildItems.Remove(child); context.Childs.Remove(child); } } context.SaveChanges();


Este problema surge porque tratamos de eliminar la tabla padre. Todavía hay datos de la tabla hija presentes. Resolvemos el problema con la ayuda de Cascade Delete.

En el modelo, crea el método en la clase dbcontext.

modelBuilder.Entity<Job>() .HasMany<JobSportsMapping>(C => C.JobSportsMappings) .WithRequired(C => C.Job) .HasForeignKey(C => C.JobId).WillCascadeOnDelete(true); modelBuilder.Entity<Sport>() .HasMany<JobSportsMapping>(C => C.JobSportsMappings) .WithRequired(C => C.Sport) .HasForeignKey(C => C.SportId).WillCascadeOnDelete(true);

Después de eso, en nuestra llamada API

var JobList = Context.Job .Include(x => x.JobSportsMappings) .ToList(); Context.Job.RemoveRange(JobList); Context.SaveChanges();

La opción de eliminación en cascada elimina el elemento principal así como la tabla secundaria relacionada con los padres con este código simple. Hazlo probar de esta manera simple.

Eliminar intervalo que se utiliza para eliminar la lista de registros en la base de datos Gracias


Este tipo de solución hizo el truco para mí:

Parent original = db.Parent.SingleOrDefault<Parent>(t => t.ID == updated.ID); db.Childs.RemoveRange(original.Childs); updated.Childs.ToList().ForEach(c => original.Childs.Add(c)); db.Entry<Parent>(original).CurrentValues.SetValues(updated);

Es importante decir que esto borra todos los registros y los inserta nuevamente. Pero para mi caso (menos de 10) está bien.

Espero que ayude.


Esto sucede porque la entidad secundaria está marcada como modificada en lugar de eliminada.

Y la modificación que EF hace a la entidad Child cuando parent.Remove(child) se ejecuta, simplemente está configurando la referencia a su padre como null .

Puede comprobar el EntityState del niño escribiendo el siguiente código en la ventana Inmediato de Visual Studio cuando se produce la excepción, después de ejecutar SaveChanges() :

_context.ObjectStateManager.GetObjectStateEntries(System.Data.EntityState.Modified).ElementAt(X).Entity

donde X debe ser reemplazado por la entidad eliminada.

Si no tiene acceso a ObjectContext para ejecutar _context.ChildEntity.Remove(child) , puede resolver este problema haciendo que la clave foránea sea parte de la clave principal en la tabla secundaria.

Parent ________________ | PK IdParent | | Name | |________________| Child ________________ | PK IdChild | | PK,FK IdParent | | Name | |________________|

De esta forma, si ejecuta parent.Remove(child) , EF marcará correctamente la entidad como eliminada.


He encontrado este problema antes de varias horas y lo intento todo, pero en mi caso la solución fue diferente a la lista arriba.

Si usa una entidad ya recuperada de la base de datos e intenta modificarla, se producirá el error, pero si obtiene una copia nueva de la entidad de la base de datos, no debería haber ningún problema. No use esto:

public void CheckUsersCount(CompanyProduct companyProduct) { companyProduct.Name = "Test"; }

Utilizar esta:

public void CheckUsersCount(Guid companyProductId) { CompanyProduct companyProduct = CompanyProductManager.Get(companyProductId); companyProduct.Name = "Test"; }


La razón por la que enfrenta esto se debe a la diferencia entre composición y agregación .

En composición, el objeto hijo se crea cuando se crea el padre y se destruye cuando se destruye su padre . Entonces su vida está controlada por su padre. Por ejemplo, una publicación de blog y sus comentarios. Si se elimina una publicación, sus comentarios deben eliminarse. No tiene sentido tener comentarios para una publicación que no existe. Lo mismo para pedidos y artículos de pedido.

En agregación, el objeto hijo puede existir independientemente de su padre . Si el elemento primario se destruye, el objeto secundario aún puede existir, ya que puede agregarse a un elemento primario diferente más adelante. por ejemplo: la relación entre una lista de reproducción y las canciones en esa lista de reproducción. Si se elimina la lista de reproducción, las canciones no se deben eliminar. Se pueden agregar a una lista de reproducción diferente.

La forma en que Entity Framework diferencia las relaciones de agregación y composición es la siguiente:

  • Para la composición: espera que el objeto secundario tenga una clave primaria compuesta (ParentID, ChildID). Esto es por diseño ya que los ID de los niños deben estar dentro del alcance de sus padres.

  • Para la agregación: espera que la propiedad clave externa en el objeto secundario sea nulable.

Por lo tanto, la razón por la que tiene este problema es por la forma en que ha establecido su clave principal en su tabla secundaria. Debe ser compuesto, pero no lo es. Por lo tanto, Entity Framework ve esta asociación como una agregación, lo que significa que cuando elimine o borre los objetos secundarios, no eliminará los registros secundarios. Simplemente eliminará la asociación y establecerá la columna de la clave externa correspondiente en NULL (para que luego esos registros secundarios puedan asociarse con un elemento primario diferente). Como su columna no permite NULL, obtiene la excepción que mencionó.

Soluciones:

1- Si tiene una razón importante para no querer usar una clave compuesta, necesita eliminar los objetos secundarios explícitamente. Y esto puede hacerse de forma más simple que las soluciones sugeridas anteriormente:

context.Children.RemoveRange(parent.Children);

2- De lo contrario, al establecer la clave principal adecuada en su tabla secundaria, su código se verá más significativo:

parent.Children.Clear();


Me encontré con este problema hoy y quería compartir mi solución. En mi caso, la solución fue eliminar los elementos secundarios antes de obtener el elemento principal de la base de datos.

Previamente lo estaba haciendo como en el siguiente código. Luego obtendré el mismo error en esta pregunta.

var Parent = GetParent(parentId); var children = Parent.Children; foreach (var c in children ) { Context.Children.Remove(c); } Context.SaveChanges();

Lo que funcionó para mí, es obtener los elementos de los niños primero, usando parentId (clave externa) y luego eliminar esos elementos. Luego puedo obtener Parent de la base de datos y, en ese momento, ya no debería tener elementos secundarios y puedo agregar elementos nuevos para niños.

var children = GetChildren(parentId); foreach (var c in children ) { Context.Children.Remove(c); } Context.SaveChanges(); var Parent = GetParent(parentId); Parent.Children = //assign new entities/items here


Probé estas soluciones y muchas otras, pero ninguna de ellas funcionó. Dado que esta es la primera respuesta en Google, agregaré mi solución aquí.

El método que funcionó bien para mí fue quitar las relaciones de la imagen durante los commits, por lo que no había nada para que EF arruinara. Lo hice volviendo a encontrar el objeto padre en el DBContext y eliminándolo. Dado que las propiedades de navegación del objeto recuperado son todas nulas, las relaciones de los niños se ignoran durante la confirmación.

var toDelete = db.Parents.Find(parentObject.ID); db.Parents.Remove(toDelete); db.SaveChanges();

Tenga en cuenta que esto supone que las claves externas están configuradas con ON DELETE CASCADE, por lo que cuando se elimina la fila principal, la base de datos limpiará los elementos secundarios.


Si está utilizando AutoMapper con Entity Framework en la misma clase, puede solucionar este problema. Por ejemplo, si tu clase es

class A { public ClassB ClassB { get; set; } public int ClassBId { get; set; } } AutoMapper.Map<A, A>(input, destination);

Esto intentará copiar ambas propiedades. En este caso, ClassBId no es Nullable. Dado que AutoMapper copiará destination.ClassB = input.ClassB; esto causará un problema.

Configure su AutoMapper para Ignorar la propiedad de ClassB .

cfg.CreateMap<A, A>() .ForMember(m => m.ClassB, opt => opt.Ignore()); // We use the ClassBId


También resolví mi problema con la respuesta de Mosh y pensé que la respuesta de PeterB era un poco ya que usaba una enumeración como clave foránea. Recuerde que deberá agregar una nueva migración después de agregar este código.

También puedo recomendar esta publicación de blog para otras soluciones:

http://www.kianryan.co.uk/2013/03/orphaned-child/

Código:

public class Child { [Key, Column(Order = 0), DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } public string Heading { get; set; } //Add other properties here. [Key, Column(Order = 1)] public int ParentId { get; set; } public virtual Parent Parent { get; set; } }


Usé la solución de Mosh , pero no fue obvio para mí cómo implementar la clave de composición correctamente en el código primero.

Así que aquí está la solución:

public class Holiday { [Key, Column(Order = 0), DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int HolidayId { get; set; } [Key, Column(Order = 1), ForeignKey("Location")] public LocationEnum LocationId { get; set; } public virtual Location Location { get; set; } public DateTime Date { get; set; } public string Name { get; set; } }