Jak zaktualizować tylko jedno pole za pomocą Entity Framework?


189

Oto tabela

Użytkownicy

UserId
UserName
Password
EmailAddress

i kod ..

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

26
Przez Password, to znaczy hashed hasło, prawda? :-)
Edward Brey

Odpowiedzi:


370

Zaktualizowano odpowiedź Ladislava, by używała DbContext (wprowadzona w 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
Udało mi się sprawić, aby ten kod działał tylko przez dodanie db.Configuration.ValidateOnSaveEnabled = false; przed db.SaveChanges ()?
Jake Drew

3
Której przestrzeni nazw należy użyć, db.Entry(user).Property(x => x.Password).IsModified = true;a której niedb.Entry(user).Property("Password").IsModified = true;
Johan

5
Takie podejście zgłasza wyjątek OptimisticConcurencyException, gdy tabela ma pole znacznika czasu.
Maksim Vi.

9
Myślę, że warto wspomnieć, że jeśli używasz, db.Configuration.ValidateOnSaveEnabled = false;możesz nadal sprawdzać poprawność aktualizowanego pola:if (db.Entry(user).Property(x => x.Password).GetValidationErrors().Count == 0)
Ziul

2
Musisz ustawić ValidateOnSaveEnabled na false, jeśli masz wymagane pola w tabeli, których nie podajesz podczas aktualizacji
Sal

54

Możesz powiedzieć EF, które właściwości należy zaktualizować w ten sposób:

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 nie jest dostępny dla DBContext
LoxLox

17

Masz w zasadzie dwie opcje:

  • idź do końca EF, w takim przypadku zrobiłbyś to
    • załaduj obiekt na podstawie userIddostarczonego - cały obiekt zostanie załadowany
    • zaktualizuj passwordpole
    • zapisz obiekt ponownie stosując kontekście za .SaveChanges()metodę

W takim przypadku to EF zależy od tego, jak sobie z tym poradzić. Właśnie to przetestowałem i w przypadku, gdy zmieniam tylko jedno pole obiektu, to, co tworzy EF, jest prawie tym, co stworzyłbyś ręcznie - coś w stylu:

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

EF jest więc wystarczająco inteligentny, aby dowiedzieć się, które kolumny rzeczywiście się zmieniły, i utworzy instrukcję T-SQL do obsługi tylko tych aktualizacji, które w rzeczywistości są konieczne.

  • definiujesz procedurę przechowywaną, która robi dokładnie to, czego potrzebujesz, w kodzie T-SQL (wystarczy zaktualizować Passwordkolumnę dla podanego UserIdi nic więcej - w zasadzie wykonuje się UPDATE dbo.Users SET Password = @Password WHERE UserId = @UserId) i tworzysz import funkcji dla tej procedury przechowywanej w modelu EF i wywołujesz to funkcja zamiast wykonywać czynności opisane powyżej

1
@ marc-s Właściwie nie musisz ładować całego obiektu!
Arvand

14

W Entity Framework Core Attachzwraca wpis, więc wszystko czego potrzebujesz to:

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

12

używam tego:

jednostka:

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

kod akcesorium:

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
Gdy próbuję, otrzymuję błędy sprawdzania poprawności jednostki, ale na pewno wygląda to świetnie.
devlord,

Nie działa ta metoda !!!: może musisz podać więcej szczegółów, jak z niej korzystać !!! - to jest błąd: „Dołączanie encji typu„ Domain.Job ”nie powiodło się, ponieważ inna encja tego samego typu ma już tę samą wartość klucza podstawowego. Może się to zdarzyć, gdy użyjesz metody„ Attach ”lub ustawisz stan encji na „Niezmieniony” lub „Zmodyfikowany”, jeśli dowolne podmioty na wykresie mają sprzeczne wartości kluczy. Może to być spowodowane tym, że niektóre podmioty są nowe i nie otrzymały jeszcze wartości kluczy wygenerowanych przez bazę danych. ”
Lucian Bumb

Perfec! Sprawdź moją odpowiedź, aby zobaczyć elastyczne podejście do każdego modelu!
kryp

10

Szukając rozwiązania tego problemu, znalazłem odmianę odpowiedzi GONeale na blogu Patricka Desjardinsa :

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

Jak widać, jako drugi parametr przyjmuje wyrażenie funkcji. Pozwoli to na użycie tej metody przez określenie w wyrażeniu Lambda, którą właściwość należy zaktualizować.

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

(Nieco podobne rozwiązanie podano również tutaj: https://stackoverflow.com/a/5749469/2115384 )

Metoda, której obecnie używam we własnym kodzie , została rozszerzona o obsługę również wyrażeń typu (Linq) ExpressionType.Convert. Było to konieczne w moim przypadku, na przykład w przypadku Guidi innych właściwości obiektu. Zostały one „opakowane” w Convert () i dlatego nie były obsługiwane przez 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
Gdy go używam, pojawia się następujący błąd: Nie można przekonwertować wyrażenia Lambda na typ „Wyrażenie <Func <RequestDetail, obiekt >> []”, ponieważ nie jest to typ delegata
Imran Rizvi

@ImranRizvi, wystarczy zaktualizować parametry do: public int Aktualizacja (jednostka T, parametry Wyrażenie <Func <T, obiekt >> [] właściwości) UWAGA słowo kluczowe params przed wyrażeniem
dalcam

6

Spóźniłem się tutaj na grę, ale tak to robię, spędziłem trochę czasu na poszukiwaniu rozwiązania, z którego byłem usatysfakcjonowany; tworzy to UPDATEinstrukcję TYLKO dla pól, które zostały zmienione, ponieważ wyraźnie definiujesz, co to jest za pomocą koncepcji „białej listy”, która jest bezpieczniejsza, aby zapobiec wstrzyknięciu formularza internetowego.

Fragment z mojego repozytorium danych 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;
}

Można to zawrzeć w try..catch, jeśli chcesz, ale osobiście lubię mojego rozmówcę, aby wiedział o wyjątkach w tym scenariuszu.

Zostałby wywołany w taki sposób (dla mnie było to przez interfejs API sieci Web ASP.NET):

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

2
Więc twoim lepszym rozwiązaniem jest to, co Elisa? Powinieneś wyraźnie określić, jakie właściwości zezwalasz na aktualizację (tak jak biała lista wymagana dla polecenia ASP.NET MVC UpdateModel), w ten sposób upewnisz się, że nie można wprowadzić wstrzyknięcia formularza hakera i nie mogą oni aktualizować pól, których nie mogą aktualizować. Jeśli jednak ktoś może przekonwertować tablicę ciągów znaków na jakiś parametr wyrażeń lambda i pracować z nim wUpdate<T>
GONeale

3
@GONeale - Właśnie przechodzę. Ktoś go złamał za pomocą Lambdas!
David Spence

1
@Elisa Można to ulepszyć, używając Func <T, Lista <obiekt>> zamiast ciągu []
Spongebob Comrade

Nawet później do gry, i być może jest to znacznie nowsza składnia, ale powinna var entity=_context.Set<T>().Attach(item);następować entity.Property(propertyName).IsModified = true;w pętli.
Auspex,

4

Struktura Entity śledzi twoje zmiany w obiektach, które sprawdziłeś w bazie danych za pomocą DbContext. Na przykład jeśli nazwa instancji DbContext to dbContext

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

A jak powinien wyglądać widok w tym przypadku?
Emanuela Colta

Jest to błędne, ponieważ zapisałoby cały obiekt użytkownika ze zmienionym hasłem.
amuliar

to prawda, ale reszta obiektu użytkownika będzie taka sama, jak poprzednio w kontekście, jedyną rzeczą, która prawdopodobnie będzie inna, jest hasło, a więc zasadniczo tylko aktualizacja hasła.
Tomislav3008

3

Wiem, że to stary wątek, ale szukałem również podobnego rozwiązania i postanowiłem skorzystać z dostarczonego rozwiązania @ Doku. Komentuję, aby odpowiedzieć na pytanie zadane przez @Imran Rizvi, podążyłem za linkiem @ Doku-so, który pokazuje podobną implementację. @Imran Rizvi zadał pytanie, że dostaje błąd przy użyciu dostarczonego rozwiązania „Nie można przekonwertować wyrażenia Lambda na typ„ Wyrażenie> [] ”, ponieważ nie jest to typ delegata”. Chciałem zaoferować niewielką modyfikację, którą wprowadziłem w rozwiązaniu @ Doku-so, która naprawia ten błąd w przypadku, gdy ktoś napotka ten post i zdecyduje się użyć rozwiązania @ Doku-so.

Problem jest drugim argumentem w metodzie aktualizacji,

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

Aby wywołać tę metodę przy użyciu dostarczonej składni ...

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

Musisz dodać słowo kluczowe „params” przed drugim argumentem.

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

lub jeśli nie chcesz zmieniać podpisu metody, a następnie, aby wywołać metodę Update, musisz dodać słowo kluczowe „ new ”, określić rozmiar tablicy, a następnie w końcu użyć składni inicjalizującej obiekt kolekcji dla każdej właściwości do zaktualizowania, jak widać poniżej.

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

W przykładzie @ Doku-so określa tablicę wyrażeń, więc musisz przekazać właściwości, aby zaktualizować tablicę, ponieważ ze względu na tablicę musisz także określić jej rozmiar. Aby tego uniknąć, możesz również zmienić argument wyrażenia, aby używał IEnumerable zamiast tablicy.

Oto moja implementacja rozwiązania @ 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();
    }

