2012-03-28 77 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