如何使用Entity Framework仅更新一个字段?

239

这里是表格

用户

UserId
UserName
Password
EmailAddress

以及代码..

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

36
“Password”指的是哈希密码,对吗? :-) - Edward Brey
17个回答

440
更新:如果您使用的是EF Core 7.0或更高版本,请参阅this answer。 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();
    }
}

62
我只能在在 db.SaveChanges() 之前添加 db.Configuration.ValidateOnSaveEnabled = false; 才能让这段代码正常工作? - Jake Drew
3
要使用 db.Entry(user).Property(x => x.Password).IsModified = true; 而不是 db.Entry(user).Property("Password").IsModified = true;,需要包含哪个命名空间? - Johan
5
当表格具有时间戳字段时,该方法会抛出一个 OptimisticConcurrencyException 异常。 - Maksim Vi.
9
值得一提的是,如果您正在使用db.Configuration.ValidateOnSaveEnabled = false;,您可能希望继续验证正在更新的字段:if (db.Entry(user).Property(x => x.Password).GetValidationErrors().Count == 0) - Ziul
3
如果您在更新时未提供所需字段,则需要将ValidateOnSaveEnabled设置为false。 - Sal
显示剩余16条评论

54

你可以通过以下方式告诉需要更新哪些属性:

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

35

EF Core 7新特性——ExecuteUpdate:

终于来了!经过长时间的等待,EF Core 7.0现在有了一种原生支持的方法来运行UPDATE(以及DELETE)语句,并允许您使用任意的LINQ查询(.Where(u => ...)),而无需先从数据库中检索相关实体:这个新的内置方法称为ExecuteUpdate——请参见"EF Core 7.0有什么新功能?"

ExecuteUpdate正是为这些场景而设计的,它可以操作任何IQueryable实例,并允许您更新任意数量行的特定列,同时始终在后台发出一个单独的UPDATE语句,使其尽可能高效。

用法:

我们来看OP的示例,即更新特定用户的密码列:

dbContext.Users
    .Where(u => u.Id == someId)
    .ExecuteUpdate(b =>
        b.SetProperty(u => u.Password, "NewPassword")
    );

正如您所看到的,调用ExecuteUpdate需要您调用SetProperty方法,以指定要更新的属性以及要分配给它的新值。

EF Core将把此转换为以下UPDATE语句:

UPDATE [u]
    SET [u].[Password] = "NewPassword"
FROM [Users] AS [u]
WHERE [u].[Id] = someId

此外,ExecuteDelete 可用于删除行:

还有一个与 ExecuteUpdate 相对应的东西,叫做 ExecuteDelete,正如其名称所示,可以用来一次性删除单个或多个行,而无需先获取它们。

用法:

// Delete users that haven't been active in 2022:
dbContext.Users
    .Where(u => u.LastActiveAt.Year < 2022)
    .ExecuteDelete();
ExecuteUpdate类似,ExecuteDelete将在后台生成DELETE SQL语句 — 在这种情况下,生成的SQL语句如下:
DELETE FROM [u]
FROM [Users] AS [u]
WHERE DATEPART(year, [u].[LastActiveAt]) < 2022

其他注意事项:

  • 请记住,ExecuteUpdateExecuteDelete都是“终止”方法,这意味着一旦调用该方法,更新/删除操作将立即执行。您不应该在此之后调用dbContext.SaveChanges()
  • 如果您对SetProperty方法感到好奇,并且对于为什么ExectueUpdate没有使用成员初始化表达式(例如.ExecuteUpdate(new User { Email = "..." })),则请参考GitHub问题的此评论(以及周围的评论)。
  • 此外,如果您对命名背后的原理以及为什么选择前缀Execute(还有其他候选项)感到好奇,请参阅此评论和前面(相当长的)对话。
  • 这两个方法也有异步等效物,分别命名为ExecuteUpdateAsyncExecuteDeleteAsync

很不幸,这在表格-每种类型映射策略上无法工作。“TPT映射为层次结构的'ExecuteDelete'/'ExecuteUpdate'操作是不受支持的。请参见https://go.microsoft.com/fwlink/?linkid=2101038获取更多信息。” - pkatsourakis

30
在 Entity Framework Core 中,Attach 方法会返回实体的入口对象,因此你只需要这样做:
var user = new User { Id = userId, Password = password };
db.Users.Attach(user).Property(x => x.Password).IsModified = true;
db.SaveChanges();

