Comment mettre à jour un seul champ à l'aide d'Entity Framework?


190

Voici le tableau

Utilisateurs

UserId
UserName
Password
EmailAddress

et le code ..

public void ChangePassword(int userId, string password){
//code to update the password..
}

26
Par Password, vous voulez dire un mot de passe haché, non? :-)
Edward Brey

Réponses:


370

La réponse de Ladislav a été mise à jour pour utiliser DbContext (introduit dans EF 4.1):

public void ChangePassword(int userId, string password)
{
  var user = new User() { Id = userId, Password = password };
  using (var db = new MyEfContextName())
  {
    db.Users.Attach(user);
    db.Entry(user).Property(x => x.Password).IsModified = true;
    db.SaveChanges();
  }
}

55
Je n'ai pu faire fonctionner ce code qu'en ajoutant db.Configuration.ValidateOnSaveEnabled = false; avant db.SaveChanges ()?
Jake Drew

3
Quel espace de noms inclure à utiliser db.Entry(user).Property(x => x.Password).IsModified = true;et nondb.Entry(user).Property("Password").IsModified = true;
Johan

5
Cette approche lève une exception OptimisticConcurencyException lorsque la table a un champ d'horodatage.
Maksim Vi.

9
Je pense qu'il vaut la peine de mentionner que si vous utilisez, db.Configuration.ValidateOnSaveEnabled = false;vous voudrez peut-être continuer à valider le champ que vous mettez à jour:if (db.Entry(user).Property(x => x.Password).GetValidationErrors().Count == 0)
Ziul

2
Vous devez définir ValidateOnSaveEnabled sur false si vous avez des champs obligatoires dans votre table que vous ne fournissez pas lors de votre mise à jour
Sal

54

Vous pouvez indiquer à EF quelles propriétés doivent être mises à jour de cette manière:

public void ChangePassword(int userId, string password)
{
  var user = new User { Id = userId, Password = password };
  using (var context = new ObjectContext(ConnectionString))
  {
    var users = context.CreateObjectSet<User>();
    users.Attach(user);
    context.ObjectStateManager.GetObjectStateEntry(user)
      .SetModifiedProperty("Password");
    context.SaveChanges();
  }
}

ObjectStateManager n'est pas disponible pour DBContext
LoxLox

17

Vous avez essentiellement deux options:

  • aller jusqu'au bout de l'EF, dans ce cas, vous
    • charger l'objet en fonction de ce qui est userIdfourni - l'objet entier est chargé
    • mettre à jour le passwordchamp
    • sauvegarder l'objet en utilisant la .SaveChanges()méthode du contexte

Dans ce cas, c'est à EF comment gérer cela en détail. Je viens de tester cela, et dans le cas où je ne change qu'un seul champ d'un objet, ce que EF crée est à peu près ce que vous créez manuellement aussi - quelque chose comme:

`UPDATE dbo.Users SET Password = @Password WHERE UserId = @UserId`

Ainsi, EF est suffisamment intelligent pour déterminer quelles colonnes ont effectivement changé, et il créera une instruction T-SQL pour gérer uniquement les mises à jour qui sont en fait nécessaires.

  • vous définissez une procédure stockée qui fait exactement ce dont vous avez besoin, en code T-SQL (mettez simplement à jour la Passwordcolonne pour le donné UserIdet rien d'autre - s'exécute essentiellement UPDATE dbo.Users SET Password = @Password WHERE UserId = @UserId) et vous créez une importation de fonction pour cette procédure stockée dans votre modèle EF et vous appelez ceci fonction au lieu de suivre les étapes décrites ci-dessus

1
@ marc-s En fait, vous n'avez pas à charger l'objet entier!
Arvand

14

Dans Entity Framework Core, Attachrenvoie l'entrée, donc tout ce dont vous avez besoin est:

var user = new User { Id = userId, Password = password };
db.Users.Attach(user).Property(x => x.Password).IsModified = true;
db.SaveChanges();

12

j'utilise ceci:

entité:

public class Thing 
{
    [Key]
    public int Id { get; set; }
    public string Info { get; set; }
    public string OtherStuff { get; set; }
}

dbcontext:

public class MyDataContext : DbContext
{
    public DbSet<Thing > Things { get; set; }
}

code d'accès:

MyDataContext ctx = new MyDataContext();

// FIRST create a blank object
Thing thing = ctx.Things.Create();

// SECOND set the ID
thing.Id = id;

// THIRD attach the thing (id is not marked as modified)
db.Things.Attach(thing); 

// FOURTH set the fields you want updated.
thing.OtherStuff = "only want this field updated.";

// FIFTH save that thing
db.SaveChanges();

1
J'obtiens des erreurs de validation d'entité lorsque j'essaye, mais ça a l'air cool.
devlord

Cette méthode ne fonctionne pas !!!: peut-être avez-vous besoin de plus de détails sur son utilisation !!! - ceci est l'erreur: "L'association d'une entité de type 'Domain.Job' a échoué car une autre entité du même type a déjà la même valeur de clé primaire. Cela peut se produire lors de l'utilisation de la méthode 'Attach' ou de la définition de l'état d'une entité à "Inchangé" ou "Modifié" si des entités du graphique ont des valeurs de clé en conflit. Cela peut être dû au fait que certaines entités sont nouvelles et n'ont pas encore reçu de valeurs de clé générées par la base de données. "
Lucian Bumb

Perfec! Vérifiez ma réponse pour voir une approche flexible pour n'importe quel modèle!
kryp

10

En cherchant une solution à ce problème, j'ai trouvé une variante de la réponse de GONeale sur le blog de Patrick Desjardins :

public int Update(T entity, Expression<Func<T, object>>[] properties)
{
  DatabaseContext.Entry(entity).State = EntityState.Unchanged;
  foreach (var property in properties)
  {
    var propertyName = ExpressionHelper.GetExpressionText(property);
    DatabaseContext.Entry(entity).Property(propertyName).IsModified = true;
  }
  return DatabaseContext.SaveChangesWithoutValidation();
}

" Comme vous pouvez le voir, il prend comme deuxième paramètre l'expression d'une fonction. Cela permettra d'utiliser cette méthode en spécifiant dans une expression Lambda la propriété à mettre à jour. "

...Update(Model, d=>d.Name);
//or
...Update(Model, d=>d.Name, d=>d.SecondProperty, d=>d.AndSoOn);

(Une solution quelque peu similaire est également donnée ici: https://stackoverflow.com/a/5749469/2115384 )

La méthode que j'utilise actuellement dans mon propre code , étendue pour gérer également les expressions (Linq) de type ExpressionType.Convert. Cela était nécessaire dans mon cas, par exemple avec Guidet d'autres propriétés d'objet. Celles-ci ont été «enveloppées» dans un Convert () et donc non gérées par System.Web.Mvc.ExpressionHelper.GetExpressionText.

public int Update(T entity, Expression<Func<T, object>>[] properties)
{
    DbEntityEntry<T> entry = dataContext.Entry(entity);
    entry.State = EntityState.Unchanged;
    foreach (var property in properties)
    {
        string propertyName = "";
        Expression bodyExpression = property.Body;
        if (bodyExpression.NodeType == ExpressionType.Convert && bodyExpression is UnaryExpression)
        {
            Expression operand = ((UnaryExpression)property.Body).Operand;
            propertyName = ((MemberExpression)operand).Member.Name;
        }
        else
        {
            propertyName = System.Web.Mvc.ExpressionHelper.GetExpressionText(property);
        }
        entry.Property(propertyName).IsModified = true;
    }

    dataContext.Configuration.ValidateOnSaveEnabled = false;
    return dataContext.SaveChanges();
}

1
Lorsque j'utilise cela, cela me donne l'erreur suivante, Impossible de convertir l'expression Lambda en type 'Expression <Func <RequestDetail, object >> []' car ce n'est pas un type de délégué
Imran Rizvi

@ImranRizvi, il vous suffit de mettre à jour les paramètres en: public int Update (entité T, params Expression <Func <T, object >> [] properties) REMARQUE le mot-clé params avant expression
dalcam

6

Je suis en retard au jeu ici, mais c'est comme ça que je le fais, j'ai passé un certain temps à chercher une solution dont j'étais satisfait; cela produit une UPDATEinstruction UNIQUEMENT pour les champs qui sont modifiés, car vous définissez explicitement ce qu'ils sont via un concept de «liste blanche» qui est plus sûr pour empêcher l'injection de formulaire Web de toute façon.

Un extrait de mon référentiel de données ISession:

public bool Update<T>(T item, params string[] changedPropertyNames) where T 
  : class, new()
{
    _context.Set<T>().Attach(item);
    foreach (var propertyName in changedPropertyNames)
    {
        // If we can't find the property, this line wil throw an exception, 
        //which is good as we want to know about it
        _context.Entry(item).Property(propertyName).IsModified = true;
    }
    return true;
}

Cela pourrait être enveloppé dans un try..catch si vous le souhaitez, mais j'aime personnellement que mon interlocuteur connaisse les exceptions de ce scénario.

Il serait appelé de cette façon (pour moi, c'était via une API Web ASP.NET):

if (!session.Update(franchiseViewModel.Franchise, new[]
    {
      "Name",
      "StartDate"
  }))
  throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound));

