【问题标题】:EF 4.1 Insert/Update Logic Best PracticesEF 4.1 插入/更新逻辑最佳实践
【发布时间】:2012-03-28 16:51:50
【问题描述】:

我使用 EF 4.1 插入大量数据,一次包装在一个事务中(比如 200 万行以上)。现在我想添加 UPDATE 逻辑。请记住,鉴于数据量,更改跟踪被禁用。在我的脑海中,我会做这样的事情:

// Obviously simplified code...
public void AddOrUpdate(Foo foo)
{
    if(!db.Foos.Any(x => someEqualityTest(foo)))
    {
        db.Foos.Add(foo);
    }

    else
    {
        var f = db.Foos.First(x => someEqualityTest(foo));
        f = foo;
    }

    db.SaveChanges();
}

关于如何改进这方面的任何想法?

【问题讨论】:

  • 您能否详细说明如何检查 Foo 的两个实例是否相等?是简单的ID比较吗?
  • 可能,我只是想概括具体细节,并专注于解决方案的更大逻辑方面

标签: c# asp.net .net entity-framework entity-framework-4.1


【解决方案1】:

我会将插入与更新分开。

对于插入,我建议使用SqlBulkCopy 来插入所有不存在的记录,这样会更快方式

首先,您的 DbContext 中的 Bulk Insert 方法:

public class YourDbContext : DbContext
{
    public void BulkInsert<T>(string tableName, IList<T> list)
    {
        using (var bulkCopy = new SqlBulkCopy(base.Database.Connection))
        {
            bulkCopy.BatchSize = list.Count;
            bulkCopy.DestinationTableName = tableName;

            var table = new DataTable();
            var props = TypeDescriptor.GetProperties(typeof(T))
                          // Dirty hack to make sure we only have system 
                          // data types (i.e. filter out the 
                          // relationships/collections)
                          .Cast<PropertyDescriptor>()
                          .Where(p => "System" == p.PropertyType.Namespace)
                          .ToArray();

            foreach (var prop in props)
            {
                bulkCopy.ColumnMappings.Add(prop.Name, prop.Name);

                var type = Nullable.GetUnderlyingType(prop.PropertyType) 
                           ?? prop.PropertyType;

                table.Columns.Add(prop.Name, type);
            }

            var values = new object[props.Length];
            foreach (var item in list)
            {
                for (var i = 0; i < values.Length; i++)
                {
                    values[i] = props[i].GetValue(item);
                }

                table.Rows.Add(values);
            }

            bulkCopy.WriteToServer(table);
        }
    }
}

然后,为您的插入/更新:

public void AddOrUpdate(IList<Foo> foos)
{
    var foosToUpdate = db.Foos.Where(x => foos.Contains(x)).ToList();

    var foosToInsert = foos.Except(foosToUpdate).ToList();

    foreach (var foo in foosToUpdate)
    {
        var f = db.Foos.First(x => someEqualityTest(x));

        // update the existing foo `f` with values from `foo`
    }

    // Insert the new Foos to the table named "Foos"
    db.BulkInsert("Foos", foosToinsert);

    db.SaveChanges();
}

【讨论】:

  • 谢谢,我创建了一个基于提供程序的解决方案,而且当前的提供程序确实使用了 SqlBulkCopy(仅限插入),是的,它更快!我正在重新访问 EF 提供程序,因为现在我也想要插入/更新逻辑。但你确实提供了一个非常聪明的解决方案,我喜欢它!
  • 切换答案。我认为您的解决方案是正确的方法
  • @ErOx 和贾斯汀:我真的不同意那个解决方案。 BulkInsert 很好!但是:您询问了 UPDATES,对于 UPDATES,此代码是一个不好的建议。第一:f = foo 技巧不起作用(见我的回答)。第二:x =&gt; foos.Contains(x) 将抛出 EF 异常。它必须是x =&gt; foos.Select(f =&gt; f.Id).Contains(x.Id)。第三也是最重要的:对许多对象使用Contains 是一个总体性能杀手:stackoverflow.com/a/8108643/270591(+ 该答案中的链接)。
  • @Slauma 感谢您的反馈。 Contains 调用是我关心的事情,这就是为什么我对这个问题发表评论,要求澄清someEqualityTest 方法是如何实现的。
  • 谢谢斯劳马。我确信我的实际解决方案将是这里的一些东西的组合,并且非常感谢您的“包含”分析的链接,我现在显然会使用 SqlQuery 方法!
【解决方案2】:

你的更新...

var f = db.Foos.First(x => someEqualityTest(foo));
f = foo;

...不会工作,因为您根本没有更改加载和附加的对象f,您只需用分离的对象foo 覆盖变量f。附加的对象仍然在上下文中,但在加载后它没有改变,你不再有指向它的变量。 SaveChanges 在这种情况下不会做任何事情。

您拥有的“标准选项”是:

var f = db.Foos.First(x => someEqualityTest(foo));
db.Entry(f).State = EntityState.Modified;

或者只是

db.Entry(foo).State = EntityState.Modified;
// attaches as Modified, no need to load f

这会将所有属性标记为已修改 - 无论它们是否真的改变了 - 并将每列的更新发送到数据库。

第二个选项只会将真正改变的属性标记为已修改,并且只为改变的列发送更新:

var f = db.Foos.First(x => someEqualityTest(foo));
db.Entry(f).CurrentValues.SetValues(foo);

现在,有 200 万个对象要更新,您没有“标准”情况,并且可能两个选项 - 尤其是第二个可能在内部使用反射来匹配源对象和目标对象的属性名称的选项 - 太慢了.

在更新性能方面的最佳选择是更改跟踪代理。这意味着您需要将实体类中的每个属性都标记为virtual(不仅是导航属性,还有标量属性),并且您不要禁用更改跟踪代理的创建(默认情况下启用) .

当您从数据库加载对象f 时,EF 将创建一个动态代理对象(从您的实体派生),类似于延迟加载代理,该代理将代码注入到每个属性设置器中以维护一个标志,如果该属性是否已更改。

代理提供的更改跟踪比基于快照的更改跟踪(发生在SaveChangesDetectChanges)快得多。

我不确定如果您使用更改跟踪代理,上述两个选项是否会更快。您可能需要手动分配属性以获得最佳性能:

var f = db.Foos.First(x => someEqualityTest(foo));
f.Property1 = foo.Property1;
f.Property2 = foo.Property2;
// ...
f.PropertyN = foo.PropertyN;

根据我在具有几千个对象的类似更新情况下的经验,没有真正的替代方法可以更改跟踪代理的性能。

【讨论】:

  • 出色而周到的答案!正是我一直在寻找的那种东西!
  • @ErOx:顺便说一句:你真的在最后通过一个SaveChanges 调用成功地插入了 200 万个对象吗?我刚刚获取了一个只有 50 万个对象的旧测量值,当我只调用一次 SaveChanges 时出现内存不足异常:stackoverflow.com/a/5942176/270591
  • 很好 - 不,我正在循环使用与您链接到的其他问题非常相似的所有添加内容。所以是的,上面的代码非常简化
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-01-19
  • 2021-06-21
  • 2021-11-08
  • 2020-05-08
  • 2016-06-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多