如何使用Entity Framework 6更新记录?

324

我正在尝试使用EF6更新记录。首先查找记录,如果存在,则进行更新。 以下是我的代码:

var book = new Model.Book
{
    BookNumber =  _book.BookNumber,
    BookName = _book.BookName,
    BookTitle = _book.BookTitle,
};
using (var db = new MyContextDB())
{
    var result = db.Books.SingleOrDefault(b => b.BookNumber == bookNumber);
    if (result != null)
    {
        try
        {
            db.Books.Attach(book);
            db.Entry(book).State = EntityState.Modified;
            db.SaveChanges();
        }
        catch (Exception ex)
        {
            throw;
        }
    }
}

每次我尝试使用上述代码更新记录时,都会出现此错误:

{System.Data.Entity.Infrastructure.DbUpdateConcurrencyException: 存储更新、插入或删除语句影响了意外数量的行数 (0)。自实体加载以来,实体可能已被修改或删除。刷新ObjectStateManager条目


9
附注:catch (Exception ex){throw;}是多余的,你完全可以将其删除。 - Sriram Sakthivel
1
try catch 块只是为了找出失败的原因。但我仍然不明白为什么这段代码会失败? - user1327064
2
我不是这个主题的专家,无法回答这个问题。但是即使没有try catch,您也可以使用“抛出异常时中断调试器”功能(http://msdn.microsoft.com/en-us/library/d14azbfh.aspx)来在发生异常时中断调试器。 - Sriram Sakthivel
1
你没有改变任何东西。玩弄实体状态不会改变对象实际上并未被修改的事实。 - Jonathan Allen
1
好的,我和你做了一样的事情,但是我没有出现错误。异常提示是DbUpdateConcurrencyException。你如何处理并发?你使用了时间戳吗?你是先克隆再合并对象还是使用了自跟踪实体呢?(这是三种最常用的方法)。如果你没有处理并发,那么我想这就是问题所在了。 - El Mac
显示剩余4条评论
20个回答

439

你试图更新记录(对我来说这意味着“更改现有记录上的值并将其保存回去”)。因此,你需要检索对象,进行更改并保存它。

using (var db = new MyContextDB())
{
    var result = db.Books.SingleOrDefault(b => b.BookNumber == bookNumber);
    if (result != null)
    {
        result.SomeValue = "Some new value";
        db.SaveChanges();
    }
}

27
赋值操作不会更新数据库,需要在上下文中调用 db.SaveChanges() 来更新修改后的对象到数据库。 - Craig W.
12
仍然让我着迷...所以var result实际上与dbcontext连接在一起...这意味着由任何dbcontext成员实例化的任何变量都将与数据库相关联,因此对该变量进行的任何更改也会被应用或持久化? - vvavepacket
10
由于上下文生成了对象,所以上下文可以跟踪该对象,包括对该对象的更改。当您调用 SaveChanges 时,上下文会评估其正在跟踪的所有对象,以确定它们是已添加、已更改还是已删除,并向连接的数据库发出相应的 SQL。 - Craig W.
3
我遇到了同样的问题-使用EF6,尝试更新一个实体。Attach + EntityState.Modified不起作用。唯一有效的方法是-您需要检索对象,进行所需更改,然后通过db.SaveChanges()保存它;。 - Gurpreet Singh
13
更新对象时不需要先检索对象。我曾经也遇到过同样的问题,直到我意识到我试图更改其中一个主键值(复合键)。只要提供正确的主键,您可以将实体状态设置为已修改,SaveChanges() 就会起作用,前提是您没有违反表上定义的其他完整性约束。 - adrianz
显示剩余7条评论

238

我一直在审查Entity Framework的源代码,发现了一种实际上可以更新实体的方法,只要你知道关键属性:

public void Update<T>(T item) where T: Entity
{
    // assume Entity base class have an Id property for all items
    var entity = _collection.Find(item.Id);
    if (entity == null)
    {
        return;
    }

    _context.Entry(entity).CurrentValues.SetValues(item);
}

否则,请查看AddOrUpdate的实现以获取想法。
希望这可以帮助你!

21
好的!不需要列举所有属性。我假设在设置完值之后需要调用 SaveChanges() 方法。 - Jan Zahradník
6
是的,在调用SaveChanges()后,更改将被持久化。 - Miguel
1
非常好的答案,IntelliSense并不清楚像这样做是行不通的:_context.MyObj = newObj; 然后 SaveChanges() 或者.... _context.MyObj.Update(newObj) 然后 SaveChanges(); 您的解决方案更新整个对象,而无需循环遍历所有属性。 - Adam
13
这个向我抱怨说我正在尝试编辑ID字段。 - Vasily Hall
6
如果模型中的ID字段(或您定义为主键的任何内容)不同(包括在其中一个模型中为null / 0),则会出现此问题。确保两个模型之间的ID匹配,它就可以正常更新了。 - Gavin Coates
显示剩余12条评论

53

您可以使用AddOrUpdate方法:

db.Books.AddOrUpdate(book); //requires using System.Data.Entity.Migrations;
db.SaveChanges();

1
IMO最佳解决方案 - Norgul
155
.AddOrUpdate() 方法用于数据库迁移,强烈不建议在迁移之外使用此方法,因此它位于 Entity.Migrations 命名空间中。 - Adam Vincent
我对这个AddOrUpdate代码进行了反向工程,并将结果发布在我的另一个答案中。 - Miguel
2
正如@AdamVincent所说,AddOrUpdate()方法是用于迁移的,不适用于仅需要更新现有行的情况。如果您没有带有搜索引用(即ID)的书,它将创建新行,并且在某些情况下可能会出现问题(例如,您有一个API,如果尝试为不存在的行调用PUT方法,则需要返回404-NotFound响应)。 - Marko
7
除非你知道自己在做什么,否则请不要使用这个功能!请阅读以下文章:https://www.michaelgmccarthy.com/2016/08/24/entity-framework-addorupdate-is-a-destructive-operation/。 - Yusha
6
今天我又回到了这个问题,我要警告大家,这并不是期望应用场景下的好解决方案。 - Yusha

32

所以你有一个需要更新的实体,并且希望以最少的代码将其更新到数据库中...

并发性总是棘手的,但我假设你只想让你的更新获胜。以下是我在相同情况下的解决方法,并修改名称以模仿你的类。换句话说,只需将attach更改为add,它就可以为我工作:

public static void SaveBook(Model.Book myBook)
{
    using (var ctx = new BookDBContext())
    {
        ctx.Books.Add(myBook);
        ctx.Entry(myBook).State = System.Data.Entity.EntityState.Modified;
        ctx.SaveChanges();
    }
}

20

如果您希望更新对象中的所有字段,则应使用Entry()方法。同时请记住,您不能更改字段ID(键),因此首先将ID设置为与要编辑的相同。

using(var context = new ...())
{
    var EditedObj = context
        .Obj
        .Where(x => x. ....)
        .First();

    NewObj.Id = EditedObj.Id; //This is important when we first create an object (NewObj), in which the default Id = 0. We can not change an existing key.

    context.Entry(EditedObj).CurrentValues.SetValues(NewObj);

    context.SaveChanges();
}

3
你至少应该尝试回答问题,而不仅仅是发布代码。 - user1143634
请不要只留下代码片段,而是对问题进行一些解释,以便更好地帮助提问者。 - feanor07

19

Attach方法会将实体的跟踪状态设置为Unchanged。要更新现有实体,您只需要将跟踪状态设置为Modified。根据EF6文档

If you have an entity that you know already exists in the database but to which changes may have been made then you can tell the context to attach the entity and set its state to Modified. For example:

var existingBlog = new Blog { BlogId = 1, Name = "ADO.NET Blog" };

using (var context = new BloggingContext())
{
    context.Entry(existingBlog).State = EntityState.Modified;

    // Do some more work...  

    context.SaveChanges();
}

谢谢。这对我来说是完美的解决方案,因为它可以节省大量更新对象属性的代码行数。当模型更新时,我们还需要更新控制器,这不是 EF 应该处理的事情。 - Zaheer

10
我找到了一个行之有效的方法。
 var Update = context.UpdateTables.Find(id);
        Update.Title = title;

        // Mark as Changed
        context.Entry(Update).State = System.Data.Entity.EntityState.Modified;
        context.SaveChanges();

9

这段代码是进行更新操作的测试结果,它只更新了一组列而不需要先查询记录。它使用了Entity Framework 7的Code First。

// This function receives an object type that can be a view model or an anonymous 
// object with the properties you want to change. 
// This is part of a repository for a Contacts object.

public int Update(object entity)
{
    var entityProperties =  entity.GetType().GetProperties();   
    Contacts con = ToType(entity, typeof(Contacts)) as Contacts;

    if (con != null)
    {
        _context.Entry(con).State = EntityState.Modified;
        _context.Contacts.Attach(con);

        foreach (var ep in entityProperties)
        {
            // If the property is named Id, don't add it in the update. 
            // It can be refactored to look in the annotations for a key 
            // or any part named Id.

            if(ep.Name != "Id")
                _context.Entry(con).Property(ep.Name).IsModified = true;
        }
    }

    return _context.SaveChanges();
}

public static object ToType<T>(object obj, T type)
{
    // Create an instance of T type object
    object tmp = Activator.CreateInstance(Type.GetType(type.ToString()));

    // Loop through the properties of the object you want to convert
    foreach (PropertyInfo pi in obj.GetType().GetProperties())
    {
        try
        {
            // Get the value of the property and try to assign it to the property of T type object
            tmp.GetType().GetProperty(pi.Name).SetValue(tmp, pi.GetValue(obj, null), null);
        }
        catch (Exception ex)
        {
            // Logging.Log.Error(ex);
        }
    }
    // Return the T type object:         
    return tmp;
}

以下是完整的代码:
public interface IContactRepository
{
    IEnumerable<Contacts> GetAllContats();
    IEnumerable<Contacts> GetAllContactsWithAddress();
    int Update(object c);
}

public class ContactRepository : IContactRepository
{
    private ContactContext _context;

    public ContactRepository(ContactContext context)
    {
        _context = context;
    }

    public IEnumerable<Contacts> GetAllContats()
    {
        return _context.Contacts.OrderBy(c => c.FirstName).ToList();
    }

    public IEnumerable<Contacts> GetAllContactsWithAddress()
    {
        return _context.Contacts
            .Include(c => c.Address)
            .OrderBy(c => c.FirstName).ToList();
    }   

    //TODO Change properties to lambda expression
    public int Update(object entity)
    {
        var entityProperties = entity.GetType().GetProperties();

        Contacts con = ToType(entity, typeof(Contacts)) as Contacts;

        if (con != null)
        {
            _context.Entry(con).State = EntityState.Modified;
            _context.Contacts.Attach(con);

            foreach (var ep in entityProperties)
            {
                if(ep.Name != "Id")
                    _context.Entry(con).Property(ep.Name).IsModified = true;
            }
        }

        return _context.SaveChanges();
    }

    public static object ToType<T>(object obj, T type)
    {
        // Create an instance of T type object
        object tmp = Activator.CreateInstance(Type.GetType(type.ToString()));

        // Loop through the properties of the object you want to convert
        foreach (PropertyInfo pi in obj.GetType().GetProperties())
        {
            try
            {
                // Get the value of the property and try to assign it to the property of T type object
                tmp.GetType().GetProperty(pi.Name).SetValue(tmp, pi.GetValue(obj, null), null);
            }
            catch (Exception ex)
            {
                // Logging.Log.Error(ex);
            }
        }
        // Return the T type object
        return tmp;
    }
}    

public class Contacts
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public string Company { get; set; }
    public string Title { get; set; }
    public Addresses Address { get; set; }    
}

public class Addresses
{
    [Key]
    public int Id { get; set; }
    public string AddressType { get; set; }
    public string StreetAddress { get; set; }
    public string City { get; set; }
    public State State { get; set; }
    public string PostalCode { get; set; }  
}

public class ContactContext : DbContext
{
    public DbSet<Addresses> Address { get; set; } 
    public DbSet<Contacts> Contacts { get; set; } 
    public DbSet<State> States { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        var connString = "Server=YourServer;Database=ContactsDb;Trusted_Connection=True;MultipleActiveResultSets=true;";
        optionsBuilder.UseSqlServer(connString);
        base.OnConfiguring(optionsBuilder);
    }
}

9

针对 .net core

context.Customer.Add(customer);
context.Entry(customer).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
context.SaveChanges();

1
使用此功能发送适当的更新还是发送所有属性?假设我有一个具有10Mb文本属性的记录。每次更新另一个属性时,它会将其发送到数据库吗? - Toolkit

5

以下是此问题的最佳解决方案:在View中添加所有ID(键)。考虑有多个名为(First,Second和Third)的表。

@Html.HiddenFor(model=>model.FirstID)
@Html.HiddenFor(model=>model.SecondID)
@Html.HiddenFor(model=>model.Second.SecondID)
@Html.HiddenFor(model=>model.Second.ThirdID)
@Html.HiddenFor(model=>model.Second.Third.ThirdID)

在 C# 代码中,
[HttpPost]
public ActionResult Edit(First first)
{
  if (ModelState.Isvalid)
  {
    if (first.FirstID > 0)
    {
      datacontext.Entry(first).State = EntityState.Modified;
      datacontext.Entry(first.Second).State = EntityState.Modified;
      datacontext.Entry(first.Second.Third).State = EntityState.Modified;
    }
    else
    {
      datacontext.First.Add(first);
    }
    datacontext.SaveChanges();
    Return RedirectToAction("Index");
  }

 return View(first);
}

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