5

Używam Entity Framework 4.3.1 w projekcie, używając najpierw kodu i interfejsu API DbContext. Moja aplikacja jest aplikacją n-warstwową, w której odłączone obiekty mogą pochodzić od klienta. Używam programu SQL Server 2008 R2, ale wkrótce przejdę do SQL Azure. Mam problem, którego nie mogę rozwiązać.Zapisywanie pojedynczych obiektów przy użyciu kodu Entity Framework najpierw

Wyobraźmy mam kilka klas:

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; } 
} 

Domyślnie EF działa na wykresach obiektów. Na przykład, jeśli mam instancję B, która hermetyzuje instancję A i ja wywołuję myDbSet.Add(myB);, będzie również oznaczać instancję A jako dodawaną (zakładając, że nie jest jeszcze śledzona).

Mam scenariusz w mojej aplikacji, gdzie muszę jasno określić, które obiekty zostaną utrwalone w bazie danych, zamiast śledzić całe wykresy obiektów. Kolejność czynności jest następująca:

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

W tym momencie pojawia się błąd mówiąc 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. wierzę, że to się dzieje, ponieważ wywołanie dodać na myB0 oznacza wystąpienie jako dodanej, który jest niezgodny z instancji A już śledzony.

Idealnie mógłbym zrobić coś jak zadzwonić myDbSet.AddOnly(myB), ale oczywiście nie mamy tej opcji.

Próbowałem kilka obejścia:

Próba nr 1: Pierwsze starałem stworzenie metody pomocnika, aby zapobiec Mya od dodawanych po raz drugi.

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); 

Podczas gdy rozwiązuje to problem z próbą dodania myA, nadal powoduje naruszenia kluczy w ObjectStateManager.

Próba nr 2: Próbowałem robić to samo co powyżej, ale ustawienie stanu do indywidualnego zamiast niezmienione. Działa to w celu zapisania, ale nalega na ustawienie myB0.A = null, która ma inne niepożądane efekty w moim kodzie.

Próba # 3: Użyłem TransactionScope wokół mojego całego DbContext. Jednak nawet po wywołaniu SaveChanges() między poszczególnymi Attach() i Add(), moduł śledzenia zmian nie opróżnia swoich śledzonych wpisów, więc mam ten sam problem co w próbie nr 1.

Próba nr 4: I dalej z TransactionScope, poza tym, że stosuje się wzór w repozytorium/DAO i wewnętrznie utworzyć nowy DbContext i wywołać SaveChanges() dla każdej odrębnej operacji ja. W tym przypadku wystąpił błąd "Aktualizacja sklepu, wstawienie lub usunięcie instrukcji wpłynęło na nieoczekiwaną liczbę wierszy". Podczas korzystania z SQL Profiler, stwierdzam, że podczas wywoływania SaveChanges() na I zrobiłem (pierwszy Add()), faktycznie wysyła UPDATE SQL do bazy danych z pierwszy po raz drugi - ale nie zmień dowolne wiersze. To wydaje mi się błędem w Entity Framework.

Próba nr 5: Zamiast korzystać z TransactionScope, zdecydowałem się użyć wyłącznie opcji DbTransaction. Nadal tworzę wiele kontekstów, ale przekazuję prefabrykowane EntityConnection do każdego nowego kontekstu podczas jego tworzenia (przez buforowanie i ręczne otwieranie EntityConnection zbudowanego przez pierwszy kontekst). Jednak gdy to zrobię, drugi kontekst uruchamia zdefiniowany przeze mnie inicjator, mimo że byłby już uruchomiony po uruchomieniu aplikacji. W środowisku deweloperskim mam to wysiew niektórych danych testowych, a to faktycznie czasy na wating dla blokady bazy danych na stole mój pierwszy zmodyfikowany Attach() (ale nadal jest zablokowany z powodu wciąż otwartej transakcji).

Pomoc !! Próbowałem już wszystkiego, co przychodzi mi do głowy, i nie w pełni refaktoryzowałem moją aplikację, aby nie korzystał z właściwości nawigacyjnych ani nie używał ręcznie skonstruowanych DAO do wykonywania instrukcji INSERT, UPDATE i DELETE. Brakuje mi tego. Wydaje się, że musi istnieć sposób na uzyskanie korzyści z Entity Framework dla mapowania O/R, ale nadal ręcznie kontrolując operacje w ramach transakcji!

+0

Czy próbowałeś już dołączać myA? Będziesz musiał to zrobić przed podłączeniem czegokolwiek innego. – cadrell0

Odpowiedz

2

Musi być coś jeszcze, czego nie pokazujesz, ponieważ nie ma problemu ze sposobem w jaki dołączasz i dodajesz jednostki. Poniższy kod spowoduje dołączenie myA, myC, i myB1 do kontekstu jako niezmienionego i ustawienie stanu myC w celu zmodyfikowania.

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