2
Alors, votre meilleure solution est ce qu'Elisa? Vous devez indiquer explicitement les propriétés que vous autorisez à être mises à jour (tout comme la liste blanche requise pour la UpdateModelcommande d' ASP.NET MVC ), de cette façon vous vous assurez que l'injection de formulaire de pirate ne peut pas se produire et qu'ils ne peuvent pas mettre à jour les champs qu'ils ne sont pas autorisés à mettre à jour. Si toutefois quelqu'un peut convertir le tableau de chaînes en une sorte de paramètre d'expressions lambda et travailler avec lui dans le Update<T>, super
GONeale

3
@GONeale - Juste de passage. Quelqu'un l'a craqué avec des lambdas!
David Spence

1
@Elisa Il peut être amélioré en utilisant Func <T, List <object>> au lieu de string []
Spongebob Comrade

Même plus tard dans le jeu, et c'est peut-être une syntaxe beaucoup plus récente, mais var entity=_context.Set<T>().Attach(item);suivie par entity.Property(propertyName).IsModified = true;dans la boucle devrait fonctionner.
Auspex

4

Le cadre d'entité suit vos modifications sur les objets que vous avez interrogés à partir de la base de données via DbContext. Par exemple, si votre nom d'instance DbContext est dbContext

public void ChangePassword(int userId, string password){
     var user = dbContext.Users.FirstOrDefault(u=>u.UserId == userId);
     user.password = password;
     dbContext.SaveChanges();
}

Et à quoi devrait ressembler la vue dans ce cas?
Emanuela Colta

C'est faux car cela enregistrerait tout l'objet utilisateur avec un mot de passe modifié.
amuliar

c'est vrai, mais le reste de l'objet User sera le même que précédemment dans le contexte, la seule chose qui sera peut-être différente est le mot de passe, donc il ne met essentiellement à jour que le mot de passe.
Tomislav3008

3

Je sais que c'est un vieux fil mais je cherchais également une solution similaire et j'ai décidé d'aller avec la solution @ Doku-so fournie. Je commente pour répondre à la question posée par @Imran Rizvi, j'ai suivi le lien @ Doku-so qui montre une implémentation similaire. La question de @Imran Rizvi était qu'il obtenait une erreur en utilisant la solution fournie 'Impossible de convertir l'expression Lambda en type' Expression> [] 'car ce n'est pas un type délégué'. Je voulais proposer une petite modification que j'ai apportée à la solution de @ Doku-so qui corrige cette erreur au cas où quelqu'un d'autre rencontrerait ce message et déciderait d'utiliser la solution de @ Doku-so.

Le problème est le deuxième argument de la méthode Update,

public int Update(T entity, Expression<Func<T, object>>[] properties). 

Pour appeler cette méthode en utilisant la syntaxe fournie ...

Update(Model, d=>d.Name, d=>d.SecondProperty, d=>d.AndSoOn); 

Vous devez ajouter le mot-clé «params» devant le deuxième argument comme tel.

public int Update(T entity, params Expression<Func<T, object>>[] properties)

