Entity Framework 5 Actualización de un registro

870

He estado explorando diferentes métodos para editar / actualizar un registro dentro de Entity Framework 5 en un entorno ASP.NET MVC3, pero hasta ahora ninguno de ellos cumple todos los cuadros que necesito. Te explicaré por qué.

He encontrado tres métodos con los cuales mencionaré los pros y los contras:

Método 1 - Cargue el registro original, actualice cada propiedad

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();
}    

Pros

  • Puede especificar qué propiedades cambian
  • Las vistas no necesitan contener todas las propiedades

Contras

  • 2 x consultas en la base de datos para cargar el original y luego actualizarlo

Método 2: cargar el registro original, establecer valores modificados

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

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

Pros

  • Solo las propiedades modificadas se envían a la base de datos.

Contras

  • Las vistas deben contener todas las propiedades
  • 2 x consultas en la base de datos para cargar el original y luego actualizarlo

Método 3: adjunte el registro actualizado y establezca el estado en EntityState.Modified

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

Pros

  • 1 x consulta en la base de datos para actualizar

Contras

  • No se puede especificar qué propiedades cambian
  • Las vistas deben contener todas las propiedades

Pregunta

Mi pregunta para ustedes chicos; ¿Hay alguna manera limpia de lograr este conjunto de objetivos?

  • Puede especificar qué propiedades cambian
  • Las vistas no necesitan contener todas las propiedades (como la contraseña)
  • 1 x consulta en la base de datos para actualizar

Entiendo que esto es algo bastante menor para señalar, pero es posible que me falte una solución simple para esto. Si no, el método uno prevalecerá ;-)

Stokedout
fuente
13
¿Usar ViewModels y un buen motor de mapeo? Solo obtiene "propiedades para actualizar" para completar su vista (y luego para actualizar). Todavía habrá las 2 consultas para actualizar (obtener original + actualizarlo), pero no llamaría a esto una "estafa". Si ese es tu único problema de rendimiento, eres un hombre feliz;)
Raphaël Althaus
Gracias @ RaphaëlAlthaus, punto muy válido. Podría hacer esto, pero tengo que crear una operación CRUD para varias tablas, así que estoy buscando un método que pueda trabajar directamente con el modelo para ahorrarme la creación de ViewModel n-1 para cada modelo.
Stokedout
3
Bueno, en mi proyecto actual (muchas entidades también) comenzamos a trabajar en Modelos, pensando que perderíamos tiempo trabajando con ViewModels. Ahora vamos a ViewModels, y con el trabajo de infraestructura (no despreciable) al inicio, ahora es mucho, mucho, más claro y más fácil de mantener. Y más seguro (no hay que temer por "campos ocultos" maliciosos o cosas así)
Raphaël Althaus
1
Y no más (horribles) ViewBags para llenar sus DropDownLists (tenemos al menos una DropDownList en casi todas nuestras vistas de CRU (D) ...)
Raphaël Althaus
Creo que tienes razón, es malo por tratar de pasar por alto ViewModels. Sí, ViewBag parece un poco sucio a veces. Por lo general, voy un paso más allá según el blog de Dino Esposito y también creo InputModels, un poco de cinturón y tirantes, pero funciona bastante bien. Solo significa 2 modelos adicionales por modelo - doh ;-)
Stokedout

Respuestas:

681

Estás buscando:

db.Users.Attach(updatedUser);
var entry = db.Entry(updatedUser);
entry.Property(e => e.Email).IsModified = true;
// other changed properties
db.SaveChanges();
Ladislav Mrnka
fuente
59
hola @Ladislav Mrnka, si quiero actualizar todas las propiedades a la vez, ¿puedo usar el siguiente código? db.Departments.Attach (departamento); db.Entry (departamento) .State = EntityState.Modified; db.SaveChanges ();
Foyzul Karim
23
@Foysal: Sí, puedes.
Ladislav Mrnka
55
Uno de los problemas con este enfoque es que no puede burlarse de db.Entry (), que es una PITA grave. EF tiene una historia de burla razonablemente buena en otros lugares: es bastante molesto que (hasta donde puedo decir) no tengan una aquí.
Ken Smith
23
@Foysal Doing context.Entry (entidad) .State = EntityState.Modified solo es suficiente, no es necesario hacer el archivo adjunto. Se adjuntará automáticamente como modificado ...
HelloWorld
44
@ Sandman4, eso significa que todas las demás propiedades deben estar allí y configurarse con el valor actual. En algunos diseños de aplicaciones, esto no es factible.
Dan Esparza
176

Realmente me gusta la respuesta aceptada. Creo que hay otra forma de abordar esto también. Supongamos que tiene una lista muy corta de propiedades que no desea incluir en una Vista, por lo que al actualizar la entidad, se omitirán. Digamos que esos dos campos son Contraseña y 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 ejemplo le permite esencialmente dejar en paz su lógica empresarial después de agregar un nuevo campo a su tabla Usuarios y a su Vista.

