Entity Framework 5 Atualizando um registro

870

Eu tenho explorado diferentes métodos de edição / atualização de um registro no Entity Framework 5 em um ambiente ASP.NET MVC3, mas até agora nenhum deles marque todas as caixas necessárias. Eu vou explicar o porquê.

Encontrei três métodos aos quais mencionarei os prós e os contras:

Método 1 - Carregar registro original, atualizar cada propriedade

var original = db.Users.Find(updatedUser.UserId);

if (original != null)
{
    original.BusinessEntityId = updatedUser.BusinessEntityId;
    original.Email = updatedUser.Email;
    original.EmployeeId = updatedUser.EmployeeId;
    original.Forename = updatedUser.Forename;
    original.Surname = updatedUser.Surname;
    original.Telephone = updatedUser.Telephone;
    original.Title = updatedUser.Title;
    original.Fax = updatedUser.Fax;
    original.ASPNetUserId = updatedUser.ASPNetUserId;
    db.SaveChanges();
}    

Prós

  • Pode especificar quais propriedades são alteradas
  • As visualizações não precisam conter todas as propriedades

Contras

  • 2 x consultas no banco de dados para carregar o original e atualizá-lo

Método 2 - Carregar registro original, definir valores alterados

var original = db.Users.Find(updatedUser.UserId);

if (original != null)
{
    db.Entry(original).CurrentValues.SetValues(updatedUser);
    db.SaveChanges();
}

Prós

  • Somente propriedades modificadas são enviadas ao banco de dados

Contras

  • As visualizações precisam conter todas as propriedades
  • 2 x consultas no banco de dados para carregar o original e atualizá-lo

Método 3 - Anexe o registro atualizado e defina o estado como EntityState.Modified

db.Users.Attach(updatedUser);
db.Entry(updatedUser).State = EntityState.Modified;
db.SaveChanges();

Prós

  • 1 x consulta no banco de dados para atualizar

Contras

  • Não é possível especificar quais propriedades são alteradas
  • As visualizações devem conter todas as propriedades

Questão

Minha pergunta para vocês; existe uma maneira limpa de atingir esse conjunto de metas?

  • Pode especificar quais propriedades são alteradas
  • As visualizações não precisam conter todas as propriedades (como senha!)
  • 1 x consulta no banco de dados para atualizar

Entendo que isso é uma coisa muito pequena a ser destacada, mas posso estar perdendo uma solução simples para isso. Caso contrário, prevalecerá o método ;-)

Stokedout
fonte
13
Use o ViewModels e um bom mecanismo de mapeamento? Você obtém apenas "propriedades para atualizar" para preencher sua visualização (e depois atualizar). Ainda haverá as 2 consultas para atualização (obtenha o original + atualize), mas eu não chamaria isso de "Con". Se esse é o seu único problema desempenho, você é um homem feliz;)
Raphaël Althaus
Obrigado @ RaphaëlAlthaus, ponto muito válido. Eu poderia fazer isso, mas tenho que criar uma operação CRUD para várias tabelas, então estou procurando um método que possa trabalhar diretamente com o modelo para me salvar da criação de n-1 ViewModel para cada modelo.
Stokedout
3
Bem, no meu projeto atual (muitas entidades também), começamos a trabalhar em modelos, pensando que perderíamos tempo trabalhando com o ViewModels. Agora vamos para o ViewModels e, com o trabalho de infraestrutura (não desprezível) no início, é muito, muito, muito mais claro e fácil de manter agora. E mais seguro (não há necessidade de temer sobre maliciosos "campos ocultos" ou coisas assim)
Raphaël Althaus
1
E não mais (terríveis) ViewBags para preencher seus DropDownLists (nós temos pelo menos um DropDownList em quase todos os nossos) vistas CRU (D ...)
Raphaël Althaus
Eu acho que você está certo, meu mal por tentar ignorar ViewModels. Sim, o ViewBag às vezes parece um pouco sujo. Eu geralmente dou um passo além, de acordo com o blog de Dino Esposito, e crio InputModels também, um cinto pequeno e aparelhos, mas funciona muito bem. Significa apenas 2 modelos extras por modelos - doh ;-)
Stokedout

Respostas:

681

Você está procurando:

