¿Cómo evito que Entity Framework intente guardar / insertar objetos secundarios?

100

Cuando guardo una entidad con el marco de la entidad, naturalmente asumí que solo intentaría guardar la entidad especificada. Sin embargo, también está intentando salvar las entidades secundarias de esa entidad. Esto está provocando todo tipo de problemas de integridad. ¿Cómo fuerzo a EF a guardar solo la entidad que quiero guardar y, por lo tanto, ignorar todos los objetos secundarios?

Si configuro manualmente las propiedades en nulo, aparece un error "La operación falló: la relación no se pudo cambiar porque una o más de las propiedades de clave externa no aceptan nulos". Esto es extremadamente contraproducente ya que configuré el objeto secundario en nulo específicamente para que EF lo dejara en paz.

¿Por qué no quiero guardar / insertar los objetos secundarios?

Dado que esto se está discutiendo de un lado a otro en los comentarios, daré una justificación de por qué quiero que mis objetos secundarios se dejen tranquilos.

En la aplicación que estoy creando, el modelo de objetos EF no se carga desde la base de datos, sino que se usa como objetos de datos que estoy completando mientras analizo un archivo plano. En el caso de los objetos secundarios, muchos de estos se refieren a tablas de búsqueda que definen varias propiedades de la tabla principal. Por ejemplo, la ubicación geográfica de la entidad principal.

Dado que yo mismo llené estos objetos, EF asume que estos son objetos nuevos y deben insertarse junto con el objeto principal. Sin embargo, estas definiciones ya existen y no quiero crear duplicados en la base de datos. Solo uso el objeto EF para realizar una búsqueda y completar la clave externa en la entidad de mi tabla principal.

Incluso con los objetos secundarios que son datos reales, primero necesito guardar al padre y obtener una clave principal o EF parece hacer un lío de cosas. Espero que esto dé alguna explicación.

Mark Micallef
fuente
Hasta donde yo sé, tendrá que anular los objetos secundarios.
Johan
Hola Johan. No funciona. Lanza errores si nulo la colección. Dependiendo de cómo lo haga, se queja de que las claves son nulas o que se ha modificado la colección. Obviamente, esas cosas son ciertas, pero lo hice a propósito para que dejara en paz los objetos que se supone que no debe tocar.
Mark Micallef
Eufórico, eso es completamente inútil.
Mark Micallef
@Euphoric Incluso cuando no cambia los objetos secundarios, EF todavía intenta insertarlos de forma predeterminada y no ignorarlos ni actualizarlos.
Johan
Lo que realmente me molesta es que si hago todo lo posible para anular esos objetos, entonces se queja en lugar de darse cuenta de que quiero que los deje en paz. Dado que esos objetos secundarios son todos opcionales (anulables en la base de datos), ¿hay alguna forma de obligar a EF a olvidar que tenía esos objetos? es decir, purgar su contexto o caché de alguna manera?
Mark Micallef

Respuestas:

55

Hasta donde yo sé, tienes dos opciones.

Opción 1)

Anule todos los objetos secundarios, esto asegurará que EF no agregue nada. Tampoco eliminará nada de su base de datos.

Opcion 2)

Configure los objetos secundarios como separados del contexto usando el siguiente código

 context.Entry(yourObject).State = EntityState.Detached

Tenga en cuenta que no puede separar un List/ Collection. Tendrá que recorrer su lista y separar cada elemento de su lista como tal

