Як оновити лише одне поле за допомогою Entity Framework?


189

Ось стіл

Користувачі

UserId
UserName
Password
EmailAddress

і код ..

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

26
До Password, ви маєте в виду хеш пароля, НЕ так? :-)
Едвард Брей

Відповіді:


370

Відповідь Ладіслава оновлено, щоб використовувати DbContext (введений в 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
Мені вдалося змусити цей код працювати, додавши db.Configuration.ValidateOnSaveEnabled = false; перед db.SaveChanges ()?
Джейк Дрю

3
Який простір імен включити для використання, db.Entry(user).Property(x => x.Password).IsModified = true;а ніdb.Entry(user).Property("Password").IsModified = true;
Йоган

5
Цей підхід кидає OptimisticConcurencyException, коли в таблиці є поле часової позначки.
Максим Ві.

9
Думаю, варто згадати, що якщо ви використовуєте, db.Configuration.ValidateOnSaveEnabled = false;ви можете продовжити перевірку поля, яке ви оновлюєте:if (db.Entry(user).Property(x => x.Password).GetValidationErrors().Count == 0)
Ziul

2
Потрібно встановити значення ValidateOnSaveEnabled на значення false, якщо у вашій таблиці потрібні поля, які ви не надаєте під час оновлення
Ср

54

Ви можете сказати EF, які властивості потрібно оновити таким чином:

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 недоступний для DBContext
LoxLox

17

У вас є два варіанти:

  • пройти шлях EF до кінця, у цьому випадку ви б хотіли
    • завантажте об’єкт на основі userIdнаданого - весь об’єкт завантажується
    • оновити passwordполе
    • зберегти об'єкт назад з допомогою контексту в .SaveChanges()метод

У цьому випадку, залежно від EF, як детально впоратися з цим. Я щойно перевірив це, і якщо я змінюю лише одне поле об'єкта, те, що створює EF, значною мірою те, що ви також створили б вручну - щось на зразок:

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

Тож EF досить розумний, щоб зрозуміти, які стовпці дійсно змінилися, і він створить оператор T-SQL для обробки саме тих оновлень, які насправді необхідні.

  • Ви визначаєте збережену процедуру, яка виконує саме те, що вам потрібно, у коді T-SQL (просто оновіть Passwordстовпець для даного UserIdта нічого іншого - в основному виконується UPDATE dbo.Users SET Password = @Password WHERE UserId = @UserId), і ви створюєте імпорт функції для цієї збереженої процедури у вашій моделі EF, і ви називаєте це функція замість того, щоб виконувати описані вище кроки

1
@ marc-s Насправді вам не доведеться завантажувати весь об’єкт!
Арванд

14

В Entity Framework Core Attachповертає запис, тому все, що вам потрібно, це:

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

12

я використовую це:

суб'єкт:

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

код доступу:

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
При спробі цього я отримую помилки перевірки сутності, але це впевнено виглядає круто.
devlord

Цей метод не працює !!!: можливо, вам потрібно детальніше розповісти, як ним користуватися !!! - це помилка: "Не вдалося приєднати сутність типу" Domain.Job ", оскільки інша суть одного типу вже має те саме значення первинного ключа. Це може статися під час використання методу" Приєднати "або встановлення стану сутності до "Незмінним" або "Модифікованим", якщо будь-які об'єкти графіка мають суперечливі ключові значення. Це може бути тому, що деякі об'єкти є новими і ще не отримали базові значення, створені для бази даних. "
Лукіан Бамб

Perfec! Перевірте мою відповідь, щоб побачити гнучкий підхід до будь-якої моделі!
крип

10

Шукаючи рішення цієї проблеми, я знайшов варіант у відповіді GONeale через блог Патріка Дежардінса :

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

" Як ви бачите, він приймає за свій другий параметр вираження функції. Це дозволить використовувати цей метод, вказавши в виразі Lambda, яке властивість оновлювати. "

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

(Дещо подібне рішення також подано тут: https://stackoverflow.com/a/5749469/2115384 )

Метод, який я зараз використовую у своєму власному коді , розширений для обробки також (Linq) виразів типу ExpressionType.Convert. Це було необхідно в моєму випадку, наприклад, з Guidіншими властивостями об'єкта. Вони були "загорнуті" в Convert () і тому не обробляються ними 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
Коли я використовую це, мені дається наступна помилка: Неможливо перетворити вираз Lambda у тип "Вираз <Func <RequestDetail, object >> []", оскільки це не тип делегата
Imran Rizvi

@ImranRizvi, вам просто потрібно оновити параметри до: public int Update (T-сутність, параметри Вираз <Властивості <Func <T, об'єкт >> []) ПРИМІТКА ключове слово params перед виразом
dalcam

6

Я запізнююсь на грі тут, але ось так я це роблю, я витратив деякий час на полювання на рішення, яким я був задоволений; це створює UPDATEоператор ТОЛЬКО для полів, які змінюються, оскільки ви чітко визначаєте, що вони є через концепцію "білого списку", яка є більш безпечною для запобігання ін'єкції веб-форм у будь-якому випадку.

Витяг із мого сховища даних 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;
}

Це може бути зафіксовано у спробу. Злови, якщо ви цього хотіли, але мені особисто подобається, щоб мій абонент знав про винятки в цьому сценарії.

Це буде називатися таким чином (для мене це було через веб-API ASP.NET):

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

2
Тож вашим кращим рішенням є те, що Еліза? Ви повинні чітко вказати, які властивості ви дозволяєте оновлювати (як і білий список, необхідний для команди ASP.NET MVC UpdateModel), таким чином ви гарантуєте, що введення форми хакера не може відбуватися, і вони не можуть оновлювати поля, вони не мають права оновлювати. Якщо хтось може перетворити рядковий масив у якийсь параметр виразів лямбда та працювати з ним у Update<T>, чудово
GONeale

3
@GONeale - Просто проїжджаючи. Хтось зламав це за допомогою лямбда!
Девід Спенс

1
@Elisa Це можна покращити за допомогою Func <T, List <object>> замість рядка []
Spongebob Comrade

Навіть пізніше до гри, і, можливо, це набагато новіший синтаксис, але var entity=_context.Set<T>().Attach(item);слідом за entity.Property(propertyName).IsModified = true;цим циклом має працювати.
Auspex

4

Entity Framework відстежує ваші зміни на об'єктах, які ви запитували з бази даних через DbContext. Наприклад, якщо вам ім'я екземпляра DbContext є dbContext

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

І як повинен виглядати погляд у цьому випадку?
Емануела Кольта

Це неправильно, оскільки це дозволить зберегти весь об’єкт користувача зі зміненим паролем.
amuliar

це правда, але решта об'єкта User буде такою ж, як і раніше в контексті, єдине, що, можливо, буде іншим - це пароль, тому його по суті є лише оновлення пароля.
Tomislav3008

3

Я знаю, що це стара тема, але я також шукав подібне рішення і вирішив перейти з рішенням @ Doku - так. Я коментую відповідь на запитання, задане @Imran Rizvi, я перейшов до посилання @ Doku-so, яке показує подібну реалізацію. Питання @Imran Rizvi полягало в тому, що він отримував помилку за допомогою наданого рішення "Неможливо перетворити вираз Lambda у тип" Вираз> [] ", оскільки це не тип делегата". Я хотів запропонувати невелику модифікацію, яку я вніс до рішення @ Doku-so, яке виправляє цю помилку, якщо хтось інший натрапить на цю посаду і вирішить скористатися рішенням @ Doku-so.

Проблема є другим аргументом у методі оновлення,

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

Для виклику цього методу за допомогою наданого синтаксису ...

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

Потрібно додати ключове слово 'params' перед другою рухомою так.

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

або якщо ви не хочете змінювати підпис методу, то для виклику методу оновлення вам потрібно додати ключове слово " новий ", вказати розмір масиву, а потім, нарешті, використовувати синтаксис ініціалізатора об'єкта колекції для кожного властивості для оновлення, як показано. нижче.

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

У прикладі @ Doku-so він вказує масив виразів, тому ви повинні передавати властивості для оновлення в масиві, оскільки для масиву ви також повинні вказати розмір масиву. Щоб уникнути цього, ви також можете змінити аргумент виразу, щоб використовувати IEnumerable замість масиву.

Ось моя реалізація рішення @ 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();
    }

Використання:

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

@ Doku - таким чином був класний підхід із використанням загальних, я використовував цю концепцію, щоб вирішити свою проблему, але ви просто не можете використовувати рішення @ Doku-so, як є, і в цій публікації, і в пов'язаному досі ніхто не відповідав на питання про помилки використання.


Я працював над вашим рішенням, коли програма передає рядок entityEntry.State = EntityState.Unchanged;усі оновлені значення в параметрі entityEntryповернути назад, тому ніяких змін не збережено, чи можете ви допомогти в цьому, дякую
sairfan

3

В EntityFramework Core 2.x не потрібно Attach:

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

Спробував це на SQL Server та профайл:

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 гарантує, що вже завантажені об'єкти не спрацьовують SELECT, а також автоматично приєднує об'єкт при необхідності (з документів):

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

Поєднуючи кілька пропозицій, я пропоную наступне:

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

покликаний

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

Або мимо

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

Або мимо

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

Як зробити цей метод доступним для іншого класу, це може бути як метод розширення?
Велкумар

У цьому підручнику .NET CORE вони демонструють кращі практики використання (нового) EF Core для оновлення конкретних властивостей у MVC. шукайте "TryUpdateModelAsync".
Хлопець

1
@Guy Awesome. Хоча, знову "найкраща практика" Майкрософт - це робити щось інше, ніж те, що будують їхні інструменти ...
Auspex

Це хороше рішення.
Тимофій Мачарія

1

Я використовую ValueInjecternuget для введення моделі прив'язки до сутності бази даних, використовуючи наступне:

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

Зверніть увагу на використання спеціальної конвенції, яка не оновлює Властивості, якщо вони недійсні з сервера.

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

Використання:

target.InjectFrom<NoNullsInjection>(source);

Значення Injecter V2

Знайдіть цю відповідь

Caveat

Ви не знатимете, чи навмисно очищено властивість, щоб звести нанівець АБО воно просто не мало значення. Іншими словами, значення властивості можна замінити лише іншим значенням, але не очистити.


0

Я шукав те саме і, нарешті, знайшов рішення

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

повірте, це працює на мене як на принадність.


0

Це те, що я використовую, використовуючи користувацький InjectNonNull (obj dest, obj src), він робить його повністю гнучким

[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
Це додасть новий рядок. Питання в тому, як оновити існуючий.
Едвард Брей
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.