反之,使用Entry意味着附加实体。 - Joerg Krause

23
你基本上有两个选择: - 如果你想完全遵循EF的方式,那么你需要: - 根据提供的`userId`加载对象——整个对象都会被加载 - 更新`password`字段 - 使用上下文的`.SaveChanges()`方法将对象保存回去 在这种情况下,EF会详细处理这个流程。我刚刚测试了一下,如果我只改变一个对象的单个字段,EF创建的东西几乎与你手动创建的东西相同,大概是这样的: ```html

In this case, it's up to EF how to handle this in detail. I just tested this, and in the case I only change a single field of an object, what EF creates is pretty much what you'd create manually, too - something like:

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

EF聪明到足以找出哪些列已经被更改,它将创建一个T-SQL语句来处理那些确实必要的更新。

  • 你可以定义一个存储过程,只在T-SQL代码中执行给定UserIdPassword列的更新操作(基本上是执行UPDATE dbo.Users SET Password = @Password WHERE UserId = @UserId)然后在你的EF模型中为该存储过程创建一个函数导入,并且调用这个函数,而不是执行上述步骤。

1
@marc-s 实际上,您不必加载整个对象! - Saber
2
还需要解释一下为什么您“不必”加载它吗?这是如何完成的? - Joseph

14

我正在使用以下代码:

实体(entity):

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'的实体失败,因为另一个具有相同主键值的实体已经存在。当在图中使用“Attach”方法或将实体的状态设置为“Unchanged”或“Modified”时,如果任何实体具有冲突的关键字值,则可以发生这种情况。这可能是因为一些实体是新的,并且尚未收到数据库生成的关键字值。" - Lucian Bumb
完美!检查我的答案,看看适用于任何模型的灵活方法! - 697

13

在寻找解决办法时,我发现了GONeale答案的一个变体,来自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();
}
"如你所见,此方法的第二个参数需要一个函数表达式。这样可以通过指定 Lambda 表达式来更新要更新的属性。"
...Update(Model, d=>d.Name);
//or
...Update(Model, d=>d.Name, d=>d.SecondProperty, d=>d.AndSoOn);

我正在自己的代码中使用的一种类似的解决方案,扩展了处理 ExpressionType.Convert 类型的(Linq)表达式。在我的情况下,这是必需的,例如对于 Guid 和其他对象属性。这些属性被“包装”在 Convert() 中,因此无法被 System.Web.Mvc.ExpressionHelper.GetExpressionText 处理。

( 这里也提供了一个类似的解决方案:https://dev59.com/1FbTa4cB1Zd3GeqP-3V2#5749469)

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 entity, params Expression<Func<T, object>>[] properties) 注意在表达式前面有params关键字。 - dalcam

8
在 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操作,并在需要时自动附加实体(来自文档):

使用给定的主键值查找实体。如果上下文正在跟踪具有给定主键值的实体,则立即返回该实体,而无需向数据库发送请求。否则,将向数据库查询具有给定主键值的实体,并将找到的此实体附加到上下文并返回。如果未找到实体,则返回null。


1
根据我的观察,执行 .Find() 仍会返回包含所有数据字段的实体。因此,如果您想要更新一个字段而不是查询整个实体,这并不是解决方案。 - René Sackers

7

我来晚了,但这是我正在做的事情。我花了一段时间寻找一个令我满意的解决方案。这将只为更改的字段生成一个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>中使用它,那就太好了。 - GONeale
3
@GONeale - 只是路过。有人使用Lambda函数解决了这个问题!(https://dev59.com/1FbTa4cB1Zd3GeqP-3V2#5749469) - David Spence
1
@Elisa 可以通过使用 Func<T, List<object>> 而不是 string[] 来改进。 - Spongebob Comrade
即使是在游戏后期,也许这是更近期的语法,但在循环中使用var entity=_context.Set<T>().Attach(item);,然后跟着entity.Property(propertyName).IsModified = true;应该可以工作。 - Auspex

6

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

在这种情况下,视图应该是什么样子? - Emanuela Colta
这是错误的,因为它会保存整个用户对象和更改后的密码。 - amuliar
这是正确的,但是在上下文中,用户对象的其余部分将与以前完全相同,唯一可能不同的是密码,因此实质上只更新了密码。 - Tomislav3008

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,