foreach (var item in properties)
{
     db.Entry(item).State = EntityState.Detached;
}
Johan
fuente
Hola Johan, intenté separar una de las colecciones y arrojó el siguiente error: El tipo de entidad HashSet`1 no es parte del modelo para el contexto actual.
Mark Micallef
@MarkyMark No separe la colección. Tendrá que recorrer la colección y separarla objeto por objeto (actualizaré mi respuesta ahora).
Johan
11
Desafortunadamente, incluso con una lista de bucles para separar todo, EF parece que todavía intenta insertar en algunas de las tablas relacionadas. En este punto, estoy listo para extraer EF y volver a SQL, que al menos se comporta de manera sensata. Que dolor.
Mark Micallef
2
¿Puedo usar context.Entry (yourObject) .State incluso antes de agregarlo?
Thomas Klammer
1
@ mirind4 No usé el estado. Si inserto un objeto, me aseguro de que todos los niños sean nulos. En la actualización, obtengo el objeto primero sin los niños.
Thomas Klammer
41

En pocas palabras: use una clave externa y le salvará el día.

Suponga que tiene una entidad de escuela y una entidad de ciudad , y esta es una relación de muchos a uno en la que una ciudad tiene muchas escuelas y una escuela pertenece a una ciudad. Y suponga que las ciudades ya existen en la tabla de búsqueda, por lo que NO desea que se vuelvan a insertar al insertar una nueva escuela.

Inicialmente, podría definir sus entidades como esta:

public class City
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class School
{
    public int Id { get; set; }
    public string Name { get; set; }

    [Required]
    public City City { get; set; }
}

Y puede hacer la inserción de la escuela de esta manera (suponga que ya tiene la propiedad de la ciudad asignada al nuevo elemento ):

public School Insert(School newItem)
{
    using (var context = new DatabaseContext())
    {
        context.Set<School>().Add(newItem);
        // use the following statement so that City won't be inserted
        context.Entry(newItem.City).State = EntityState.Unchanged;
        context.SaveChanges();
        return newItem;
    }
}

El enfoque anterior puede funcionar perfectamente en este caso, sin embargo, prefiero el enfoque de clave externa , que para mí es más claro y flexible. Vea la solución actualizada a continuación:

public class City
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class School
{
    public int Id { get; set; }
    public string Name { get; set; }

    [ForeignKey("City_Id")]
    public City City { get; set; }

    [Required]
    public int City_Id { get; set; }
}

De esta manera, define explícitamente que la escuela tiene una clave externa City_Id y se refiere a la entidad City . Entonces, cuando se trata de la inserción de School , puede hacer:

    public School Insert(School newItem, int cityId)
    {
        if(cityId <= 0)
        {
            throw new Exception("City ID no provided");
        }

        newItem.City = null;
        newItem.City_Id = cityId;

        using (var context = new DatabaseContext())
        {
            context.Set<School>().Add(newItem);
            context.SaveChanges();
            return newItem;
        }
    }

En este caso, especificas explícitamente el City_Id del nuevo registro y eliminas el City del gráfico para que EF no se moleste en agregarlo al contexto junto con School .

Aunque a la primera impresión el enfoque de clave externa parece más complicado, créame, esta mentalidad le ahorrará mucho tiempo cuando se trata de insertar una relación de varios a varios (imaginando que tiene una relación entre la escuela y el estudiante, y el estudiante tiene una propiedad de la ciudad) y así sucesivamente.

Espero que esto te sea útil.

lenglei
fuente
Gran respuesta, me ayudó mucho! Pero, ¿no sería mejor indicar el valor mínimo de 1 para City_Id usando el [Range(1, int.MaxValue)]atributo?
Dan Rayson
¡¡Como un sueño!! ¡Un millón de gracias!
CJH
Esto eliminaría el valor del objeto School en el llamador. ¿SaveChanges recarga el valor de las propiedades de navegación nulas? De lo contrario, la persona que llama debería volver a cargar el objeto City después de llamar al método Insert () si necesita esa información. Este es el patrón que he usado a menudo, pero todavía estoy abierto a mejores patrones si alguien tiene uno bueno.
Desequilibrado
1
Esto fue muy útil para mí. EntityState.Unchaged es exactamente lo que necesitaba para asignar un objeto que representa una clave externa de tabla de búsqueda en un gráfico de objeto grande que estaba guardando como una sola transacción. EF Core arroja un mensaje de error menos que intuitivo, en mi opinión. Guardé en caché mis tablas de búsqueda que rara vez cambian por motivos de rendimiento. Su suposición es que, debido a que el PK del objeto de la tabla de búsqueda es el mismo que el que está en la base de datos, ya sabe que no ha cambiado y simplemente asigna el FK al elemento existente. En lugar de intentar insertar el objeto de la tabla de búsqueda como nuevo.
tnk479
Ninguna combinación de anular o establecer el estado sin cambios me funciona. EF insiste en que necesita insertar un nuevo registro secundario cuando solo quiero actualizar un campo escalar en el padre.
BobRz
21

Si solo desea almacenar cambios en un objeto principal y evitar almacenar cambios en cualquiera de sus objetos secundarios , entonces ¿por qué no hacer lo siguiente?

using (var ctx = new MyContext())
{
    ctx.Parents.Attach(parent);
    ctx.Entry(parent).State = EntityState.Added;  // or EntityState.Modified
    ctx.SaveChanges();
}

La primera línea adjunta el objeto principal y todo el gráfico de sus objetos secundarios dependientes al contexto en Unchangedestado.

La segunda línea cambia el estado del objeto padre únicamente, dejando a sus hijos en el Unchangedestado.

Tenga en cuenta que utilizo un contexto recién creado, por lo que esto evita guardar otros cambios en la base de datos.

keykey76
fuente
2
Esta respuesta tiene la ventaja de que si alguien llega más tarde y agrega un objeto secundario, no romperá el código existente. Esta es una solución de "participación" en la que las demás requieren que excluya explícitamente los objetos secundarios.
Jim
Esto ya no funciona en el núcleo. "Adjuntar: adjunta todas las entidades accesibles, excepto cuando una entidad accesible tiene una clave generada en la tienda y no se asigna ningún valor de clave; estos se marcarán como agregados". Si los niños son nuevos también, se agregarán.
mmix
14

Una de las soluciones sugeridas es asignar la propiedad de navegación desde el mismo contexto de base de datos. En esta solución, se reemplazaría la propiedad de navegación asignada desde fuera del contexto de la base de datos. Por favor, vea el siguiente ejemplo como ilustración.

class Company{
    public int Id{get;set;}
    public Virtual Department department{get; set;}
}
class Department{
    public int Id{get; set;}
    public String Name{get; set;}
}

Guardando en la base de datos:

 Company company = new Company();
 company.department = new Department(){Id = 45}; 
 //an Department object with Id = 45 exists in database.    

 using(CompanyContext db = new CompanyContext()){
      Department department = db.Departments.Find(company.department.Id);
      company.department = department;
      db.Companies.Add(company);
      db.SaveChanges();
  }

Microsoft incluye esto como una característica, sin embargo, me parece molesto. Si el objeto del departamento asociado con el objeto de la empresa tiene una identificación que ya existe en la base de datos, entonces ¿por qué EF no asocia el objeto de la empresa con el objeto de la base de datos? ¿Por qué deberíamos cuidar de la asociación por nosotros mismos? Cuidar la propiedad de navegación durante la adición de un nuevo objeto es algo así como mover las operaciones de la base de datos de SQL a C #, algo engorroso para los desarrolladores.

Bikash Bishwokarma
fuente
Estoy 100% de acuerdo en que es ridículo que EF intente crear un nuevo registro cuando se proporciona la identificación. Si hubiera una manera de llenar las opciones de la lista de selección con el objeto real, estaríamos en el negocio.
T3.0
10

Primero debe saber que hay dos formas de actualizar la entidad en EF.

  • Objetos adjuntos

Cuando cambia la relación de los objetos adjuntos al contexto del objeto mediante uno de los métodos descritos anteriormente, Entity Framework debe mantener sincronizadas las claves externas, las referencias y las colecciones.

  • Objetos desconectados

Si está trabajando con objetos desconectados, debe administrar manualmente la sincronización.

En la aplicación que estoy creando, el modelo de objetos EF no se carga desde la base de datos, sino que se usa como objetos de datos que estoy completando mientras analizo un archivo plano.

Eso significa que está trabajando con un objeto desconectado, pero no está claro si está utilizando una asociación independiente o una asociación de clave externa .

  • Añadir

    Al agregar una nueva entidad con un objeto secundario existente (objeto que existe en la base de datos), si EF no rastrea el objeto secundario, el objeto secundario se volverá a insertar. A menos que primero adjunte manualmente el objeto secundario.

      db.Entity(entity.ChildObject).State = EntityState.Modified;
      db.Entity(entity).State = EntityState.Added;
  • Actualizar

    Puede simplemente marcar la entidad como modificada, luego se actualizarán todas las propiedades escalares y las propiedades de navegación simplemente se ignorarán.

      db.Entity(entity).State = EntityState.Modified;

Diferencia de gráfico

Si desea simplificar el código cuando trabaja con un objeto desconectado, puede intentar graficar la biblioteca de diferencias .

Aquí está la introducción, Presentación de GraphDiff para Entity Framework Code First: permitir actualizaciones automáticas de un gráfico de entidades separadas .

Código de muestra

  • Inserte la entidad si no existe; de ​​lo contrario, actualice.

      db.UpdateGraph(entity);
  • Inserte la entidad si no existe, de lo contrario actualice E inserte el objeto secundario si no existe, de lo contrario actualice.

      db.UpdateGraph(entity, map => map.OwnedEntity(x => x.ChildObject));
Yuliam Chandra
fuente
3

La mejor forma de hacerlo es anulando la función SaveChanges en su contexto de datos.

    public override int SaveChanges()
    {
        var added = this.ChangeTracker.Entries().Where(e => e.State == System.Data.EntityState.Added);

        // Do your thing, like changing the state to detached
        return base.SaveChanges();
    }
Wouter Schut
fuente
2

Tengo el mismo problema cuando intento guardar el perfil, ya tengo el saludo de mesa y soy nuevo para crear el perfil. Cuando inserto el perfil, también lo inserto en el saludo. Así que intenté así antes de savechanges ().

db.Entry (Profile.Salutation) .State = EntityState.Unchanged;

ZweHtatNaing
fuente
1

Esto funcionó para mí:

// temporarily 'detach' the child entity/collection to have EF not attempting to handle them
var temp = entity.ChildCollection;
entity.ChildCollection = new HashSet<collectionType>();

.... do other stuff

context.SaveChanges();

entity.ChildCollection = temp;
Klaus
fuente
0

Lo que hemos hecho es antes de agregar el padre al dbset, desconectar las colecciones secundarias del padre, asegurándonos de enviar las colecciones existentes a otras variables para permitir trabajar con ellas más adelante y luego reemplazar las colecciones secundarias actuales con nuevas colecciones vacías. Establecer las colecciones secundarias en nulo / nada pareció fallar para nosotros. Después de hacer eso, agregue el padre al archivo dbset. De esta forma, los niños no se agregan hasta que usted lo desee.

Shaggie
fuente
0

Sé que es una publicación antigua, sin embargo, si está utilizando el enfoque de código primero, puede lograr el resultado deseado utilizando el siguiente código en su archivo de mapeo.

Ignore(parentObject => parentObject.ChildObjectOrCollection);

Esto básicamente le dirá a EF que excluya la propiedad "ChildObjectOrCollection" del modelo para que no se asigne a la base de datos.

Syed danés
fuente
¿En qué contexto se usa "Ignorar"? No parece existir en el supuesto contexto.
T3.0
0

Tuve un desafío similar al usar Entity Framework Core 3.1.0, mi lógica de repositorio es bastante genérica.

Esto funcionó para mí:

builder.Entity<ChildEntity>().HasOne(c => c.ParentEntity).WithMany(l =>
       l.ChildEntity).HasForeignKey("ParentEntityId");

Tenga en cuenta que "ParentEntityId" es el nombre de la columna de clave externa en la entidad secundaria. Agregué la línea de código mencionada anteriormente en este método:

protected override void OnModelCreating(ModelBuilder builder)...
Manelisi Nodada
fuente