5

Tôi đang sử dụng Khuôn khổ thực thể 4.3.1 trong một dự án, sử dụng mã đầu tiên và API DbContext. Ứng dụng của tôi là một ứng dụng n-tier nơi các đối tượng bị ngắt kết nối có thể đến từ một ứng dụng khách. Tôi đang sử dụng SQL Server 2008 R2 nhưng sẽ được chuyển đến SQL Azure sớm. Tôi đang gặp một vấn đề mà tôi không thể giải quyết được.Lưu các đối tượng đơn lẻ với mã Khuôn khổ thực thể đầu tiên

Hãy tưởng tượng tôi có một vài lớp:

class A { 
    // Random stuff here 
} 
class B { 
    // Random stuff here 
    public A MyA { get; set; } 
} 
class C { 
    // Random stuff here 
    public A MyA { get; set; } 
} 

Theo mặc định, EF hoạt động trên đồ thị đối tượng. Ví dụ, nếu tôi có một thể hiện của B đóng gói một thể hiện của A và tôi gọi myDbSet.Add(myB);, nó cũng sẽ đánh dấu cá thể của A khi được thêm vào (giả sử nó chưa được theo dõi).

Tôi có một kịch bản trong ứng dụng của mình, nơi tôi cần phải rõ ràng về những đối tượng nào được lưu vào cơ sở dữ liệu, thay vì theo dõi toàn bộ biểu đồ đối tượng. Trình tự thao tác như sau:

A myA = new A(); // Represents something already in DB that doesn't need to be udpated. 
C myC = new C() { // Represents something already in DB that DOES need to be updated. 
    A = myA; 
} 
B myB0 = new B() { // Not yet in DB. 
    A = myA; 
} 
B myB1 = new B() { // Not yet in DB. 
    A = myA; 
} 

myDbSetC.Attach(myC); 
context.Entry(myC).State = Modified; 

myDbSetB.Add(myB0); // Tries to track myA with a state of Added 
myDbSetB.Add(myB1); 

context.SaveChanges(); 

Tại thời điểm này tôi nhận được một lỗi nói AcceptChanges cannot continue because the object's key values conflict with another object in the ObjectStateManager. Make sure that the key values are unique before calling AcceptChanges. Tôi tin rằng điều này xảy ra bởi vì gọi thêm vào myB0 đánh dấu sự thể hiện của A như là gia tăng, xung đột với trường hợp của A đã đang được theo dõi.

Lý tưởng nhất là tôi có thể làm điều gì đó như gọi số myDbSet.AddOnly(myB), nhưng rõ ràng là chúng tôi không có tùy chọn đó.

Tôi đã thử nhiều cách giải quyết:

Cố gắng # 1: Trước tiên, tôi cố gắng tạo ra một phương pháp helper để ngăn chặn MYA từ được thêm một lần thứ hai.

private void MarkGraphAsUnchanged<TEntity>(TEntity entity) where TEntity : class { 
     DbEntityEntry entryForThis = this.context.Entry<TEntity>(entity); 
     IEnumerable<DbEntityEntry> entriesItWantsToChange = this.context.ChangeTracker.Entries().Distinct(); 

     foreach (DbEntityEntry entry in entriesItWantsToChange) { 
      if (!entryForThis.Equals(entry)) { 
       entry.State = System.Data.EntityState.Unchanged; 
      } 
     } 
    } 

... 

myDbSetB.Add(myB0); 
MarkGraphAsUnchanged(myB0); 

Trong khi điều này giải quyết được sự cố khi cố gắng thêm myA, nó vẫn gây ra các vi phạm chính trong ObjectStateManager.

Cố gắng # 2: Tôi đã thử thực hiện tương tự như trên, nhưng đặt trạng thái thành Đã tách rời thay vì Không thay đổi. Điều này làm việc để tiết kiệm, nhưng nó nhấn mạnh vào việc thiết lập myB0.A = null, trong đó có tác dụng phụ khác trong mã của tôi.

Cố gắng # 3: Tôi đã sử dụng một TransactionScope xung quanh toàn bộ DbContext của mình. Tuy nhiên, ngay cả khi gọi SaveChanges() giữa mỗi Attach()Add(), trình theo dõi thay đổi không xóa các mục được theo dõi của nó để tôi có cùng một vấn đề như trong nỗ lựC# 1.

Cố gắng # 4: tôi tiếp tục với TransactionScope, ngoại trừ tôi đã sử dụng một/DAO mô hình kho và trong nội bộ tạo ra một DbContext mới và gọi SaveChanges() cho từng hoạt động riêng biệt tôi làm. Trong trường hợp này, tôi gặp lỗi 'Lưu trữ cập nhật, chèn hoặc xóa tuyên bố đã ảnh hưởng đến số lượng hàng không mong muốn'. Khi sử dụng SQL Profiler, tôi thấy rằng khi gọi SaveChanges() trên hoạt động thứ hai mà tôi đã thực hiện (Add()) đầu tiên, nó thực sự gửi UPDATE SQL đến cơ sở dữ liệu từ hoạt động đầu tiên lần thứ hai - nhưng không thay đổi bất kỳ hàng nào. Điều này cảm thấy giống như một lỗi trong Entity Framework với tôi.

