如何使用实体框架仅更新一个字段?


188

这是桌子

用户数

UserId
UserName
Password
EmailAddress

和代码。

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

26
通过Password,你的意思是哈希密码,对不对?:-)
爱德华·布雷

Answers:


368

Ladislav的答案已更新为使用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;
Johan 2013年

5
当表具有时间戳字段时,此方法将引发OptimisticConcurencyException。
马克西姆六世。

9
我认为值得一提的是,如果您正在使用db.Configuration.ValidateOnSaveEnabled = false;,则可能希望继续验证要更新的字段:if (db.Entry(user).Property(x => x.Password).GetValidationErrors().Count == 0)
Ziul 2015年

2
如果您在更新过程中未提供表中的必填字段,则需要将ValidateOnSaveEnabled设置为false
Sal

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

16

您基本上有两种选择:

  • 一路走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实际上,您不必加载整个对象!
Arvand

13

在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 2013年

此方法不起作用!!!:也许您需要提供更多使用方法的详细信息!!!-这是错误:“附加类型为'Domain.Job'的实体失败,因为相同类型的另一个实体已经具有相同的主键值。在使用'附加'方法或设置实体的状态时可能会发生如果图形中的任何实体具有相互冲突的键值,则设置为“未更改”或“已修改”。这可能是因为某些实体是新实体,尚未收到数据库生成的键值。”
Lucian Bumb

完美!检查我的答案以查看适用于任何模型的灵活方法!
kryp

10

在寻找解决方案时,我通过Patrick Desjardins的博客在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

我当前在自己的代码中使用的方法,已扩展为还处理type的(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表达式转换为类型'Expression <Func <RequestDetail,object >> []'
Imran Rizvi

@ImranRizvi,您只需要将参数更新为:public int Update(T实体,params Expression <Func <T,object >> []属性)注意expression之前的params关键字
dalcam

6

我在这里玩游戏迟到了,但这就是我的工作方式,我花了一段时间寻找一个令我满意的解决方案;UPDATE当您通过“白名单”概念显式定义它们的含义时,这仅对已更改的字段产生一条语句,这反过来更安全地防止了Web表单注入。

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

如果您愿意,可以将其包装在try..catch中,但是我个人希望调用者了解这种情况下的异常。

它将以这种方式调用(对我而言,这是通过ASP.NET Web API进行的):

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

2
那么您更好的解决方案是什么Elisa?您应该明确声明允许更新哪些属性(就像ASP.NET MVC的UpdateModel命令所需的白名单一样),这样可以确保不会发生黑客表单注入并且它们无法更新不允许更新的字段。但是,如果有人可以将字符串数组转换为某种lambda表达式参数,然后在Update<T>
--gneale中

3
@GONeale-刚刚通过。有人使用Lambdas破解了它!
David Spence

1
@Elisa可以通过使用Func <T,List <object >>代替string []进行改进
Spongebob同志

甚至到了游戏晚些时候,也许这是最新的语法,但是在循环中var entity=_context.Set<T>().Attach(item);跟随它entity.Property(propertyName).IsModified = true;应该起作用。
Auspex

4

实体框架跟踪您对通过DbContext从数据库查询的对象所做的更改。例如,如果您的DbContext实例名称是dbContext

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

在这种情况下,视图应如何显示?
伊曼纽拉·科尔塔

这是错误的,因为它将用更改的密码保存整个User对象。
Amuliar

的确如此,但是User对象的其余部分将与上下文中的内容相同,唯一可能不同的是密码,因此它实际上仅更新了密码。
Tomislav3008 '18

3

我知道这是一个旧线程,但我也在寻找类似的解决方案,因此决定使用提供的@ Doku-so解决方案。我评论要回答@Imran Rizvi提出的问题,我遵循了@ Doku-so链接,该链接显示了类似的实现。@Imran Rizvi的问题是,使用提供的解决方案“由于无法将Lambda表达式转换为Type'Expression> []',因为它不是委托类型”,他遇到了错误。我想对@ Doku-so的解决方案进行一些小的修改,以解决此错误,以防其他人遇到此帖子并决定使用@ Doku-so的解决方案。

问题是Update方法中的第二个参数,

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方法,您需要添加' new '关键字,指定数组的大小,然后最后对每个属性使用collection object initializer语法进行更新,如下所示下面。

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

在@ Doku-so的示例中,他指定了一个表达式数组,因此您必须传递属性以在数组中进行更新,因为该数组还必须指定该数组的大小。为了避免这种情况,您还可以将expression参数更改为使用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-so使用泛型方法提供了一种很酷的方法,我用这个概念解决了我的问题,但您只是不能按原样使用@ Doku-so的解决方案,在本帖子和链接的帖子中都没有人回答使用错误问题。


当程序通过行时,我正在为您的解决方案工作entityEntry.State = EntityState.Unchanged;,参数中的所有更新值entityEntry都将还原,因此未保存任何更改,请您帮忙,谢谢
sairfan

2

在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

查找可确保已加载的实体不会触发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;

如何使此方法可用于其他类,可能类似于扩展方法?
Velkumar 2016年

此.NET CORE教程中,他们展示了使用(新的)EF Core更新MVC中特定属性的最佳实践。寻找“ TryUpdateModelAsync”。
盖伊

1
@Guy太棒了。虽然,微软的“最佳实践”再一次是做一些除了他们的工具之外的事情……
Auspex

这是一个很好的解决方案。
Timothy Macharia

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

价值注入器V2

查找此答案

警告

您将不知道该属性是否被有意清除为null或只是没有任何值。换句话说,属性值只能替换为另一个值,而不能清除。


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
这将添加一个新行。问题是如何更新现有的。
爱德华·布雷
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.