Entity Framework 5 更新记录

896

我一直在探索在ASP.NET MVC3环境中使用Entity Framework 5编辑/更新记录的不同方法,但到目前为止,还没有一个方法满足我所有需求。我会解释原因。

我发现了三种方法,它们各有优缺点:

方法1 - 加载原始记录,更新每个属性

var original = db.Users.Find(updatedUser.UserId);

if (original != null)
{
    original.BusinessEntityId = updatedUser.BusinessEntityId;
    original.Email = updatedUser.Email;
    original.EmployeeId = updatedUser.EmployeeId;
    original.Forename = updatedUser.Forename;
    original.Surname = updatedUser.Surname;
    original.Telephone = updatedUser.Telephone;
    original.Title = updatedUser.Title;
    original.Fax = updatedUser.Fax;
    original.ASPNetUserId = updatedUser.ASPNetUserId;
    db.SaveChanges();
}    

优点

  • 可以指定更改的属性
  • 视图不需要包含每个属性

缺点

  • 需要在数据库中查询两次,一次加载原始记录,一次更新它

方法2 - 加载原始记录,设置更改的值

var original = db.Users.Find(updatedUser.UserId);

if (original != null)
{
    db.Entry(original).CurrentValues.SetValues(updatedUser);
    db.SaveChanges();
}

优点

  • 只有修改的属性被发送到数据库。

缺点

  • 视图需要包含所有属性。
  • 需要在数据库上进行2次查询,以加载原始记录并更新它。

方法3 - 附加更新的记录并将状态设置为EntityState.Modified。

db.Users.Attach(updatedUser);
db.Entry(updatedUser).State = EntityState.Modified;
db.SaveChanges();

优点

  • 只需一次数据库查询来更新

缺点

  • 无法指定更改的属性
  • 视图必须包含每个属性

问题

我的问题是:有没有一种简洁的方法可以实现以下目标?

  • 能够指定更改的属性
  • 视图不需要包含每个属性(比如密码!)
  • 只需一次数据库查询来更新

我知道这可能只是一个小问题,但我可能错过了一个简单的解决方案。如果没有,那么就采用第一种方法吧 ;-)


13
使用ViewModels和良好的映射引擎,你只需要"更新属性"来填充视图(然后进行更新)。仍然需要进行两次查询以进行更新(获取原始数据+更新),但我不认为这是一个"缺点"。如果这是您唯一的性能问题,那么您是一个幸福的人 ;) - Raphaël Althaus
3
在我的当前项目中(涉及许多实体),我们最初开始通过处理模型来工作,认为使用ViewModels会浪费时间。现在我们转向使用ViewModels,并且在一开始进行了(不可忽略的)基础设施工作之后,现在维护起来要清晰、容易得多。而且更加安全(不需要担心恶意“隐藏字段”或类似的问题)。 - Raphaël Althaus
1
不再需要(可怕的)ViewBags来填充您的DropDownLists了(我们几乎在所有CRU(D)视图上都有至少一个DropDownList...)。 - Raphaël Althaus
“方法1”存在惰性加载问题,可能会导致很多意外的事务和极差的性能。 - Amirhossein Mehrvarzi
问题的根源在于,由于它们是 POCO,因此在设置属性时更新的对象上的属性不会被标记为脏。相比之下,例如 LLBLGen 会为您生成实体,当您设置其值时,它们会自动将字段标记为脏。 - Ian Warburton
显示剩余7条评论
9个回答

688

你正在寻找:

db.Users.Attach(updatedUser);
var entry = db.Entry(updatedUser);
entry.Property(e => e.Email).IsModified = true;
// other changed properties
db.SaveChanges();

62
你好 @Ladislav Mrnka,如果我想一次性更新所有属性,我可以使用以下代码吗? db.Departments.Attach(department); db.Entry(department).State = EntityState.Modified;
db.SaveChanges();
- Foyzul Karim
5
这种方法的问题之一是你无法对db.Entry()进行模拟,这是一个严重的麻烦。在其他方面,EF有一个相当不错的模拟故事,但令人恼火的是(据我所知)他们在这里没有之一。 - Ken Smith
24
@Foysal 只需要执行 context.Entry(entity).State = EntityState.Modified 就足够了,无需进行 attach 操作。实体已经被修改过,它会自动被附加上去。 - HelloWorld
4
@Sandman4 的意思是其他属性也需要存在并设置为当前值。在某些应用设计中,这可能行不通。 - Dan Esparza
3
"EF有一个相当不错的模拟故事" - 为什么现在所有的东西都要变成一个故事呢? - Ian Warburton
显示剩余6条评论

