Entity Framework 5 Mise à jour d'un enregistrement

870

J'ai exploré différentes méthodes de modification / mise à jour d'un enregistrement dans Entity Framework 5 dans un environnement ASP.NET MVC3, mais jusqu'à présent, aucune d'entre elles ne coche toutes les cases dont j'ai besoin. Je vais vous expliquer pourquoi.

J'ai trouvé trois méthodes auxquelles je mentionnerai les avantages et les inconvénients:

Méthode 1 - Charger l'enregistrement d'origine, mettre à jour chaque propriété

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

Avantages

  • Peut spécifier quelles propriétés changent
  • Les vues n'ont pas besoin de contenir toutes les propriétés

Les inconvénients

  • 2 x requêtes sur la base de données pour charger l'original puis le mettre à jour

Méthode 2 - Charger l'enregistrement d'origine, définir les valeurs modifiées

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

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

Avantages

  • Seules les propriétés modifiées sont envoyées à la base de données

Les inconvénients

  • Les vues doivent contenir chaque propriété
  • 2 x requêtes sur la base de données pour charger l'original puis le mettre à jour

Méthode 3 - Attachez l'enregistrement mis à jour et définissez l'état à EntityState.Modified

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

Avantages

  • 1 x requête sur la base de données à mettre à jour

Les inconvénients

  • Impossible de spécifier quelles propriétés changent
  • Les vues doivent contenir chaque propriété

Question

Ma question à vous les gars; existe-t-il un moyen propre de réaliser cet ensemble d'objectifs?

  • Peut spécifier quelles propriétés changent
  • Les vues n'ont pas besoin de contenir toutes les propriétés (comme le mot de passe!)
  • 1 x requête sur la base de données à mettre à jour

Je comprends que c'est une chose assez mineure à souligner, mais je manque peut-être une solution simple à cela. Sinon, la méthode prévaudra ;-)

Stokedout
la source
13
Utilisez ViewModels et un bon moteur de cartographie? Vous obtenez uniquement des "propriétés à mettre à jour" pour remplir votre vue (puis pour mettre à jour). Il y aura toujours les 2 requêtes pour la mise à jour (obtenir l'original + le mettre à jour), mais je n'appellerais pas cela un "Con". Si c'est votre seul problème de performance, vous êtes un homme heureux;)
Raphaël Althaus
Merci @ RaphaëlAlthaus, point très valable. Je pourrais le faire, mais je dois créer une opération CRUD pour un certain nombre de tables, donc je cherche une méthode qui peut fonctionner directement avec le modèle pour me sauver en créant n-1 ViewModel pour chaque modèle.
Stokedout
3
Eh bien, dans mon projet actuel (de nombreuses entités aussi), nous avons commencé à travailler sur des modèles, pensant que nous perdrions du temps à travailler avec ViewModels. Nous allons maintenant voir ViewModels, et avec des travaux d'infrastructure (non négligeables) au début, c'est de loin, beaucoup, beaucoup plus clair et plus facile à entretenir maintenant. Et plus sûr (pas besoin de craindre les "champs cachés" malveillants ou des choses comme ça)
Raphaël Althaus
1
Et plus de (horribles) ViewBags pour remplir vos DropDownLists (nous avons au moins une DropDownList sur presque toutes nos vues CRU (D) ...)
Raphaël Althaus
Je pense que vous avez raison, ma mauvaise pour avoir essayé de négliger ViewModels. Oui, ViewBag semble parfois un peu sale. Je vais généralement un peu plus loin selon le blog de Dino Esposito et crée également des InputModels, une ceinture un peu et des bretelles, mais cela fonctionne assez bien.
Signifie

Réponses:

681

Tu recherches:

db.Users.Attach(updatedUser);
var entry = db.Entry(updatedUser);
entry.Property(e => e.Email).IsModified = true;
// other changed properties
db.SaveChanges();
Ladislav Mrnka
la source
59
salut @Ladislav Mrnka, si je veux mettre à jour toutes les propriétés en même temps, puis-je utiliser le code ci-dessous? db.Departments.Attach (département); db.Entry (department) .State = EntityState.Modified; db.SaveChanges ();
Foyzul Karim
23
@Foysal: Oui, vous le pouvez.
Ladislav Mrnka
5
L'un des problèmes de cette approche est que vous ne pouvez pas vous moquer de db.Entry (), qui est un PITA sérieux. EF a une histoire moqueuse assez bonne ailleurs - c'est assez ennuyeux (pour autant que je sache) qu'ils n'en ont pas ici.
Ken Smith
23
@Foysal Doing context.Entry (entity) .State = EntityState.Modified seul ne suffit pas de faire l'attachement. Il sera automatiquement attaché tel que modifié ...
HelloWorld
4
@ Sandman4, cela signifie que toutes les autres propriétés doivent être présentes et définies sur la valeur actuelle. Dans certains modèles d'applications, cela n'est pas possible.
Dan Esparza
176

J'aime vraiment la réponse acceptée. Je pense qu'il y a encore une autre façon d'aborder cela. Supposons que vous ayez une très courte liste de propriétés que vous ne voudriez jamais inclure dans une vue, donc lors de la mise à jour de l'entité, celles-ci seront omises. Disons que ces deux champs sont Mot de passe et 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();   

Cet exemple vous permet essentiellement de laisser votre logique métier seule après avoir ajouté un nouveau champ à votre table Utilisateurs et à votre vue.

smd
la source
Je recevrai toujours une erreur si je ne spécifie pas de valeur pour la propriété SSN, même si j'ai défini IsModified sur false, il valide toujours la propriété par rapport aux règles du modèle. Donc, si la propriété est marquée comme NON NUL, elle échouera si je ne définis aucune valeur différente de null.
RolandoCC
Vous ne recevrez pas d'erreur car ces champs ne figureront pas dans votre formulaire. Vous omettez les champs que vous ne mettrez certainement pas à jour, récupérez l'entrée de la base de données en utilisant le formulaire renvoyé en le joignant et dites à l'entrée que ces champs ne sont pas en cours de modification. La validation du modèle est contrôlée dans le ModelState, pas dans le contexte. Cet exemple fait référence à un utilisateur existant, d'où «updatedUser». Si votre SSN est un champ obligatoire, il l'aurait été lors de sa création.
smd
4
Si je comprends bien, "updatedUser" est une instance d'un objet déjà rempli avec un FirstOrDefault () ou similaire, donc je ne mets à jour que les propriétés que j'ai modifiées et en définissant d'autres sur ISModified = false. Cela fonctionne bien. Mais, ce que j'essaie de faire, c'est de mettre à jour un objet sans le remplir d'abord, sans faire de FirstOrDefault () avant la mise à jour. C'est lorsque je reçois une erreur si je ne spécifie pas de valeur pour tous les champs requis, même si j'ai défini ISModified = false sur ces propriétés. entry.Property (e => e.columnA) .IsModified = false; Sans cette ligne, ColumnA échouera.
RolandoCC
Ce que vous décrivez, c'est la création d'une nouvelle entité. Cela s'applique uniquement à la mise à jour.
smd
1
RolandoCC, mettez db.Configuration.ValidateOnSaveEnabled = false; avant le 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
la source
Cela semble être une très bonne solution - pas de bruit ni de bruit; vous n'avez pas à spécifier manuellement les propriétés et cela prend en compte toutes les puces OP - y a-t-il une raison pour laquelle cela n'a pas plus de votes?
nocarrier
Mais ce n'est pas le cas. Il possède l'un des plus grands «inconvénients», plus d'un accès à la base de données. Vous auriez encore à charger l'original avec cette réponse.
smd
1
@smd pourquoi dites-vous qu'il frappe la base de données plus d'une fois? Je ne vois pas cela se produire à moins que l'utilisation de SetValues ​​() n'ait cet effet, mais cela ne semble pas être vrai.
parlement
@parlement Je pense que je devais être endormi quand j'ai écrit ça. Mes excuses. Le problème réel remplace une valeur nulle voulue. Si l'utilisateur mis à jour n'a plus de référence à quelque chose, il ne serait pas judicieux de le remplacer par la valeur d'origine si vous vouliez l'effacer.
smd
22

J'ai ajouté une méthode de mise à jour supplémentaire à ma classe de base de référentiel qui est similaire à la méthode de mise à jour générée par Scaffolding. Au lieu de définir l'objet entier sur "modifié", il définit un ensemble de propriétés individuelles. (T est un paramètre générique 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;
    }
}