db.Users.Attach(updatedUser);
var entry = db.Entry(updatedUser);
entry.Property(e => e.Email).IsModified = true;
// other changed properties
db.SaveChanges();
Ladislav Mrnka
fonte
59
oi @Ladislav Mrnka, se eu quiser atualizar todas as propriedades de uma só vez, posso usar o código abaixo? db.Departments.Attach (departamento); db.Entry (departamento) .State = EntityState.Modified; db.SaveChanges ();
Foyzul Karim
23
@Foysal: Sim, você pode.
precisa saber é o seguinte
5
Um dos problemas dessa abordagem é que você não pode zombar do db.Entry (), que é uma PITA séria. A EF tem uma história de zombaria razoavelmente boa em outros lugares - é muito irritante que (até onde eu saiba) eles não tenham uma aqui.
Ken Smith
23
@Foysal Doing context.Entry (entity) .State = EntityState.Modified sozinho é suficiente, não é necessário fazer a conexão. Ele será automaticamente anexado como modificado ...
HelloWorld
4
@ Sandman4, isso significa que todas as outras propriedades precisam estar lá e ser definidas com o valor atual. Em alguns designs de aplicativos, isso não é viável.
Dan Esparza 26/03
176

Eu realmente gosto da resposta aceita. Eu acredito que há ainda outra maneira de abordar isso também. Digamos que você tenha uma lista muito curta de propriedades que você nunca desejaria incluir em uma Visualização, portanto, ao atualizar a entidade, elas seriam omitidas. Digamos que esses dois campos sejam Senha e SSN.

db.Users.Attach(updatedUser);

var entry = db.Entry(updatedUser);
entry.State = EntityState.Modified;

entry.Property(e => e.Password).IsModified = false;
entry.Property(e => e.SSN).IsModified = false;   

db.SaveChanges();   

Este exemplo permite que você essencialmente deixe sua lógica de negócios em paz após adicionar um novo campo à sua tabela Usuários e à sua Visualização.

smd
fonte
Ainda assim, receberei um erro se não especificar um valor para a propriedade SSN, mesmo que eu defina IsModified como false, ele ainda validará a propriedade com base nas regras do modelo. Portanto, se a propriedade estiver marcada como NOT NULL, ela falhará se eu não definir nenhum valor diferente de null.
RolandoCC
Você não receberá um erro porque esses campos não estarão no seu formulário. Você deixa de fora os campos que definitivamente não estará atualizando, pega a entrada do banco de dados usando o formulário retornado anexando-o e informa à entrada que esses campos não estão sendo modificados. A validação do modelo é controlada no ModelState, não no contexto. Este exemplo está referenciando um usuário existente, portanto, "updatedUser". Se o seu SSN for um campo obrigatório, ele estaria lá quando foi criado.
smd
4
Se bem entendi, "updatedUser" é uma instância de um objeto já preenchido com FirstOrDefault () ou similar, portanto, estou atualizando apenas as propriedades que alterei e definindo outras como ISModified = false. Isso funciona bem. Mas, o que estou tentando fazer é atualizar um objeto sem preenchê-lo primeiro, sem fazer nenhum FirstOrDefault () antes da atualização. É quando recebo um erro se não especificar um valor para todos os campos solicitados, mesmo que eu defina ISModified = false nessas propriedades. entry.Property (e => e.columnA) .IsModified = false; Sem essa linha, a ColumnA falhará.
RolandoCC
O que você está descrevendo é criar uma nova entidade. Isso se aplica apenas à atualização.
quer
1
RolandoCC, coloque db.Configuration.ValidateOnSaveEnabled = false; antes do db.SaveChanges ();
Wilky #
28
foreach(PropertyInfo propertyInfo in original.GetType().GetProperties()) {
    if (propertyInfo.GetValue(updatedUser, null) == null)
        propertyInfo.SetValue(updatedUser, propertyInfo.GetValue(original, null), null);
}
db.Entry(original).CurrentValues.SetValues(updatedUser);
db.SaveChanges();
Stefano Camisassi
fonte
Parece uma solução muito boa - sem confusão ou confusão; você não precisa especificar propriedades manualmente e isso leva em consideração todos os marcadores dos POs - existe algum motivo para isso não ter mais votos?
Nocarrier 10/08
Isso não acontece. Ele tem um dos maiores "contras", mais de um hit no banco de dados. Você ainda teria que carregar o original com esta resposta.
smd 29/08
1
@smd Por que você diz que atinge o banco de dados mais de uma vez? Não vejo isso acontecendo, a menos que o uso de SetValues ​​() tenha esse efeito, mas isso não parece ser verdade.
parlamento
@ parlamentar Acho que devo ter dormido quando escrevi isso. Desculpas. O problema real está substituindo um valor nulo pretendido. Se o usuário atualizado não tiver mais referência a algo, não seria correto substituí-lo pelo valor original se você pretendesse limpá-lo.
SMD
22

