如何使用实体框架核心更新记录?

在实体框架核心中更新数据库表数据的最佳方法是什么?

  1. 检索表行,执行更改并保存
  2. 在 DB 上下文中使用关键字 更新并处理不存在项的异常

在 EF6之上,我们可以使用哪些改进的特性?

269370 次浏览

要使用实体框架核心更新实体,这是一个逻辑过程:

  1. DbContext类创建实例
  2. 按键检索实体
  3. 对实体的属性进行更改
  4. 保存更改

DbContext中的 Update()方法:

开始跟踪处于修改状态的给定实体,以便在调用 SaveChanges()时在数据库中对其进行更新。

Update 方法不在数据库中保存更改; 而是为 DbContext 实例中的条目设置状态。

因此,我们可以先调用 Update()方法来保存数据库中的更改。

我将假设一些对象定义来回答您的问题:

  1. 数据库名称为 Store

  2. 表名为 Product

产品类别定义:

public class Product
{
public int? ProductID { get; set; }
    

public string ProductName { get; set; }
    

public string Description { get; set; }
    

public decimal? UnitPrice { get; set; }
}

DbContext 类定义:

public class StoreDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
    

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Your Connection String");


base.OnConfiguring(optionsBuilder);
}
    

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>(entity =>
{
// Set key for entity
entity.HasKey(p => p.ProductID);
});
        

base.OnModelCreating(modelBuilder);
}
}

更新实体的逻辑:

using (var context = new StoreDbContext())
{
// Retrieve entity by id
// Answer for question #1
var entity = context.Products.FirstOrDefault(item => item.ProductID == id);
        

// Validate entity is not null
if (entity != null)
{
// Answer for question #2


// Make changes on entity
entity.UnitPrice = 49.99m;
entity.Description = "Collector's edition";
            

/* If the entry is being tracked, then invoking update API is not needed.
The API only needs to be invoked if the entry was not tracked.
https://www.learnentityframeworkcore.com/dbcontext/modifying-data */
// context.Products.Update(entity);
            

// Save changes in database
context.SaveChanges();
}
}

根据 微软文档:

Read-first 方法需要额外的数据库读取,并且可能导致处理并发冲突的代码更加复杂

但是,您应该知道,在 DbContext 上使用 Update 方法将把所有字段标记为 修改过的,并将它们全部包含在查询中。如果要更新字段的子集,应该使用 Attach 方法,然后手动将所需的字段标记为 修改过的

context.Attach(person);
context.Entry(person).Property(p => p.Name).IsModified = true;
context.SaveChanges();

Microsoft Docs 给了我们两种方法。

推荐 HttpPost 编辑代码: 阅读和更新

这与我们在以前的实体框架版本中使用的老方法相同。这就是微软对我们的建议。

好处

  • 预防 职位过多
  • EFs 自动更改跟踪在通过表单输入更改的字段上设置 Modified标志。

替代 HttpPost 编辑代码: 创建和附加

另一种方法是将模型绑定器创建的实体附加到 EF 上下文,并将其标记为已修改。

正如在另一个答案中提到的,先读取方法需要额外的数据库读取,并且可能导致处理并发冲突的代码更加复杂。

public async Task<bool> Update(MyObject item)
{
Context.Entry(await Context.MyDbSet.FirstOrDefaultAsync(x => x.Id == item.Id)).CurrentValues.SetValues(item);
return (await Context.SaveChangesAsync()) > 0;
}

超级简单

using (var dbContext = new DbContextBuilder().BuildDbContext())
{
dbContext.Update(entity);
await dbContext.SaveChangesAsync();
}

在看完所有的答案后,我想我会添加两个简单的选项

  1. 如果您已经使用 FirstOrDefault ()访问记录,并且启用了跟踪功能(不使用。函数,因为它将禁用跟踪)并更新一些字段,然后您可以简单地调用上下文。保存更改()

  2. 在其他情况下,您可以使用 HtppPost 将实体发布到服务器,或者由于某种原因禁用了跟踪,那么您应该调用上下文。在上下文之前更新(entityName)。保存更改()

第一个选项将只更新您更改的字段,但第二个选项将更新数据库中的所有字段,即使实际上没有任何字段值被更新:)

一种更通用的方法

为了简化这种方法,使用了一个“ id”接口

public interface IGuidKey
{
Guid Id { get; set; }
}

Helper 方法

public static void Modify<T>(this DbSet<T> set, Guid id, Action<T> func)
where T : class, IGuidKey, new()
{
var target = new T
{
Id = id
};
var entry = set.Attach(target);
func(target);
foreach (var property in entry.Properties)
{
var original = property.OriginalValue;
var current = property.CurrentValue;


if (ReferenceEquals(original, current))
{
continue;
}


if (original == null)
{
property.IsModified = true;
continue;
}


var propertyIsModified = !original.Equals(current);
property.IsModified = propertyIsModified;
}
}

