2012-03-28 60 views
1

我使用EF 4.1一次插入大量数据,每次包装在一个事务中(如200万行)。现在我想添加更新逻辑。请记住,根据数据量禁用更改跟踪。关闭我的头顶,我会做这样的事情:EF 4.1插入/更新逻辑最佳实践

// 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(); 
} 

任何想法如何可能改善呢?

+0

你能摆脱你如何检查多一点光,如果两个实例Foo是平等的吗?这是一个简单的ID比较? – 2012-03-28 17:43:54

+0

可能是,我只是想概括一下具体细节,并将注意力放在解决方案的较大逻辑方面 – Didaxis 2012-03-28 18:33:47

回答

2

我会保持插入与更新分开。

对于插入,我建议使用SqlBulkCopy插入所有尚不存在的记录,这将是方式更快。

首先,大容量插入方法在你的DbContext:

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(); 
} 
+0

谢谢,我创建了一个基于提供者的解决方案,实际上当前的提供者使用SqlBulkCopy(仅插入),是的,它的速度更快!我正在重新访问EF提供者,因为现在我想插入/更新逻辑。但你提供了一个非常聪明的解决方案,我喜欢它! – Didaxis 2012-03-28 18:11:19

+0

切换回答。我认为你的解决方案是正确的方法 – Didaxis 2012-03-28 18:15:37

+0

谢谢!很高兴我能帮上忙。 – 2012-03-28 18:23:12

1

你更新...

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

...将无法正常工作,因为你没有改变加载和连接对象f可言,你只是覆盖变量f与分离对象foo。附加的对象仍然在上下文中,但是在加载后它没有被改变,并且你没有指向它的变量。 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 

这标志着作为修改的所有特性 - 如果他们真的改变与否没有关系 - 并且将发送更新为数据库的每一列。

这只会标志着真正改变属性修改,并只发送了更改的列的UPDATE第二个选项:

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

现在,200万点要更新的对象,你没有一个“标准“的情况,并且可能的是,两种选择 - 特别是可能使用内部反射来匹配源和目标对象的属性名称的第二种 - 太慢。

当涉及到更新性能时,最好的选择是更改跟踪代理。这意味着您需要将实体类中的EVERY属性标记为virtual(不仅是导航属性,还包括标量属性),并且您不禁止创建更改跟踪代理(默认情况下启用)。

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

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

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

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

在我类似的更新情况经验对象的几千元也没有改变对性能跟踪代理真正的替代品。

+0

优秀,深思熟虑的答案!只是我一直在寻找的东西! – Didaxis 2012-03-28 17:35:55

+0

@ErOx:顺便说一句:你真的在一个单独的'SaveChanges'调用中成功插入了200万个对象吗?我刚刚为50多万个对象抓取了一个旧测量值,并且当我只调用一次SaveChanges时,出现了内存不足的情况:http://stackoverflow.com/a/5942176/270591 – Slauma 2012-03-28 17:53:17

+0

好的catch - 不,我正在回收上下文,每增加很多与您链接的其他问题非常相似。所以是的,上面的代码是非常简化的 – Didaxis 2012-03-28 18:05:34