Eu adicionei um método de atualização extra à minha classe base do repositório que é semelhante ao método de atualização gerado pelo Scaffolding. Em vez de definir o objeto inteiro como "modificado", ele define um conjunto de propriedades individuais. (T é um parâmetro genérico de classe.)

public void Update(T obj, params Expression<Func<T, object>>[] propertiesToUpdate)
{
    Context.Set<T>().Attach(obj);

    foreach (var p in propertiesToUpdate)
    {
        Context.Entry(obj).Property(p).IsModified = true;
    }
}

E depois ligar, por exemplo:

public void UpdatePasswordAndEmail(long userId, string password, string email)
{
    var user = new User {UserId = userId, Password = password, Email = email};

    Update(user, u => u.Password, u => u.Email);

    Save();
}

Eu gosto de uma viagem ao banco de dados. Provavelmente é melhor fazer isso com os modelos de visualização, para evitar repetir conjuntos de propriedades. Ainda não fiz isso porque não sei como evitar trazer as mensagens de validação dos validadores de modelo de exibição para o meu projeto de domínio.

Ian Warburton
fonte
Aha ... projeto separado para modelos de exibição e projeto separado para repositórios que funcionam com modelos de exibição.
Ian Warburton
11
public interface IRepository
{
    void Update<T>(T obj, params Expression<Func<T, object>>[] propertiesToUpdate) where T : class;
}

public class Repository : DbContext, IRepository
{
    public void Update<T>(T obj, params Expression<Func<T, object>>[] propertiesToUpdate) where T : class
    {
        Set<T>().Attach(obj);
        propertiesToUpdate.ToList().ForEach(p => Entry(obj).Property(p).IsModified = true);
        SaveChanges();
    }
}
Matthew Steven Monkan
fonte
Por que não apenas DbContext.Attach (obj); DbContext.Entry (obj) .State = EntityState.Modified;
Nelsontruran
Isso controla a setparte da instrução de atualização.
Tanveer Badar
4

Apenas para adicionar à lista de opções. Você também pode pegar o objeto no banco de dados e usar uma ferramenta de mapeamento automático como o Mapeador Automático para atualizar as partes do registro que deseja alterar.

Bostwick
fonte
3

Dependendo do seu caso de uso, todas as soluções acima se aplicam. Isto é como eu costumo fazê-lo no entanto:

Para código do lado do servidor (por exemplo, um processo em lote), normalmente carrego as entidades e trabalho com proxies dinâmicos. Geralmente, em processos em lote, você precisa carregar os dados de qualquer maneira no momento em que o serviço é executado. Tento carregar em lote os dados em vez de usar o método find para economizar tempo. Dependendo do processo, eu uso controle de simultaneidade otimista ou pessimista (eu sempre uso otimista, exceto em cenários de execução paralela em que preciso bloquear alguns registros com instruções sql simples, isso é raro). Dependendo do código e do cenário, o impacto pode ser reduzido para quase zero.

Para cenários do lado do cliente, você tem algumas opções

  1. Use modelos de vista. Os modelos devem ter uma propriedade UpdateStatus (não modificado-inserido-atualizado-excluído). É de responsabilidade do cliente definir o valor correto para esta coluna, dependendo das ações do usuário (inserir-atualizar-excluir). O servidor pode consultar os valores originais no banco de dados ou o cliente deve enviar os valores originais ao servidor junto com as linhas alteradas. O servidor deve anexar os valores originais e usar a coluna UpdateStatus para cada linha para decidir como lidar com os novos valores. Nesse cenário, eu sempre uso simultaneidade otimista. Isso fará apenas as instruções insert - update - delete e não nenhuma seleção, mas pode ser necessário algum código inteligente para percorrer o gráfico e atualizar as entidades (depende do seu cenário - aplicativo). Um mapeador pode ajudar, mas não lida com a lógica CRUD

  2. Use uma biblioteca como breeze.js que oculte grande parte dessa complexidade (conforme descrito em 1) e tente ajustá-la ao seu caso de uso.

Espero que ajude

Chriss
fonte