Stosowanie:

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

@ Doku-so zapewniło fajne podejście przy użyciu ogólnych, użyłem tej koncepcji, aby rozwiązać mój problem, ale po prostu nie możesz użyć rozwiązania @ Doku-so w takiej postaci, w jakiej się znajduje, i zarówno w tym poście, jak i w łączonym poście nikt nie odpowiedział na pytania o błędzie użytkowania.


Pracowałem nad twoim rozwiązaniem, gdy program przejdzie przez linię entityEntry.State = EntityState.Unchanged;wszystkie zaktualizowane wartości w parametrze entityEntryget cofnij, więc żadne zmiany nie są zapisywane, czy możesz pomóc w tym, dziękuję
sairfan

3

W EntityFramework Core 2.x nie ma potrzeby Attach:

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

Próbowałem tego w SQL Server i profilowałem:

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

Znajdź zapewnia, że ​​już załadowane encje nie wyzwalają SELECT, a także automatycznie dołącza encję w razie potrzeby (z dokumentacji):

    ///     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

Łącząc kilka sugestii proponuję następujące:

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

nazwany przez

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

Lub przez

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

Lub przez

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

Jak udostępnić tę metodę innym klasom, może to być metoda rozszerzenia?
Velkumar

w tym samouczku .NET CORE pokazano najlepsze praktyki przy użyciu (nowego) EF Core do aktualizacji określonych właściwości w MVC. poszukaj „TryUpdateModelAsync”.
Guy