następujący kod zostanie poprawnie wykryć, że wszystkie podmioty są już zamontowane i zamiast rzucać wyjątek (jak to zrobić w ObjectContext API) lub wstawienie wszystkie podmioty ponownie (jak można się spodziewać), to wystarczy zmienić myB0 i myB1 dodanego do stanu:

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

Jeśli Twój myA i myC są prawidłowo zainicjowany z kluczami istniejących podmiotów cały kod zostanie poprawnie wykonanie i zapisać z wyjątkiem jednego problemu:

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

Wygląda na to, że independent association i niezależne powiązanie ma swój własny stan, ale interfejs API do ustawienia jego stanu to not available in DbContext API. Jeśli jest to nowa relacja, którą chcesz zapisać, nie zostanie zapisana, ponieważ jest wciąż śledzona jako niezmieniona. Musisz użyć klucza obcego lub stowarzyszenie trzeba konwertować kontekst ObjectContext:

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

i używać ObjectStateManager do change state of the relation.

+0

Masz rację, są to niezależne skojarzenia. Powiedziałeś "Jeśli twoje myA i myC są poprawnie zainicjalizowane za pomocą kluczy istniejących jednostek". Zakładam, że menedżer zmian/menedżer stanu używa tych kluczy do identyfikacji różnych wpisów. Po dalszych analizach (mój rzeczywisty wykres obiektów jest znacznie bardziej złożony), jest jeden przypadek, w którym są dwa obiekty, nazwijmy je As. Mają te same klucze i zawartość, ale są różnymi instancjami w pamięci. Powoduje to problemy: duplikat A zostaje dodany wraz z B, mimo że A jest już śledzony tymi samymi kluczami. –

+0

Nie możesz mieć dwóch wystąpień dla tego samego obiektu. Zawsze będzie powodować problemy lub wyjątki. –

+0

Dzięki za napiwek! Przejdę i upewnię się, że instancje obiektów są takie same i zobacz, czy nadal mam problemy. Niefortunne jest to ograniczenie i nie ma sposobu na obejście śledzenia zmiany i wykonanie każdej operacji ręcznie. Nadal uważam, że istnieją pewne problemy związane z wieloma kontekstami w ramach tej samej transakcji ... chociaż są one prawdopodobnie inne niż ten problem z wieloma wystąpieniami. Niezależnie od tego, jeśli upewnienie się, że instancje są takie same, rozwiązuje problem, zaakceptuję odpowiedź. Dzięki jeszcze raz! –

0

Jak zasugerował Ladislav, dostałem instancje obiektów spójne, co rozwiązało problem z próbą dodania zbędnego Asa.

Okazuje się, że zarówno B0, jak i B1 faktycznie hermetyzują inne obiekty (odpowiednio D0 i D1), które z kolei zamykają A. Obie D0 i D1 znajdowały się już w bazie danych, ale nie były śledzone przez Entity.

Dodanie B0/B1 spowodowało również błędne wstawienie D0/D1. Skończyło się na wykorzystaniu kontekstowego API obiektu, który Ladislav zasugerował, aby zaznaczyć ObjectStateEntry dla D0/D1 na Unchanged, a relacje między D0/D1 i A jako Unchanged. Wydaje się, że to, czego potrzebuję: zaktualizuj C i wstaw tylko B0/B1.

Poniżej znajduje się mój kod do tego, który nazywam tuż przed SaveChanges.Zauważ, że jestem pewien, że nadal są jakieś przypadki skrajne, które nie są obsługiwane, a to nie jest całkowicie testowane - ale powinno dać szorstki pomysł, co należy zrobić.

// 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 !!! Podstawowy wzór to:

  1. Jawnie śledź zmiany w trakcie ich wprowadzania.
  2. Wróć i posprzątaj wszystkie rzeczy, które Entity uważa za konieczne, ale tak naprawdę nie.
  3. Właściwie zapisz zmiany w bazie danych.

Ta metoda "powrotu i oczyszczenia" jest oczywiście nieoptymalna, ale wydaje się być najlepszą opcją w danym momencie, bez konieczności ręcznego dołączania elementów peryferyjnych (takich jak D0/D1) przed Próbuję wykonać operację zapisu. Posiadanie całej tej logiki w ogólnym repozytorium pomaga - logika musi być napisana tylko raz. Mam nadzieję, że w przyszłej wersji Entity może dodać tę funkcję bezpośrednio (i usunąć ograniczenie dotyczące posiadania wielu wystąpień obiektu na stercie, ale z tym samym kluczem).

Powiązane problemy