DbSet.Attach (entidad) vs DbContext.Entry (entidad) .State = EntityState.Modified

115

Cuando estoy en un escenario separado y obtengo un dto del cliente que mapeo en una entidad para guardarlo, hago esto:

context.Entry(entity).State = EntityState.Modified;
context.SaveChanges();

Porque ¿qué es entonces el DbSet.Attach(entity)

o ¿por qué debería usar el método .Attach cuando EntityState.Modified ya adjunta la entidad?

Elisabeth
fuente
Mejor agregue información de la versión, esto se ha preguntado antes. No tengo claro si esto merece una nueva pregunta.
Henk Holterman

Respuestas:

278

Cuando lo haces context.Entry(entity).State = EntityState.Modified;, no solo estás adjuntando la entidad al DbContext, también estás marcando a toda la entidad como sucia. Esto significa que cuando lo haga context.SaveChanges(), EF generará una declaración de actualización que actualizará todos los campos de la entidad.

Esto no siempre se desea.

Por otro lado, DbSet.Attach(entity)adjunta la entidad al contexto sin marcarlo como sucio. Es equivalente a hacercontext.Entry(entity).State = EntityState.Unchanged;

Al adjuntar de esta manera, a menos que proceda a actualizar una propiedad en la entidad, la próxima vez que llame context.SaveChanges() , EF no generará una actualización de la base de datos para esta entidad.

Incluso si planea realizar una actualización de una entidad, si la entidad tiene muchas propiedades (columnas de base de datos) pero solo desea actualizar algunas, puede que le resulte ventajoso hacer una DbSet.Attach(entity), y luego solo actualizar las pocas propiedades que necesitan una actualización. Hacerlo de esta manera generará una declaración de actualización más eficiente de EF. EF solo actualizará las propiedades que modificó (a diferencia decontext.Entry(entity).State = EntityState.Modified; que hará que se actualicen todas las propiedades / columnas)

Documentación relevante: Add / Attach y Entity States .

Ejemplo de código

Digamos que tienes la siguiente entidad:

public class Person
{
    public int Id { get; set; } // primary key
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Si su código se ve así:

context.Entry(personEntity).State = EntityState.Modified;
context.SaveChanges();

El SQL generado se verá así:

UPDATE person
SET FirstName = 'whatever first name is',
    LastName = 'whatever last name is'
WHERE Id = 123; -- whatever Id is.

Observe cómo la declaración de actualización anterior actualizará todas las columnas, independientemente de si realmente ha cambiado los valores o no.

Por el contrario, si su código utiliza el adjunto "normal" de esta manera:

context.People.Attach(personEntity); // State = Unchanged
personEntity.FirstName = "John"; // State = Modified, and only the FirstName property is dirty.
context.SaveChanges();

Entonces la declaración de actualización generada es diferente:

UPDATE person
SET FirstName = 'John'
WHERE Id = 123; -- whatever Id is.

Como puede ver, la declaración de actualización solo actualiza los valores que realmente se cambiaron después de que adjuntó la entidad al contexto. Dependiendo de la estructura de su mesa, esto puede tener un impacto positivo en el rendimiento.

Ahora bien, qué opción es mejor para usted depende completamente de lo que esté tratando de hacer.

sstan
fuente
1
EF no genera la cláusula WHERE de esta manera. Si adjuntó una entidad creada con new (es decir, nueva Entidad ()) y la configuró como modificada, debe configurar todos los campos originales debido al bloqueo optimista. La cláusula WHERE generada en la consulta UPDATE generalmente contiene todos los campos originales (no solo Id), por lo que si no lo hace, EF lanzará una excepción de concurrencia.
bubi
3
@budi: Gracias por sus comentarios. Volví a probar para estar seguro, y para una entidad básica, se comporta como describí, con la WHEREcláusula que contiene solo la clave principal y sin ninguna verificación de concurrencia. Para tener verificación de concurrencia, necesito configurar explícitamente una columna como un token de concurrencia o rowVersion. En ese caso, la WHEREcláusula solo tendrá la clave principal y la columna del token de simultaneidad, no todos los campos. Si sus pruebas muestran lo contrario, me encantaría saberlo.
sstan
¿Cómo puedo encontrar dinámicamente que se modifique la propiedad?
Navid_pdp11
2
@ Navid_pdp11 DbContext.Entry(person).CurrentValuesy DbContext.Entry(person).OriginalValues.
Shimmy Weitzhandler
puede estar un poco fuera del tema, pero si uso un patrón de repositorio, tengo que crear un repositorio para cada modelo, ya que cada modelo tiene alguna entidad que debe estar en un estado sin seguimiento al insertar un nuevo registro en db, por lo que no puedo tener un repositorio genérico que adjunta entidades al contexto durante la inserción. ¿Cómo manejas esto mejor?
jayasurya_j
3

Cuando usa el DbSet.Updatemétodo, Entity Framework marca todas las propiedades de su entidad como EntityState.Modified, por lo que las rastrea. Si desea cambiar solo algunas de sus propiedades, no todas, utilice DbSet.Attach. Este método crea todas sus propiedades EntityState.Unchanged, por lo que debe crear las propiedades que desea actualizar EntityState.Modified. Por lo tanto, cuando la aplicación DbContext.SaveChangesllegue, solo operará propiedades modificadas.

Orhun
fuente
0

Además (de la respuesta marcada) hay una diferencia importante entre context.Entry(entity).State = EntityState.Unchangedycontext.Attach(entity) (en EF Core):

Hice algunas pruebas para entenderlo más por mí mismo (por lo tanto, esto también incluye algunas pruebas de referencia general), así que este es mi escenario de prueba:

  • Usé EF Core 3.1.3
  • solía QueryTrackingBehavior.NoTracking
  • Usé solo atributos para el mapeo (ver más abajo)
  • Usé diferentes contextos para obtener el pedido y actualizarlo.
  • Limpié toda la base de datos para cada prueba

Estos son los modelos:

public class Order
{
    public int Id { get; set; }
    public string Comment { get; set; }
    public string ShippingAddress { get; set; }
    public DateTime? OrderDate { get; set; }
    public List<OrderPos> OrderPositions { get; set; }
    [ForeignKey("OrderedByUserId")]
    public User OrderedByUser { get; set; }
    public int? OrderedByUserId { get; set; }
}

public class OrderPos
{
    public int Id { get; set; }
    public string ArticleNo { get; set; }
    public int Quantity { get; set; }
    [ForeignKey("OrderId")]
    public Order Order { get; set; }
    public int? OrderId { get; set; }
}

public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Estos son los datos de prueba (originales) en la base de datos: ingrese la descripción de la imagen aquí

Para obtener el pedido:

order = db.Orders.Include(o => o.OrderPositions).Include(o => o.OrderedByUser).FirstOrDefault();

Ahora las pruebas:

Actualización simple con EntityState :

db.Entry(order).State = EntityState.Unchanged;
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be IGNORED
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be IGNORED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 2 Calls:
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555', 1, 5)
// UPDATE [Orders] SET [ShippingAddress] = 'Germany' WHERE [Id] = 1

Actualización simple con adjuntar :

db.Attach(order);
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be UPDATED
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be UPDATED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 1 Call:
// UPDATE [OrderPositions] SET [ArticleNo] = 'K-1234' WHERE [Id] = 1
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555 (NEW)', 1, 5)
// UPDATE [Orders] SET [ShippingAddress] = 'Germany' WHERE [Id] = 1
// UPDATE [Users] SET [FirstName] = 'William (CHANGED)' WHERE [Id] = 1

Actualización con el cambio de Child-Ids con EntityState :

db.Entry(order).State = EntityState.Unchanged;
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUser.Id = 3; // will be IGNORED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be IGNORED
order.OrderPositions[0].Id = 3; // will be IGNORED
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be IGNORED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 2 Calls:
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555', 1, 5)
// UPDATE [Orders] SET [ShippingAddress] = 'Germany' WHERE [Id] = 1

Actualización con cambio de ID de niños con Attach :

db.Attach(order);
order.ShippingAddress = "Germany"; // would be UPDATED
order.OrderedByUser.Id = 3; // will throw EXCEPTION
order.OrderedByUser.FirstName = "William (CHANGED)"; // would be UPDATED
order.OrderPositions[0].Id = 3; // will throw EXCEPTION
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // would be UPDATED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // would be INSERTED
db.SaveChanges();
// Throws Exception: The property 'Id' on entity type 'User' is part of a key and so cannot be modified or marked as modified. To change the principal of an existing entity with an identifying foreign key first delete the dependent and invoke 'SaveChanges' then associate the dependent with the new principal.)

Nota: Esto arroja una excepción, no importa si la identificación se cambió o se estableció en el valor original, parece que el estado de la identificación se establece en "cambiado" y esto no está permitido (porque es la clave principal)

Actualizar con el cambio de Child-Ids como nuevo (no hay diferencia entre EntityState y Attach):

db.Attach(order); // or db.Entry(order).State = EntityState.Unchanged;
order.OrderedByUser = new User();
order.OrderedByUser.Id = 3; // // Reference will be UPDATED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be UPDATED (on User 3)
db.SaveChanges();
// Will generate SQL in 2 Calls:
// UPDATE [Orders] SET [OrderedByUserId] = 3, [ShippingAddress] = 'Germany' WHERE [Id] = 1
// UPDATE [Users] SET [FirstName] = 'William (CHANGED)' WHERE [Id] = 3

Nota: Vea la diferencia con la actualización con EntityState sin nuevo (arriba). Esta vez, el nombre se actualizará debido a la nueva instancia de usuario.

Actualización con el cambio de Reference-Ids con EntityState :

db.Entry(order).State = EntityState.Unchanged;
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUserId = 3; // will be UPDATED
order.OrderedByUser.Id = 2; // will be IGNORED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be IGNORED
order.OrderPositions[0].Id = 3; // will be IGNORED
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be IGNORED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 2 Calls:
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555', 1, 5)
// UPDATE [Orders] SET [OrderedByUserId] = 3, [ShippingAddress] = 'Germany' WHERE [Id] = 1

Actualización con el cambio de los ID de referencia con Adjuntar :

db.Attach(order);
order.ShippingAddress = "Germany"; // will be UPDATED
order.OrderedByUserId = 3; // will be UPDATED
order.OrderedByUser.FirstName = "William (CHANGED)"; // will be UPDATED (on FIRST User!)
order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be UPDATED
order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED
db.SaveChanges();
// Will generate SQL in 1 Call:
// UPDATE [OrderPositions] SET [ArticleNo] = 'K-1234' WHERE [Id] = 1
// INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555 (NEW)', 1, 5)
// UPDATE [Orders] SET [OrderedByUserId] = 3, [ShippingAddress] = 'Germany' WHERE [Id] = 1
// UPDATE [Users] SET [FirstName] = 'William (CHANGED)' WHERE [Id] = 1

Nota: La referencia se cambiará a Usuario 3, pero también se actualizará el usuario 1, supongo que esto se debe a order.OrderedByUser.Idque no ha cambiado (sigue siendo 1).

Conclusión Con EntityState tiene más control, pero debe actualizar las subpropiedades (segundo nivel) usted mismo. Con Attach puedes actualizar todo (supongo que con todos los niveles de propiedades), pero tienes que estar atento a las referencias. Solo por ejemplo: si User (OrderedByUser) fuera un dropDown, cambiar el valor a través de un dropDown podría sobrescribir todo el objeto User. En este caso, el dropDown-Value original se sobrescribirá en lugar de la referencia.

Para mí, el mejor caso es establecer objetos como OrderedByUser en nulo y solo establecer el order.OrderedByUserId en el nuevo valor, si solo quiero cambiar la referencia (no importa si EntityState o Attach).

Espero que esto ayude, sé que es mucho texto: D

StewieG
fuente