176

我真的很喜欢被采纳的答案。 我相信还有另一种方法可以处理这个问题。 假设您有一个非常短的属性列表,您不希望在视图中包含其中的任何属性,因此在更新实体时,这些属性将被省略。 假设这两个字段是密码和社会安全号码。

db.Users.Attach(updatedUser);

var entry = db.Entry(updatedUser);
entry.State = EntityState.Modified;

entry.Property(e => e.Password).IsModified = false;
entry.Property(e => e.SSN).IsModified = false;   

db.SaveChanges();   

在向用户表(User table)和视图(View)添加新字段后,此示例允许您基本上不更改业务逻辑而保留原样。


1
即使我将IsModified设置为false,如果我不为SSN属性指定一个值,仍然会收到错误提示,因为它仍会根据模型规则验证该属性。因此,如果该属性被标记为NOT NULL,如果我没有设置任何与null不同的值,它将失败。 - RolandoCC
1
您不会收到错误,因为这些字段不会出现在您的表单中。您可以省略那些您肯定不会更新的字段,使用传回的表单从数据库中获取条目,并告诉该条目这些字段不会被修改。模型验证由ModelState控制,而不是上下文。此示例引用了现有用户,因此称为“updatedUser”。如果您的社保号码是必填字段,则在创建时应已存在。 - smd
5
如果我理解正确,"updatedUser" 是一个已经用 FirstOrDefault() 或类似方法填充的对象实例,因此我只更新了我更改的属性,并将其他属性设置为 ISModified=false。这个方法运行良好。但是,我尝试的是在不先填充对象、不做任何 FirstOrDefault() 更新它。如果我没有为所有必需字段指定值,就会收到错误提示,即使我在这些属性上设置了 ISModified=false。entry.Property(e => e.columnA).IsModified = false;如果没有这一行代码,ColumnA 将会失败。 - RolandoCC
你所描述的是创建一个新实体。这仅适用于更新。 - smd
1
在db.SaveChanges()之前加上db.Configuration.ValidateOnSaveEnabled = false;,RolandoCC。 - Wilky

28
foreach(PropertyInfo propertyInfo in original.GetType().GetProperties()) {
    if (propertyInfo.GetValue(updatedUser, null) == null)
        propertyInfo.SetValue(updatedUser, propertyInfo.GetValue(original, null), null);
}
db.Entry(original).CurrentValues.SetValues(updatedUser);
db.SaveChanges();

这似乎是一个非常好的解决方案 - 没有任何麻烦; 您不必手动指定属性,它考虑了所有 OP 的要点 - 是否有任何原因,这没有更多的投票? - Shawn J. Molloy
它并没有。它有最大的“缺点”之一,需要多次访问数据库。您仍然需要使用此答案加载原始数据。 - smd
1
@smd 为什么你说它会多次访问数据库?除非使用 SetValues() 会产生这种影响,但这似乎不是真的。 - parliament
@parliament 我想我写下这些话时可能已经睡着了,抱歉。实际问题是覆盖了一个预期为空的值。如果更新后的用户不再引用某些内容,那么将其替换为原始值是不正确的,如果您的意图是清除它的话。 - smd

25

我已经在我的存储库基类上添加了一个额外的更新方法,该方法类似于Scaffolding生成的更新方法。它不是将整个对象设置为“修改”,而是设置一组单独的属性。(T 是一个类泛型参数。)

public void Update(T obj, params Expression<Func<T, object>>[] propertiesToUpdate)
{
    Context.Set<T>().Attach(obj);

    foreach (var p in propertiesToUpdate)
    {
        Context.Entry(obj).Property(p).IsModified = true;
    }
}

接着,例如要调用:

public void UpdatePasswordAndEmail(long userId, string password, string email)
{
    var user = new User {UserId = userId, Password = password, Email = email};

    Update(user, u => u.Password, u => u.Email);

    Save();
}

我喜欢一次到数据库的旅行。然而,最好使用视图模型来完成此操作,以避免重复设置属性。我还没有这样做,因为我不知道如何避免将视图模型验证器中的验证消息带入我的领域项目。