Cố gắng # 5: Thay vì sử dụng TransactionScope, tôi quyết định chỉ sử dụng DbTransaction. Tôi vẫn tạo ra nhiều bối cảnh nhưng vượt qua EntityConnection được dựng sẵn cho mỗi ngữ cảnh mới khi nó được tạo (bằng caching và mở thủ công EntityConnection được xây dựng bởi bối cảnh đầu tiên). Tuy nhiên, khi tôi làm điều này, ngữ cảnh thứ hai chạy một trình khởi tạo mà tôi đã định nghĩa, mặc dù nó sẽ chạy khi ứng dụng khởi động lần đầu tiên. Trong môi trường dev, tôi có một số dữ liệu thử nghiệm, và nó thực sự đã hẹn hò với một khóa cơ sở dữ liệu trên một bảng của tôi đã sửa đổi Attach() đầu tiên (nhưng vẫn bị khóa do giao dịch vẫn đang mở).

Trợ giúp !! Tôi đã thử tất cả mọi thứ tôi có thể nghĩ đến, và ngắn hoàn toàn tái cấu trúc ứng dụng của tôi để không sử dụng các thuộc tính điều hướng hoặc sử dụng DAO được xây dựng theo cách thủ công để làm các câu lệnh INSERT, UPDATE và DELETE, tôi thua lỗ. Dường như phải có một cách để có được những lợi ích của Khung thực thể đối với ánh xạ O/R nhưng vẫn kiểm soát thủ công các hoạt động trong một giao dịch!

+0

Bạn đã thử gắn myA chưa? Bạn sẽ cần phải làm điều này trước khi đính kèm bất cứ điều gì khác. – cadrell0

Trả lời

2

Phải có nội dung khác mà bạn không hiển thị vì không có vấn đề gì với cách bạn đính kèm và thêm đối tượng. Mã sau sẽ đính kèm myA, myC, myB0myB1 vào ngữ cảnh dưới dạng không đổi và đặt trạng thái là myC để sửa đổi.

myDbSetC.Attach(myC); 
context.Entry(myC).State = Modified; 

đoạn mã sau sẽ chính xác phát hiện rằng tất cả các đơn vị đã được gắn và thay vì ném ngoại lệ (vì nó sẽ làm trong ObjectContext API) hoặc chèn tất cả các đối tượng một lần nữa (như bạn mong đợi) nó sẽ chỉ cần thay đổi myB0myB1 trạng thái thêm:

myDbSetB.Add(myB0); 
myDbSetB.Add(myB1); 

Nếu bạn myAmyC được khởi tạo một cách chính xác với các phím của đơn vị hiện toàn bộ mã sẽ thực hiện một cách chính xác và tiết kiệm ngoại trừ vấn đề duy nhất:

C myC = new C() { 
    A = myA; 
} 

Điều này có vẻ như independent association và liên kết độc lập có trạng thái riêng nhưng API để đặt trạng thái là not available in DbContext API. Nếu đây là một mối quan hệ mới bạn muốn lưu nó sẽ không được lưu vì nó vẫn được theo dõi là không thay đổi. Bạn có thể phải sử dụng liên kết chính nước ngoài hoặc bạn phải chuyển đổi ngữ cảnh của bạn để ObjectContext:

ObjectContext objectContext = ((IObjectContextAdapter)dbContext).ObjectContext; 

và sử dụng ObjectStateManager-change state of the relation.

+0

Bạn đúng, đây là những hiệp hội độc lập. Bạn nói "Nếu myA và myC của bạn được khởi tạo đúng với các khóa của các thực thể hiện tại". Giả định của tôi là trình theo dõi thay đổi/trình quản lý trạng thái đang sử dụng các khóa đó để xác định các mục nhập khác nhau. Sau khi phân tích thêm (đồ thị đối tượng thực tế của tôi phức tạp hơn nhiều), có một trường hợp có hai đối tượng, hãy gọi chúng là As. Chúng có cùng khóa và nội dung nhưng là các cá thể khác nhau trong bộ nhớ. Điều đó gây ra vấn đề: bản sao A được thêm cùng với B mặc dù A đã được theo dõi bằng cùng một khóa. –

+0

Bạn không được có hai phiên bản cho cùng một thực thể. Nó sẽ luôn dẫn đến các vấn đề hoặc ngoại lệ. –

+0

