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

115

Quando estou em um cenário desanexado e obtenho um dto do cliente que mapeio em uma entidade para salvá-lo, faço o seguinte:

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

Pois o que é então o DbSet.Attach(entity)

ou por que devo usar o método .Attach quando EntityState.Modified já anexa a entidade?

Elisabeth
fonte
Melhor adicionar algumas informações de versão, isso já foi perguntado antes. Não estou certo se isso merece uma nova pergunta.
Henk Holterman

Respostas:

278

Ao fazer isso context.Entry(entity).State = EntityState.Modified;, você não está apenas anexando a entidade ao DbContext, mas também marcando toda a entidade como suja. Isso significa que quando você fizer isso context.SaveChanges(), o EF gerará uma instrução de atualização que atualizará todos os campos da entidade.

Isso nem sempre é desejado.

Por outro lado, DbSet.Attach(entity)anexa a entidade ao contexto sem marcá-la como suja. É equivalente a fazercontext.Entry(entity).State = EntityState.Unchanged;

Ao anexar dessa forma, a menos que você prossiga para atualizar uma propriedade na entidade, na próxima vez que você chamar context.SaveChanges(), o EF não gerará uma atualização de banco de dados para esta entidade.

Mesmo se você estiver planejando fazer uma atualização em uma entidade, se a entidade tiver muitas propriedades (colunas de banco de dados), mas você quiser atualizar apenas algumas, pode achar vantajoso fazer uma DbSet.Attach(entity)e, em seguida, atualizar apenas algumas propriedades que precisam de atualização. Fazer isso dessa forma gerará uma instrução de atualização mais eficiente do EF. EF só atualizará as propriedades que você modificou (em contraste com o context.Entry(entity).State = EntityState.Modified;que fará com que todas as propriedades / colunas sejam atualizadas)

Documentação relevante: Adicionar / Anexar e Estados de Entidade .

Exemplo de código

Digamos que você tenha a seguinte entidade:

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

Se o seu código for assim:

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

O SQL gerado será semelhante a este:

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

Observe como a instrução de atualização acima atualizará todas as colunas, independentemente de você ter realmente alterado os valores ou não.

Em contraste, se seu código usa o anexo "normal" como este:

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

Então, a instrução de atualização gerada é diferente:

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

Como você pode ver, a instrução update atualiza apenas os valores que foram realmente alterados depois que você anexou a entidade ao contexto. Dependendo da estrutura da sua tabela, isso pode ter um impacto positivo no desempenho.

Agora, qual opção é melhor para você depende inteiramente do que você está tentando fazer.

Sstan
fonte
1
EF não gera a cláusula WHERE dessa forma. Se você anexou uma entidade criada com new (ie new Entity ()) e definiu como modificada, você deve definir todos os campos originais por causa do bloqueio otimista. A cláusula WHERE gerada na consulta UPDATE geralmente contém todos os campos originais (não apenas Id), portanto, se você não fizer isso, o EF lançará uma exceção de simultaneidade.
bubi
3
@budi: Obrigado por seu feedback. Testei novamente para ter certeza e, para uma entidade básica, ela se comporta como descrevi, com a WHEREcláusula contendo apenas a chave primária e sem nenhuma verificação de simultaneidade. Para ter verificação de simultaneidade, preciso configurar explicitamente uma coluna como um token de simultaneidade ou rowVersion. Nesse caso, a WHEREcláusula terá apenas a chave primária e a coluna do token de simultaneidade, não todos os campos. Se seus testes mostrarem o contrário, eu adoraria ouvir sobre isso.
stão
como posso encontrar dinamicamente a propriedade da bruxa modificada?
Navid_pdp11
2
@ Navid_pdp11 DbContext.Entry(person).CurrentValuese DbContext.Entry(person).OriginalValues.
Shimmy Weitzhandler
pode estar um pouco fora do assunto, mas se eu usar um padrão de repositório, tenho que criar um repositório para cada modelo, pois cada modelo tem alguma entidade que precisa estar em estado não rastreado ao inserir um novo registro no banco de dados, então não posso ter um repositório genérico que anexa entidades ao contexto durante a inserção. Como você lida melhor com isso?
jayasurya_j
3

Quando você usa o DbSet.Updatemétodo, o Entity Framework marca todas as propriedades de sua entidade como EntityState.Modified, portanto, as controla. Se você quiser alterar apenas algumas de suas propriedades, não todas, use DbSet.Attach. Este método cria todas as suas propriedades EntityState.Unchanged, portanto, você deve fazer as propriedades que deseja atualizar EntityState.Modified. Portanto, quando o aplicativo atinge o DbContext.SaveChanges, ele operará apenas as propriedades modificadas.

Orhun
fonte
0

Além disso (para a resposta marcada), há uma diferença importante entre context.Entry(entity).State = EntityState.Unchangede context.Attach(entity)(no EF Core):

Fiz alguns testes para entender melhor por mim mesmo (portanto, isso também inclui alguns testes de referência geral), então este é o meu cenário de teste:

  • Eu usei EF Core 3.1.3
  • eu usei QueryTrackingBehavior.NoTracking
  • Usei apenas atributos para mapeamento (veja abaixo)
  • Usei diferentes contextos para obter o pedido e atualizá-lo
  • Limpei todo o banco de dados para cada teste

Estes são os 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; }
}

Estes são os dados de teste (originais) no banco de dados: insira a descrição da imagem aqui

Para obter o pedido:

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

Agora os testes:

Atualização simples com 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

Atualização simples com anexo :

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

Atualização com a alteração de Child-Ids com 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

Atualização com a mudança de Child-Ids com Anexar :

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.)

Observação: isso gera exceção, não importa se o Id foi alterado ou foi definido com o valor original, parece que o estado do Id está definido como "alterado" e isso não é permitido (porque é a chave primária)

Atualize com a alteração de Child-Ids como novos (sem diferença entre EntityState e 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: Veja a diferença para Atualizar com EntityState sem novo (acima). Desta vez o Nome será atualizado, devido à nova instância do Usuário.

Atualize com a alteração dos Reference-Ids com 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

Atualização com a mudança da referência-Ids com Anexar :

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: A referência será alterada para o Usuário 3, mas também o usuário 1 será atualizado, acho que isso ocorre porque o order.OrderedByUser.Idestá inalterado (ainda é 1).

Conclusão Com EntityState você tem mais controle, mas precisa atualizar as subpropriedades (segundo nível) por conta própria. Com o Attach você pode atualizar tudo (acho que com todos os níveis de propriedades), mas você tem que ficar de olho nas referências. Apenas por exemplo: Se User (OrderedByUser) fosse um dropDown, alterar o valor por meio de dropDown poderia substituir todo o objeto User. Nesse caso, o dropDown-Value original seria sobrescrito em vez da referência.

Para mim, o melhor caso é definir objetos como OrderedByUser como nulo e definir apenas o order.OrderedByUserId para o novo valor, se eu quiser apenas alterar a referência (não importa se EntityState ou Attach).

Espero que ajude, sei que é muito texto: D

StewieG
fonte