用法

dbContext.Operations.Modify(id, x => { x.Title = "aaa"; });

我个人认为,您正在做的操作是一个向上操作,如果数据已经存在,我们更新,否则我们插入。Flexlab 有一个很好的库支持这种语法的 Upsert 操作

var country = new Country
{
Name = "Australia",
ISO = "AU",
Created = DateTime.UtcNow,
};




await DataContext.Upsert(country)
.On(c => c.ISO)
.UpdateColumns(c => new Country
{
Name = "Australia"
Updated = DateTime.UtcNow,
})
.RunAsync();

代码将检查表中的属性 Country.ISO。如果它还不存在,它会将新行完全插入到表中。否则,如果任何具有相同 Country.ISO的行已经存在,它将更新该行的 Country.NameCountry.Updated列。

这个方法非常快,因为在更新或插入数据之前,我们只对数据库执行一次调用,而不是两次调用来检查数据是否已经存在。

请注意,如果您不打算执行 Upsert 操作,则此答案不适用于您

假设我们有一个实体 StudentAppDbContext,如下所示。

class Student
{
public int Id { get; set; }
public string Name { get; set; } = default!;
public int Age { get; set; }
}




public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> opts) : base(opts) { }


public DbSet<Student> Students { get; set; }
}

版本 A

  • CurrentValues只能为被跟踪的实体(found)工作。
  • 只有更改后的属性被标记为 Modified
  • 自动属性映射,这在使用类型参数 TEntity而不是固定类型 Student时非常有用。
async Task Edit_A(int id, Student incoming, AppDbContext db)
{
if (await db.Students.FindAsync(id) is Student found)
{
db.Entry(found).CurrentValues.SetValues(incoming);


await db.SaveChangesAsync();
}
}

版本 B

  • 它只对被跟踪的实体(found)有效。
  • 不需要映射所有属性,因为只有更改后的属性被标记为 Modified
  • 手动属性映射,因此我们不能使用泛型类型参数。
async Task Edit_B(int id, Student incoming, AppDbContext db)
{
if (await db.Students.FindAsync(id) is Student found)
{
found.Name = incoming.Name;
found.Age = incoming.Age;
        



await db.SaveChangesAsync();
}
}

版本 C

  • Update()仅对未跟踪的实体(incoming)起作用,并使其被跟踪。在调用 Update(incoming)之前取消跟踪 found是强制性的,因为只能用给定的主键跟踪一个实体。
  • 所有属性(包括未更改的属性)都被标记为 Modified。它的效率较低。
  • 对泛型类型参数有用的自动属性映射。
async Task Edit_C(int id, Student incoming, AppDbContext db)
{
if (await db.Students.FindAsync(id) is Student found)
{
db.Students.Entry(found).State = EntityState.Detached;
db.Students.Update(incoming);
await db.SaveChangesAsync();
}
}

版本 D

  • 它与 C 版本相同,为了完整起见,我在下面重写了一遍。
  • 它只对未跟踪的实体(incoming)起作用,并使其被跟踪。取消跟踪 found是强制性的,因为只能用给定的主键跟踪一个实体。
  • 所有属性(包括未更改的属性)都被标记为 Modified。它的效率较低。
  • 对泛型类型参数有用的自动属性映射。
async Task Edit_D(int id, Student incoming, AppDbContext db)
{
if (await db.Students.FindAsync(id) is Student found)
{
db.Students.Entry(found).State = EntityState.Detached;
db.Students.Entry(incoming).State = EntityState.Modified;
await db.SaveChangesAsync();
}
}

版本 E

  • 它只对未跟踪的实体(incoming)起作用,并使其被跟踪。取消跟踪 found是强制性的,因为只能用给定的主键跟踪一个实体。
  • 不需要映射所有属性,因为只有标记为 IsModified=true的属性(包括未更改的属性)才会更新。如果将 IsModified=true标记为未更改的属性,则效率较低。
  • 手动属性映射,因此我们不能使用泛型类型参数。
async Task Edit_E(int id, Student incoming, AppDbContext db)
{
if (await db.Students.FindAsync(id) is Student found)
{
db.Students.Entry(found).State = EntityState.Detached;
db.Students.Entry(incoming).Property(s => s.Name).IsModified = true;
db.Students.Entry(incoming).Property(s => s.Age).IsModified = true;
await db.SaveChangesAsync();
}
}

我把它设置为一个社区维基,你想编辑多少都可以。