Cảm ơn mẹo! Tôi sẽ đi qua và đảm bảo các trường hợp đối tượng là như nhau và xem tôi vẫn gặp sự cố. Thật không may là hạn chế này áp dụng và không có cách nào để bỏ qua theo dõi thay đổi và thực hiện từng thao tác theo cách thủ công. Tôi cũng vẫn nghĩ rằng có một số vấn đề liên quan đến nhiều ngữ cảnh trong cùng một giao dịch ... mặc dù những vấn đề này có thể khác với vấn đề nhiều phiên bản này. Bất kể, nếu đảm bảo các trường hợp là cùng một giải quyết vấn đề tôi sẽ chấp nhận câu trả lời. Cảm ơn một lần nữa! –

0

Như Ladislav đã đề xuất, tôi đã nhận được các trường hợp đối tượng nhất quán, giải quyết vấn đề của nó cố gắng thêm dư thừa Như.

Khi nó chỉ ra, cả B0 và B1 thực sự đóng gói các đối tượng khác (D0 và D1, tương ứng) mà lần lượt đóng gói A. Cả D0 và D1 đã có trong cơ sở dữ liệu nhưng không được theo dõi bởi Entity.

Thêm B0/B1 khiến D0/D1 cũng được chèn vào, sai. Tôi đã kết thúc bằng cách sử dụng bối cảnh đối tượng API Ladislav đề xuất để đánh dấu cả ObjectStateEntry cho D0/D1 thành Unchanged và các mối quan hệ giữa D0/D1 và A là Unchanged. Điều này dường như làm những gì tôi cần: cập nhật C và chỉ chèn B0/B1.

Dưới đây là mã của tôi để thực hiện việc này, mà tôi gọi ngay trước SaveChanges.Lưu ý rằng tôi chắc chắn vẫn còn một số trường hợp cạnh không được xử lý và điều này không được kiểm tra quá mức - nhưng phải cung cấp ý tưởng thô những gì cần phải làm.

// Entries are put in here when they are explicitly added, modified, or deleted. 
private ISet<DbEntityEntry> trackedEntries = new HashSet<DbEntityEntry>(); 
private void MarkGraphAsUnchanged() 
{ 
    IEnumerable<DbEntityEntry> entriesItWantsToChange = this.context.ChangeTracker.Entries().Distinct(); 
    foreach (DbEntityEntry entry in entriesItWantsToChange) 
    { 
     if (!this.trackedEntries.Contains(entry)) 
     { 
      entry.State = System.Data.EntityState.Unchanged; 
     } 
    } 

    IEnumerable<ObjectStateEntry> allEntries = 
      this.context.ObjectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Added) 
      .Union(this.context.ObjectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Deleted)) 
      .Union(this.context.ObjectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Modified)); 

     foreach (ObjectStateEntry entry in allEntries) 
     { 
      if (entry.IsRelationship) 
      { 
       /* We can't mark relationships are being unchanged if we are truly adding or deleting the entity. 
       * To determine this, we need to first lookup the entity keys, then state entries themselves. 
       */ 
       EntityKey key1 = null; 
       EntityKey key2 = null; 
       if (entry.State == EntityState.Deleted) 
       { 
        key1 = (EntityKey)entry.OriginalValues[0]; 
        key2 = (EntityKey)entry.OriginalValues[1]; 
       } 
       else if (entry.State == EntityState.Added) 
       { 
        key1 = (EntityKey)entry.CurrentValues[0]; 
        key2 = (EntityKey)entry.CurrentValues[1]; 
       } 

       ObjectStateEntry entry1 = this.context.ObjectContext.ObjectStateManager.GetObjectStateEntry(key1); 
       ObjectStateEntry entry2 = this.context.ObjectContext.ObjectStateManager.GetObjectStateEntry(key2); 

       if ((entry1.State != EntityState.Added) && (entry1.State != EntityState.Deleted) && (entry2.State != EntityState.Added) && (entry2.State != EntityState.Deleted)) 
       { 
        entry.ChangeState(EntityState.Unchanged); 
       } 
      } 
     } 
    } 

Whew !!! Mẫu cơ bản là:

  1. Theo dõi rõ ràng các thay đổi khi chúng được tạo.
  2. Quay trở lại và dọn dẹp tất cả những thứ mà thực thể nghĩ rằng nó cần phải làm, nhưng không thực sự.
  3. Thực sự lưu các thay đổi đối với DB.

Phương pháp này phải "tối ưu và dọn dẹp" rõ ràng là không tối ưu, nhưng có vẻ là tùy chọn tốt nhất cho thời điểm này mà không cần phải đính kèm các thực thể ngoại vi theo cách thủ công (như D0/D1) trước Tôi cố gắng bất kỳ hoạt động tiết kiệm. Có tất cả logic này trong một kho lưu trữ chung giúp - logic chỉ cần được viết một lần. Tôi hy vọng trong một bản phát hành trong tương lai, Entity có thể thêm trực tiếp khả năng này (và loại bỏ các hạn chế về việc có nhiều cá thể của một đối tượng trên heap nhưng với cùng một khóa).

Các vấn đề liên quan