smd
fuente
Aún así, recibiré un error si no especifico un valor para la propiedad SSN, aunque configuré IsModified como falso, todavía valida la propiedad contra las reglas del modelo. Entonces, si la propiedad está marcada como NOT NULL, fallará si no configuro ningún valor diferente de nulo.
RolandoCC
No recibirá un error porque esos campos no estarán en su formulario. Usted deja de lado los campos que definitivamente no actualizará, toma la entrada de la base de datos utilizando el formulario pasado adjuntándolo y le dice a la entrada que esos campos no se están modificando. La validación del modelo se controla en ModelState, no en el contexto. Este ejemplo hace referencia a un usuario existente, por lo tanto, "updatedUser". Si su SSN es un campo obligatorio, habría estado allí cuando se creó por primera vez.
smd
44
Si entiendo correctamente, "updatedUser" es una instancia de un objeto ya poblado con FirstOrDefault () o similar, por lo que estoy actualizando solo las propiedades que cambié y estableciendo otras en ISModified = false. Esto funciona bien Pero, lo que estoy tratando de hacer es actualizar un objeto sin llenarlo primero, sin hacer ningún FirstOrDefault () antes de la actualización. Esto es cuando recibo un error si no especifico un valor para todos los campos requeridos, incluso si configuro ISModified = false en esas propiedades. entry.Property (e => e.columnA) .IsModified = false; Sin esta línea, la Columna A fallará.
RolandoCC
Lo que estás describiendo es crear una nueva entidad. Esto se aplica solo a la actualización.
smd
1
RolandoCC, ponga db.Configuration.ValidateOnSaveEnabled = false; antes de 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
fuente
Esto parece una buena solución, sin complicaciones ni problemas; no tiene que especificar manualmente las propiedades y tiene en cuenta todas las viñetas de OP: ¿hay alguna razón para que esto no tenga más votos?
nocarrier
Aunque no lo hace. Tiene uno de los mayores "inconvenientes", más de un hit en la base de datos. Aún tendría que cargar el original con esta respuesta.
smd
1
@smd ¿por qué dices que llega a la base de datos más de una vez? No veo que eso suceda a menos que usar SetValues ​​() tenga ese efecto, pero eso no parece ser cierto.
Parlamento
@parlamento Creo que debo haber estado dormido cuando escribí eso. Disculpas El problema real es anular un valor nulo previsto. Si el usuario actualizado ya no tiene referencia a algo, no sería correcto reemplazarlo con el valor original si desea borrarlo.
smd
22

He agregado un método de actualización adicional en mi clase base de repositorio que es similar al método de actualización generado por Scaffolding. En lugar de configurar todo el objeto como "modificado", establece un conjunto de propiedades individuales. (T es un parámetro genérico de clase).

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;
    }
}

Y luego llamar, por ejemplo:

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();
}

Me gusta un viaje a la base de datos. Sin embargo, probablemente sea mejor hacerlo con los modelos de vista para evitar repetir conjuntos de propiedades. Todavía no lo he hecho porque no sé cómo evitar llevar los mensajes de validación de mis validadores de modelos de vista a mi proyecto de dominio.

Ian Warburton
fuente
Ajá ... proyecto separado para modelos de vista y proyecto separado para repositorios que funcionan con modelos de vista.
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
fuente
¿Por qué no solo DbContext.Attach (obj); DbContext.Entry (obj) .State = EntityState.Modified;
nelsontruran
Esto controla la setparte de la declaración de actualización.
Tanveer Badar
4

Solo para agregar a la lista de opciones. También puede tomar el objeto de la base de datos y usar una herramienta de mapeo automático como Auto Mapper para actualizar las partes del registro que desea cambiar.

Bostwick
fuente
3

Dependiendo de su caso de uso, se aplican todas las soluciones anteriores. Sin embargo, así es como lo hago habitualmente:

Para el código del lado del servidor (por ejemplo, un proceso por lotes), normalmente cargo las entidades y trabajo con proxys dinámicos. Por lo general, en los procesos por lotes, debe cargar los datos de todos modos en el momento en que se ejecuta el servicio. Intento cargar por lotes los datos en lugar de usar el método find para ahorrar algo de tiempo. Dependiendo del proceso, uso control de concurrencia optimista o pesimista (siempre uso optimista, excepto para escenarios de ejecución paralelos en los que necesito bloquear algunos registros con declaraciones SQL simples, aunque esto es raro). Dependiendo del código y el escenario, el impacto puede reducirse a casi cero.

Para escenarios del lado del cliente, tiene algunas opciones

  1. Usar modelos de vista. Los modelos deben tener una propiedad UpdateStatus (no modificado-insertado-actualizado-eliminado). Es responsabilidad del cliente establecer el valor correcto para esta columna en función de las acciones del usuario (insertar-actualizar-eliminar). El servidor puede consultar a la base de datos los valores originales o el cliente debe enviar los valores originales al servidor junto con las filas modificadas. El servidor debe adjuntar los valores originales y usar la columna UpdateStatus para cada fila para decidir cómo manejar los nuevos valores. En este escenario siempre uso simultaneidad optimista. Esto solo hará las instrucciones de inserción - actualización - eliminación y no ninguna selección, pero puede necesitar un código inteligente para recorrer el gráfico y actualizar las entidades (depende de su escenario - aplicación). Un mapeador puede ayudar pero no maneja la lógica CRUD

  2. Use una biblioteca como breeze.js que oculte la mayor parte de esta complejidad (como se describe en 1) e intente ajustarla a su caso de uso.

Espero eso ayude

Chriss
fuente