【问题标题】:Upsert in generic EF core repository在通用 EF 核心存储库中更新插入
【发布时间】:2022-12-10 09:52:27
【问题描述】:

虽然感觉这个问题应该已经回答了很多次,但我找不到任何有用的东西(除了我想避免的 BulkInsert 插件)

        public virtual void AddRange(ICollection<T> entries)
        {
            _context.Set<T>().AddRange(entries);
        }

        public virtual void UpdateRange(ICollection<T> entries)
        {
            _context.Set<T>().UpdateRange(entries);
        }

那么如何将这两种方法结合在一起呢? T 是一个类并且有一个键“Id”属性(或者它可以有其他复合键,因此我希望这个解决方案是真正通用的),但我不想从中创建一个接口(检查是否等于到 0 以将条目标记为已添加,否则已修改),因为它会使设计复杂化。

【问题讨论】:

  • DbSet 已经是一个通用的单一实体存储库。 DbContext 已经是一个多实体存储库工作统一体。除非您想覆盖现有行为,否则您不会明确告诉 EF Core 分离的对象是新的还是修改过的。 ORM 旨在给人一种使用内存中对象而不是表和行的印象。其中最先进的不需要“upsert”——这甚至不是存储库模式,它是更原始的数据访问对象模式
  • Add/AddRangeUpdate/UpdateAsync 都没有向数据库写入任何内容。他们在特定状态下附加一个分离的实体。如果实体具有数据库生成的密钥,Update 将开始跟踪具有处于修改状态的密钥和没有处于已添加状态的密钥的实体。无需调用Add/AddRange,除非应用程序想要使用客户端生成的密钥附加新实体。调用SaveChanges 时将保存所有更改
  • if equals to 0 to mark entry as Added, Modified otherwiseEF 已经这样做了
  • You don't to tell EF Core explicitly whether a detached object is new or modified - 好的,让我们考虑一下:我有一个名为 Onion 的表。我有一份要插入到这张桌子上的洋葱清单。其中一些已经存在于数据库中——我必须为这些调用更新。其中一些是新的 - 我必须为这些调用添加。如何让 EF 自行决定是调用 Update 还是 Insert 而不是手动调用?
  • 那是错误的心智模型。您将 EF 视为数据访问对象或数据库连接。 EF Core 已经提供了您所要求的内容。我已经发布了解释 EF 如何决定的文档的链接在什么状态下跟踪对象- 不是调用什么方法。您可以对新对象和修改后的对象使用 Update。如果键是数据库生成的,则没有键值的对象被认为是新的。具有键值的对象被认为已修改

标签: c# .net generics entity-framework-core upsert


【解决方案1】:

由于您想执行“BulkInsert”,因此您可以使用DbContext 提供的UpdateRange(IEnumerable items) 方法。因此,在您的情况下,您将在存储库中创建一个新方法并执行:

public void Upsert<T>(IEnumerable<T> items)
{

    _context.DbSet<T>.UpdateRange(items);
    _context.SaveChanges(); // persist to db
}

然后你可以像这样使用它(伪代码)

var myRepository = new repository<Onion>(_context);
var entities = _context.Onions;
foreach(var entity in entities){
   entity.Layers = 8
}
myRepository.Upsert(entities);
// you could do SaveChanges() here too.

如果你想做一些自定义的事情,你可以使用下面的,但它不如 UpdateRange 高效,因为它对传入的集合中的每个项目进行查询。

public void AddOrUpdateRange<T>(ICollection<T> items)
{
    foreach (var item in items)
    {
        var existingItem = _context.Set<T>().Find(item.Id);
        if (existingItem != null)
        {
            _context.Entry(existingItem).CurrentValues.SetValues(item);
        }
        else
        {
            _context.Set<T>().Add(item);
        }
    }
    _context.SaveChanges();
}

【讨论】:

  • 如果尝试更新数据库中不存在的条目,UpdateRange 将给出以下错误The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded
  • 该错误表明您尚未从数据库中提取行。确保在尝试更新之前检索现有行。
  • 它也可能与:github.com/dotnet/efcore/issues/4073有关。
  • 我没有从数据库中提取任何内容,我需要插入或更新它们。并让 EF 自己决定是添加还是更新,而不是手动检查。因此问题
  • 好吧,首先要检查是否存在某些东西,您需要先检查查询它,对吗?
【解决方案2】:

所有好的对话/cmets。我有一个类似的 Upsert 操作场景,但我必须让我的回购处理它。我试图修改它以满足您的需要并像下面这样出现。

public async Task<bool> UpsertOnions(IEnumerable<Onions> list)
{
    var existingRecs = (await _onionRepo.Get(existing => list.Select(model => model.Pk).Contains(existing.Pk)).ToList();
    if (!existingRecs.Any())
        return await _onionRepo.UpdateRange(existingRecs);
    
    var allNetNew = list.Except(existingRecs);
    return await _onionRepo.AddRange(allNetNew);
}

在这里,我假设当您获取数据并映射到所需的数据模型时,具有自己的 openId 的外部数据应该与模型的 Pk 字段相关联。

我也试图在不考虑特定数据库提供商的情况下从一般方面发言。就像 Postgres 或 SQL,您的数据模型只会将您的 Pk 属性装饰为 [Key]

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-02-08
    • 2018-05-21
    • 2013-12-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多