ou si vous ne voulez pas changer la signature de la méthode, pour appeler la méthode Update, vous devez ajouter le mot-clé ' new ', spécifier la taille du tableau, puis enfin utiliser la syntaxe d'initialisation d'objet de collection pour chaque propriété à mettre à jour comme indiqué au dessous de.

Update(Model, new Expression<Func<T, object>>[3] { d=>d.Name }, { d=>d.SecondProperty }, { d=>d.AndSoOn });

Dans l'exemple de @ Doku-so, il spécifie un tableau d'expressions, vous devez donc transmettre les propriétés à mettre à jour dans un tableau, à cause du tableau, vous devez également spécifier la taille du tableau. Pour éviter cela, vous pouvez également modifier l'argument de l'expression pour utiliser IEnumerable au lieu d'un tableau.

Voici ma mise en œuvre de la solution de @ Doku-so.

public int Update<TEntity>(LcmsEntities dataContext, DbEntityEntry<TEntity> entityEntry, params Expression<Func<TEntity, object>>[] properties)
     where TEntity: class
    {
        entityEntry.State = System.Data.Entity.EntityState.Unchanged;

        properties.ToList()
            .ForEach((property) =>
            {
                var propertyName = string.Empty;
                var bodyExpression = property.Body;
                if (bodyExpression.NodeType == ExpressionType.Convert
                    && bodyExpression is UnaryExpression)
                {
                    Expression operand = ((UnaryExpression)property.Body).Operand;
                    propertyName = ((MemberExpression)operand).Member.Name;
                }
                else
                {
                    propertyName = System.Web.Mvc.ExpressionHelper.GetExpressionText(property);
                }

                entityEntry.Property(propertyName).IsModified = true;
            });

        dataContext.Configuration.ValidateOnSaveEnabled = false;

        return dataContext.SaveChanges();
    }

Usage:

this.Update<Contact>(context, context.Entry(modifiedContact), c => c.Active, c => c.ContactTypeId);

@ Doku-so a fourni une approche intéressante en utilisant des génériques, j'ai utilisé le concept pour résoudre mon problème, mais vous ne pouvez tout simplement pas utiliser la solution de @ Doku-so telle quelle et dans cet article et dans l'article lié, personne n'a répondu aux questions sur les erreurs d'utilisation.


Je travaillais sur votre solution lorsque le programme passe la ligne entityEntry.State = EntityState.Unchanged;toutes les valeurs mises à jour dans le paramètre entityEntrysont annulées, donc aucune modification n'est enregistrée, pouvez-vous s'il vous plaît aider, merci
sairfan

3

Dans EntityFramework Core 2.x, il n'est pas nécessaire de Attach:

 // get a tracked entity
 var entity = context.User.Find(userId);
 entity.someProp = someValue;
 // other property changes might come here
 context.SaveChanges();

J'ai essayé cela dans SQL Server et en le profilant:

exec sp_executesql N'SET NOCOUNT ON;
UPDATE [User] SET [someProp] = @p0
WHERE [UserId] = @p1;
SELECT @@ROWCOUNT;

',N'@p1 int,@p0 bit',@p1=1223424,@p0=1

Find garantit que les entités déjà chargées ne déclenchent pas de SELECT et attache également automatiquement l'entité si nécessaire (à partir de la documentation):

    ///     Finds an entity with the given primary key values. If an entity with the given primary key values
    ///     is being tracked by the context, then it is returned immediately without making a request to the
    ///     database. Otherwise, a query is made to the database for an entity with the given primary key values
    ///     and this entity, if found, is attached to the context and returned. If no entity is found, then
    ///     null is returned.

1

En combinant plusieurs suggestions, je propose les suivantes:

    async Task<bool> UpdateDbEntryAsync<T>(T entity, params Expression<Func<T, object>>[] properties) where T : class
    {
        try
        {
            var entry = db.Entry(entity);
            db.Set<T>().Attach(entity);
            foreach (var property in properties)
                entry.Property(property).IsModified = true;
            await db.SaveChangesAsync();
            return true;
        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.WriteLine("UpdateDbEntryAsync exception: " + ex.Message);
            return false;
        } 
    }

appelé par

UpdateDbEntryAsync(dbc, d => d.Property1);//, d => d.Property2, d => d.Property3, etc. etc.);