啊哈...为视图模型单独创建一个项目,为与视图模型一起工作的存储库单独创建一个项目。 - Ian Warburton
真的很喜欢这种方法。仍然明确,但更容易理解。 - aIKid

11
public interface IRepository
{
    void Update<T>(T obj, params Expression<Func<T, object>>[] propertiesToUpdate) where T : class;
}

public class Repository : DbContext, IRepository
{
    public void Update<T>(T obj, params Expression<Func<T, object>>[] propertiesToUpdate) where T : class
    {
        Set<T>().Attach(obj);
        propertiesToUpdate.ToList().ForEach(p => Entry(obj).Property(p).IsModified = true);
        SaveChanges();
    }
}

为什么不直接使用 DbContext.Attach(obj); DbContext.Entry(obj).State = EntityState.Modified; 呢? - nelsontruran
这个控制着更新语句中的set部分。 - Tanveer Badar

4

EF Core 7.0 新特性:ExecuteUpdate

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

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

用法:

假设您想要更新特定用户的电子邮件和显示名称:

dbContext.Users
    .Where(u => u.Id == someId)
    .ExecuteUpdate(b => b
        .SetProperty(u => u.Email, "NewEmail@gmail.com")
        .SetProperty(u => u.DisplayName, "New Display Name")
    );

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

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

UPDATE [u]
    SET [u].[Email] = "NewEmail@gmail.com",
    [u].[DisplayName] = "New Display Name"
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语句 —— 在这种情况下,是以下语句:
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

1
我想我已经找到了我要找的东西。谢谢,伙计。 - Oluwadamilola Adegunwa

2

除了上述提到的选项外,您还可以从数据库中获取对象,并使用自动映射工具 Auto Mapper 更新要更改的记录部分。


2
根据您的使用情况,以上所有解决方案都适用。然而,这是我通常的做法:
对于服务器端代码(例如批处理),我通常加载实体并使用动态代理进行操作。通常在批处理中,您需要在服务运行时加载数据。我尝试批量加载数据而不是使用find方法以节省时间。根据进程,我使用乐观或悲观并发控制(除了需要使用纯sql语句锁定某些记录的并行执行场景外,我总是使用乐观并发控制,这种情况很少出现)。根据代码和场景,影响可以减少到几乎为零。
对于客户端场景,您有几个选项:
1. 使用视图模型。模型应具有UpdateStatus属性(未修改-插入-更新-删除)。客户端有责任根据用户操作(插入-更新-删除)设置正确的值到此列中。服务器可以查询原始值或客户端应将更改的行与原始值一起发送到服务器。服务器应附加原始值并使用每行的UpdateStatus列来决定如何处理新值。在这种情况下,我总是使用乐观并发控制。这只会执行插入-更新-删除语句而不会执行任何查询语句,但可能需要一些聪明的代码来遍历图形并更新实体(取决于您的情况-应用程序)。映射器可以帮助但不处理CRUD逻辑。
2. 使用像breeze.js这样的库,它隐藏了大部分此复杂性(如1中所述),并尝试将其适应您的使用情况。
希望对您有所帮助。

0

已经有一些非常好的答案了,但我想再加上我的意见。这里有一种非常简单的方法可以将视图对象转换为实体。简单的想法是只有在视图模型中存在的属性才会被写入实体。这类似于@Anik Islam Abhi的答案,但具有空值传播。

public static T MapVMUpdate<T>(object updatedVM, T original)
{
    PropertyInfo[] originalProps = original.GetType().GetProperties();
    PropertyInfo[] vmProps = updatedVM.GetType().GetProperties();
    foreach (PropertyInfo prop in vmProps)
    {
        PropertyInfo projectProp = originalProps.FirstOrDefault(x => x.Name == prop.Name);
        if (projectProp != null)
        {
            projectProp.SetValue(original, prop.GetValue(updatedVM));
        }
    }
    return original;
}

优点

  • 视图不需要具备实体的所有属性。
  • 当您添加或删除视图的属性时,无需更新代码。
  • 完全通用。

缺点

  • 对数据库有两次访问,一次是加载原始实体,另一次是保存它。

对我来说,这种方法的简单性和低维护要求胜过增加的数据库调用。


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