Entity Framework 根据复合键插入重复项

Entity Framework inserting duplicates based on a composite key

我正在尝试将大量对象列表插入我的数据库(一次大约 30,000 条记录),并根据各列的复合键判断该记录是否为重复行。这是使这一点更清楚的代码:

await {db_context}.AddRangeAsync(Metrics.Where(x =>!MetricsInDb.AsEnumerable().Any(y => 
x.CreativeId == y.CreativeId 
&& x.LineItemId == y.LineItemId 
&& x.Date.Date == y.Date.Date 
&& x.City == y.City 
&& x.Country == y.Country 
&& x.Metro == y.Metro 
&& x.State == y.State)));

为了进一步解释,我有两个列表。

Metrics 是我要插入的对象列表。
MetricsInDb 是我要比较的另一个对象列表。
.Any() 中,基本上我想说的是,如果所有这些列都匹配,那么它就是重复的。不要插入重复的行。

对我来说,逻辑似乎是合理的。我不确定是否有更好的方法在像这样的大型复合键上执行此操作。

我最初在这里有一个 .AsParallel().... Metrics.AsParallel().Where(x => !MetricsInDb.......) 我认为这是问题所在,但显然不是在运行几次后它仍在插入重复项。

任何和所有提示都会非常有用。提前致谢!

复合键给这样的操作增加了挑战,但是我看到这种方法的真正问题是:

Metrics.Where(x =>!MetricsInDb.AsEnumerable().Any( ...

这会将数据库中的所有指标记录加载到内存中。随着 table 的增长,这在性能和资源使用方面将变得不可持续。

对于较大的批量操作,我的第一选择可能是 使用 EF 而是采用过程 offline/background.

当涉及到使用 EF 的批量操作时,我使用的方法是首先将所有处理记录导入一个空的暂存区 table,然后您可以从那里执行暂存区之间的连接 table 和真实数据。登台记录可以根据复合键声明返回到度量标准行的关系。这两个实体(Metrics 和 StagingMetrics)在有界上下文中注册,上下文只知道这两个实体及其关系。

modelBuilder.Entity<StagingMetrics>()
    .HasOptional(x => x.Metric)
    .WithMany()
    .HasForeignKey(x => new 
    {
        x.CreativeId, 
        x.LineItemId, 
        x.Date.Date,
        x.City,
        x.Country,
        x.Metro,
        x.State
    });

获取要添加的项目:

var newMetrics = stagingDbContext.StagingMetrics.Where(x => x.Metric == null);

从那里我们可以将其转换回度量标准。 (它们是相同的,但需要进行转换,以便可以将其写为 Metric)

var newMetrics = stagingDbContext.StagingMetrics.Where(x => x.Metric == null)
    .Select(x => new Metric
    {
        CreativeId = x.CreativeId, 
        LineItemId = x.LineItemId, 
        Date = x.Date,
        City = x.City,
        Country = x.Country,
        Metro = x.Metro,
        State = x.State
    }).ToList();

由此,我们可以通过 StagingDbContext 或主应用程序 DbContext 添加新指标。要考虑的最后一步是截断暂存 table,这可以通过 SQL 命令完成:

stagingDbContext.Database.ExecuteSqlCommand("TRUNCATE TABLE StagingMetrics");

我会考虑在第一个 运行 进程时截断 table。

这种方法的一个考虑因素是 StagingDbContext 应该限定在这个操作范围内,以避免代码在截断之后引用 StagingMetrics table 中的行的可能性。

因此,更完整的导入方法流程可能如下所示:

using (var stagingDbContext = StagingDbContextFactory.Create())
{
    stagingDbContext.Database.ExecuteSqlCommand("TRUNCATE TABLE StagingMetrics");        

    var newMetrics = stagingDbContext.StagingMetrics.Where(x => x.Metric == null)
    .Select(x => new Metric
    {
        CreativeId = x.CreativeId, 
        LineItemId = x.LineItemId, 
        Date = x.Date,
        City = x.City,
        Country = x.Country,
        Metro = x.Metro,
        State = x.State
    }).ToList();

    if (newMetrics.Any())
    {
        AppDbContext.Metrics.AddRange(newMetrics);
        AppDbContext.SaveChanges();
    }
    // Can truncate the Staging table here as well, or leave the contents to debug if there was an issue.
}

其中 StagingDbContextFactory 是工厂 class 到 return DbContext 的新实例,AppDbContext 是注入的主应用程序 DbContext。

这不是一个可以零碎触发的过程,例如 Web 请求的一部分,因为多个调用可能会尝试在上一个调用完成之前截断暂存 table。如果这可以作为上传或类似用户请求的一部分执行,则将请求记录到后台工作人员的处理队列中,确保一次只处理一个导入。