Ou par

await UpdateDbEntryAsync(dbc, d => d.Property1);

Ou par

bool b = UpdateDbEntryAsync(dbc, d => d.Property1).Result;

Comment rendre cette méthode disponible pour une autre classe, peut être comme une méthode d'extension?
Velkumar le

dans ce didacticiel .NET CORE, ils montrent les meilleures pratiques en utilisant (le nouveau) EF Core pour mettre à jour des propriétés spécifiques dans MVC. recherchez «TryUpdateModelAsync».
Guy

1
@Guy Awesome. Cependant, une fois de plus, la "meilleure pratique" de Microsoft est de faire autre chose que ce que leurs outils construisent ...
Auspex

C'est une bonne solution.
Timothy Macharia le

1

J'utilise ValueInjecternuget pour injecter le modèle de liaison dans l'entité de base de données en utilisant ce qui suit:

public async Task<IHttpActionResult> Add(CustomBindingModel model)
{
   var entity= await db.MyEntities.FindAsync(model.Id);
   if (entity== null) return NotFound();

   entity.InjectFrom<NoNullsInjection>(model);

   await db.SaveChangesAsync();
   return Ok();
}

Notez l'utilisation d'une convention personnalisée qui ne met pas à jour les propriétés si elles sont nulles à partir du serveur.

ValueInjecter v3 +

public class NoNullsInjection : LoopInjection
{
    protected override void SetValue(object source, object target, PropertyInfo sp, PropertyInfo tp)
    {
        if (sp.GetValue(source) == null) return;
        base.SetValue(source, target, sp, tp);
    }
}

Usage:

target.InjectFrom<NoNullsInjection>(source);

Value Injecter V2

Recherchez cette réponse

Caveat

Vous ne saurez pas si la propriété est intentionnellement effacée à null OU si elle n'a simplement aucune valeur. En d'autres termes, la valeur de propriété ne peut être remplacée que par une autre valeur mais pas effacée.


0

Je cherchais la même chose et j'ai finalement trouvé la solution

using (CString conn = new CString())
{
    USER user = conn.USERs.Find(CMN.CurrentUser.ID);
    user.PASSWORD = txtPass.Text;
    conn.SaveChanges();
}

croyez-moi, cela fonctionne pour moi comme un charme.


0

C'est ce que j'utilise, en utilisant InjectNonNull personnalisé (obj dest, obj src), cela le rend totalement flexible

[HttpPost]
public async Task<IActionResult> Post( [FromQuery]Models.Currency currency ) {
  if ( ModelState.IsValid ) {
    // find existing object by Key
    Models.Currency currencyDest = context.Currencies.Find( currency.Id ); 

    context.Currencies.Attach( currencyDest );

    // update only not null fields
    InjectNonNull( currencyDest, currency );

    // save
    await context.SaveChangesAsync( );
  }  
  return Ok();
}

// Custom method
public static T InjectNonNull<T>( T dest, T src ) {
  foreach ( var propertyPair in PropertyLister<T, T>.PropertyMap ) {
    var fromValue = propertyPair.Item2.GetValue( src, null );
    if ( fromValue != null && propertyPair.Item1.CanWrite ) {
       propertyPair.Item1.SetValue( dest, fromValue, null );
    }
  }
  return dest;
}

-1
public async Task<bool> UpdateDbEntryAsync(TEntity entity, params Expression<Func<TEntity, object>>[] properties)
{
    try
    {
        this.Context.Set<TEntity>().Attach(entity);
        EntityEntry<TEntity> entry = this.Context.Entry(entity);
        entry.State = EntityState.Modified;
        foreach (var property in properties)
            entry.Property(property).IsModified = true;
        await this.Context.SaveChangesAsync();
        return true;
    }
    catch (Exception ex)
    {
        throw ex;
    }
}

-7
public void ChangePassword(int userId, string password)
{
  var user = new User{ Id = userId, Password = password };
  using (var db = new DbContextName())
  {
    db.Entry(user).State = EntityState.Added;
    db.SaveChanges();
  }
}

1
Cela ajoutera une nouvelle ligne. La question est de savoir comment mettre à jour un existant.
Edward Brey
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.