Et puis appeler, par exemple:

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

J'aime un voyage dans la base de données. Il est probablement préférable de le faire avec des modèles de vue, afin d'éviter de répéter des ensembles de propriétés. Je ne l'ai pas encore fait car je ne sais pas comment éviter d'introduire les messages de validation sur mes validateurs de modèle de vue dans mon projet de domaine.

Ian Warburton
la source
Aha ... projet séparé pour les modèles de vue et projet séparé pour les référentiels qui fonctionnent avec les modèles de vue.
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
la source
Pourquoi pas simplement DbContext.Attach (obj); DbContext.Entry (obj) .State = EntityState.Modified;
nelsontruran
Cela contrôle la setpartie de l'instruction de mise à jour.
Tanveer Badar
4

Juste pour ajouter à la liste des options. Vous pouvez également récupérer l'objet dans la base de données et utiliser un outil de mappage automatique comme Auto Mapper pour mettre à jour les parties de l'enregistrement que vous souhaitez modifier.

Bostwick
la source
3

Selon votre cas d'utilisation, toutes les solutions ci-dessus s'appliquent. Voici comment je le fais habituellement cependant:

Pour le code côté serveur (par exemple un processus par lots), je charge généralement les entités et travaille avec des proxys dynamiques. Habituellement, dans les processus par lots, vous devez de toute façon charger les données au moment où le service s'exécute. J'essaie de charger les données par lots au lieu d'utiliser la méthode find pour gagner du temps. Selon le processus, j'utilise un contrôle de concurrence optimiste ou pessimiste (j'utilise toujours optimiste, sauf pour les scénarios d'exécution parallèle où j'ai besoin de verrouiller certains enregistrements avec des instructions SQL simples, cela est cependant rare). Selon le code et le scénario, l'impact peut être réduit à presque zéro.

Pour les scénarios côté client, vous avez quelques options

  1. Utilisez des modèles de vue. Les modèles doivent avoir une propriété UpdateStatus (non modifié-inséré-mis à jour-supprimé). Il est de la responsabilité du client de définir la valeur correcte sur cette colonne en fonction des actions de l'utilisateur (insert-update-delete). Le serveur peut soit interroger la base de données pour les valeurs d'origine, soit le client doit envoyer les valeurs d'origine au serveur avec les lignes modifiées. Le serveur doit joindre les valeurs d'origine et utiliser la colonne UpdateStatus pour chaque ligne pour décider comment gérer les nouvelles valeurs. Dans ce scénario, j'utilise toujours une concurrence optimiste. Cela ne fera que les instructions d'insertion - mise à jour - suppression et pas de sélection, mais il faudra peut-être du code intelligent pour parcourir le graphique et mettre à jour les entités (cela dépend de votre scénario - application). Un mappeur peut aider mais ne gère pas la logique CRUD

  2. Utilisez une bibliothèque comme breeze.js qui cache la majeure partie de cette complexité (comme décrit en 1) et essayez de l'adapter à votre cas d'utilisation.

J'espère que cela aide

Chriss
la source