1
@Guy Awesome. Chociaż „najlepszą praktyką” Microsoftu jest zrobienie czegoś innego niż to, co budują ich narzędzia ...
Auspex,

To dobre rozwiązanie.
Timothy Macharia

1

Używam ValueInjecternugetu do wstrzykiwania modelu wiązania do encji bazy danych przy użyciu:

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

Zwróć uwagę na użycie niestandardowej konwencji, która nie aktualizuje właściwości, jeśli są one zerowe z serwera.

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

Stosowanie:

target.InjectFrom<NoNullsInjection>(source);

Wartość wtryskiwacza V2

Wyszukaj tę odpowiedź

Zastrzeżenie

Nie będziesz wiedział, czy właściwość została celowo wyczyszczona na wartość zerową LUB po prostu nie miała żadnej wartości. Innymi słowy, wartość właściwości może być zastąpiona inną wartością, ale nie wyczyszczona.


0

Szukałem tego samego i wreszcie znalazłem rozwiązanie

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

uwierz mi, działa dla mnie jak urok.


0

Właśnie tego używam, używając niestandardowego InjectNonNull (obj dest, obj src), dzięki czemu jest w pełni elastyczny

[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
Spowoduje to dodanie nowego wiersza. Pytanie brzmi, jak zaktualizować istniejący.
Edward Brey,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.