Jeden DbContext na żądanie internetowe… dlaczego?


398

Czytałem wiele artykułów wyjaśniających, jak skonfigurować Entity Framework, DbContextaby tylko jeden był tworzony i używany na żądanie HTTP za pomocą różnych frameworków DI.

Dlaczego to dobry pomysł? Jakie korzyści zyskujesz stosując to podejście? Czy istnieją sytuacje, w których byłby to dobry pomysł? Czy są rzeczy, które można zrobić za pomocą tej techniki, których nie można zrobić, gdy tworzy się instancję DbContexts dla wywołania metody repozytorium?


9
Gueddari w mehdi.me/ambient-dbcontext-in-ef6 wywołuje instancję DbContext dla każdej metody repozytorium wywołują antipattern. Cytat: „Robiąc to, tracisz prawie wszystkie funkcje, które Entity Framework zapewnia za pośrednictwem DbContext, w tym pamięć podręczną pierwszego poziomu, mapę tożsamości, jednostkę pracy oraz możliwości śledzenia zmian i leniwego ładowania . ” Doskonały artykuł z świetnymi sugestiami dotyczącymi obsługi cyklu życia DBContexts. Zdecydowanie warte przeczytania.
Christoph

Odpowiedzi:


564

UWAGA: Ta odpowiedź mówi o Entity Framework DbContext, ale ma zastosowanie do wszelkiego rodzaju implementacji Jednostek Pracy, takich jak LINQ do SQL DataContexti NHibernate ISession.

Zacznijmy od echa Iana: posiadanie singla DbContextdla całej aplikacji jest złym pomysłem. Jedyną sytuacją, w której ma to sens, jest posiadanie aplikacji jednowątkowej i bazy danych używanej wyłącznie przez tę instancję pojedynczej aplikacji. Nie DbContextjest bezpieczny dla wątków, a ponieważ DbContextdane w pamięci podręcznej szybko się zestarzeją. Doprowadzi to do różnego rodzaju problemów, gdy wielu użytkowników / aplikacji pracuje jednocześnie nad tą bazą danych (co jest oczywiście bardzo częste). Ale oczekuję, że już to wiesz i po prostu chcesz wiedzieć, dlaczego nie po prostu wstrzyknąć nowej instancji (tj. Przejściowy styl życia) DbContextkażdemu, kto jej potrzebuje. (aby uzyskać więcej informacji o tym, dlaczego pojedynczy DbContext- lub nawet w kontekście na wątek - jest zły, przeczytaj tę odpowiedź ).

Zacznę od stwierdzenia, że ​​rejestracja DbContextjako przejściowa może działać, ale zazwyczaj chcesz mieć jedno wystąpienie takiej jednostki pracy w określonym zakresie. W aplikacji internetowej może być praktyczne zdefiniowanie takiego zakresu na granicach żądania sieciowego; w ten sposób styl życia na żądanie internetowe. Umożliwia to działanie całego zestawu obiektów w tym samym kontekście. Innymi słowy, działają w ramach tej samej transakcji biznesowej.

Jeśli nie masz celu, aby zestaw operacji działał w tym samym kontekście, w takim przypadku przejściowy styl życia jest w porządku, ale jest kilka rzeczy do obejrzenia:

  • Ponieważ każdy obiekt ma swoją własną instancję, każda klasa, która zmienia stan systemu, musi wywołać _context.SaveChanges()(w przeciwnym razie zmiany zostaną utracone). Może to skomplikować Twój kod i nałożyć na niego drugą odpowiedzialność (odpowiedzialność za kontrolowanie kontekstu), co stanowi naruszenie zasady pojedynczej odpowiedzialności .
  • Musisz upewnić się, że jednostki [ładowane i zapisywane przez DbContext] nigdy nie opuszczają zakresu takiej klasy, ponieważ nie można ich użyć w kontekście innej klasy. Może to ogromnie skomplikować Twój kod, ponieważ gdy potrzebujesz tych jednostek, musisz załadować je ponownie według identyfikatora, co może również powodować problemy z wydajnością.
  • Ponieważ DbContextimplementuje IDisposable, prawdopodobnie nadal chcesz usunąć wszystkie utworzone instancje. Jeśli chcesz to zrobić, zasadniczo masz dwie opcje. Musisz je zutylizować w ten sam sposób zaraz po wywołaniu context.SaveChanges(), ale w takim przypadku logika biznesowa przejmuje własność obiektu, który jest przekazywany z zewnątrz. Drugą opcją jest usunięcie wszystkich utworzonych instancji na granicy żądania HTTP, ale w takim przypadku nadal potrzebujesz pewnego zakresu, aby poinformować kontener, kiedy te instancje muszą zostać usunięte.

Inną opcją jest wcale nie wstrzykiwanie DbContext. Zamiast tego wstrzykujesz plik, DbContextFactoryktóry jest w stanie utworzyć nową instancję (kiedyś używałem tego podejścia). W ten sposób logika biznesowa wyraźnie kontroluje kontekst. Jeśli mogłoby to wyglądać tak:

public void SomeOperation()
{
    using (var context = this.contextFactory.CreateNew())
    {
        var entities = this.otherDependency.Operate(
            context, "some value");

        context.Entities.InsertOnSubmit(entities);

        context.SaveChanges();
    }
}

Zaletą tego jest to, że zarządzasz życiem DbContextjawnie i łatwo to skonfigurować. Pozwala także na użycie jednego kontekstu w pewnym zakresie, który ma wyraźne zalety, takie jak uruchamianie kodu w pojedynczej transakcji biznesowej i możliwość przekazywania jednostek, ponieważ pochodzą one z tego samego DbContext.

Minusem jest to, że będziesz musiał przechodzić DbContextod metody do metody (która nazywa się Method Injection). Zauważ, że w pewnym sensie to rozwiązanie jest takie samo jak podejście „zakresowe”, ale teraz zakres jest kontrolowany w samym kodzie aplikacji (i być może jest wielokrotnie powtarzany). Jest to aplikacja odpowiedzialna za tworzenie i usuwanie jednostki pracy. Ponieważ DbContextjest tworzony po skonstruowaniu wykresu zależności, konstruktor Wtrysk jest poza obrazem i musisz przejść do metody Wtrysk, gdy musisz przekazać kontekst z jednej klasy do drugiej.

Metoda wstrzykiwania nie jest taka zła, ale kiedy logika biznesowa staje się bardziej złożona i angażuje się więcej klas, będziesz musiał przekazać ją od metody do metody i klasy do klasy, co może bardzo skomplikować kod (widziałem to w przeszłości). W przypadku prostej aplikacji to podejście wystarczy.

Ze względu na wady to podejście fabryczne ma zastosowanie w przypadku większych systemów, inne podejście może być przydatne i to jest to, w którym pozwalasz kontenerowi lub kodowi infrastruktury / rootowaniu kompozycji zarządzać jednostką pracy. To jest styl, którego dotyczy twoje pytanie.

Pozwalając kontenerowi i / lub infrastrukturze obsłużyć to, kod aplikacji nie jest zanieczyszczony przez konieczność utworzenia (opcjonalnie) zatwierdzenia i usunięcia instancji UoW, co zapewnia logikę biznesową prostą i czystą (tylko jedna odpowiedzialność). Z tym podejściem wiążą się pewne trudności. Na przykład, czy popełniłeś i zlikwidowałeś instancję?

Pozbywanie się jednostki pracy można wykonać na końcu żądania internetowego. Jednak wielu ludzi błędnie zakłada, że ​​jest to również miejsce, w którym można zatwierdzić jednostkę pracy. Jednak w tym momencie aplikacji po prostu nie można ustalić, czy jednostka pracy powinna zostać faktycznie zatwierdzona. np. jeśli kod warstwy biznesowej zgłosił wyjątek, który został złapany wyżej na stosie wywołań, zdecydowanie nie chcesz zatwierdzać.

Prawdziwym rozwiązaniem jest ponownie jawne zarządzanie jakimś zakresem, ale tym razem zrób to w katalogu głównym. Wyodrębnienie całej logiki biznesowej stojącej za wzorcem polecenia / procedury obsługi , będziesz mógł napisać dekorator, który można owinąć wokół każdego modułu obsługi poleceń, który pozwala to zrobić. Przykład:

class TransactionalCommandHandlerDecorator<TCommand>
    : ICommandHandler<TCommand>
{
    readonly DbContext context;
    readonly ICommandHandler<TCommand> decorated;

    public TransactionCommandHandlerDecorator(
        DbContext context,
        ICommandHandler<TCommand> decorated)
    {
        this.context = context;
        this.decorated = decorated;
    }

    public void Handle(TCommand command)
    {
        this.decorated.Handle(command);

        context.SaveChanges();
    } 
}

Zapewnia to, że kod infrastruktury trzeba zapisać tylko raz. Każdy solidny pojemnik DI umożliwia skonfigurowanie takiego dekoratora, aby był owijany wokół wszystkich ICommandHandler<T>implementacji w spójny sposób.


2
Wow - dzięki za dokładną odpowiedź. Gdybym mógł dwukrotnie głosować, zrobiłbym to. Powyżej mówisz „... nie ma zamiaru pozwolić, aby cały zestaw operacji działał w tym samym kontekście, w takim przypadku przejściowy styl życia jest w porządku ...”. Co konkretnie rozumiesz przez „przejściowy”?
Andrew

14
@Andrew: „Przejściowy” to koncepcja wstrzykiwania zależności, co oznacza, że ​​jeśli usługa jest skonfigurowana jako przejściowa, nowa instancja usługi jest tworzona za każdym razem, gdy jest wstrzykiwana do konsumenta.
Steven

1
@ user981375: W przypadku operacji CRUD można utworzyć ogólny CreateCommand<TEnity>i ogólny CreateCommandHandler<TEntity> : ICommandHandler<CreateCommand<TEntity>>(i zrobić to samo dla Aktualizuj i Usuń, i miał jedno GetByIdQuery<TEntity>zapytanie). Jednak powinieneś zadać sobie pytanie, czy ten model jest użyteczną abstrakcją dla operacji CRUD, czy tylko dodaje złożoności. Mimo to możesz skorzystać z możliwości łatwego dodawania problemów przekrojowych (przez dekoratorów) za pomocą tego modelu. Będziesz musiał rozważyć zalety i wady.
Steven

3
+1 Czy uwierzyłbyś, że napisałem całą tę odpowiedź przed jej przeczytaniem? BTW IMO Uważam, że ważne jest, aby omówić utylizację DbContext na końcu (chociaż to wspaniałe, że pozostajesz agnostykiem w kontenerze)
Ruben Bartelink

1
Ale nie przekazujesz kontekstu do klasy zdobionej, w jaki sposób klasa zdobiona może pracować z tym samym kontekstem, co przekazana do TransactionCommandHandlerDecorator? na przykład, jeśli zdobiona klasa jest InsertCommandHandlerklasą, jak mogłaby zarejestrować operację wstawiania w kontekście (DbContext w EF)?
Masoud

34

Żadna odpowiedź tutaj nie odpowiada na pytanie. OP nie zapytał o projekt DbContext dla pojedynczego / na aplikację, zapytał o projekt żądania dla (sieci) i jakie potencjalne korzyści mogłyby istnieć.

Odniosę się do http://mehdi.me/ambient-dbcontext-in-ef6/, ponieważ Mehdi jest fantastycznym źródłem:

Możliwe zwiększenie wydajności.

Każda instancja DbContext utrzymuje pamięć podręczną pierwszego poziomu wszystkich encji, które ładuje z bazy danych. Za każdym razem, gdy pytasz byt za pomocą jego klucza podstawowego, DbContext najpierw spróbuje odzyskać go z pamięci podręcznej pierwszego poziomu, zanim przystąpi do kwerendy z bazy danych. W zależności od wzorca zapytań o dane ponowne użycie tego samego DbContext w wielu sekwencyjnych transakcjach biznesowych może spowodować mniej zapytań do bazy danych dzięki pamięci podręcznej pierwszego poziomu DbContext.

Umożliwia leniwe ładowanie.

Jeśli Twoje usługi zwracają trwałe jednostki (w przeciwieństwie do zwracania modeli widoku lub innych rodzajów DTO) i chcesz skorzystać z leniwego ładowania tych jednostek, czas życia instancji DbContext, z którego te jednostki zostały pobrane, musi wykraczać poza zakres transakcji biznesowej. Jeśli metoda usługi pozbyła się instancji DbContext, z której korzystała przed zwróceniem, każda próba leniwego ładowania właściwości na zwróconych obiektach zakończyła się niepowodzeniem (niezależnie od tego, czy użycie leniwego ładowania jest dobrym pomysłem, jest zupełnie inną debatą, w którą nie wchodzimy tutaj). W naszym przykładzie aplikacji sieciowej leniwe ładowanie byłoby zwykle stosowane w metodach działania kontrolera na obiektach zwracanych przez oddzielną warstwę usługi. W tym wypadku,

Pamiętaj, że są też wady. Ten link zawiera wiele innych zasobów do przeczytania na ten temat.

Wystarczy opublikować to na wypadek, gdyby ktoś natknął się na to pytanie i nie wchłonął odpowiedzi, które w rzeczywistości nie dotyczą tego pytania.


Dobry link! Jawne zarządzanie DBContext wygląda jak najbezpieczniejsze podejście.
aggsol

34

Istnieją dwie sprzeczne zalecenia firmy Microsoft i wiele osób korzysta z DbContexts w całkowicie rozbieżny sposób.

  1. Jedną z rekomendacji jest „Pozbyć się DbContexts tak szybko, jak to możliwe” ponieważ posiadanie DbContext Alive zajmuje cenne zasoby, takie jak połączenia db itp.
  2. Drugi stwierdza, że zalecany jest jeden DbContext na żądanie

Te są ze sobą sprzeczne, ponieważ jeśli twoje żądanie robi wiele niezwiązanych z Db rzeczy, to twój DbContext jest przechowywany bez powodu. Dlatego marnowanie DbContext przy życiu jest niepotrzebne, podczas gdy twoja prośba czeka tylko na zrobienie losowych rzeczy ...

Tak wiele osób, które przestrzegają reguły 1, ma DbContexts w swoim Wzorcu repozytorium” i tworzy nowe wystąpienie na zapytanie do bazy danych, więc X * DbContext na żądanie

Po prostu pobierają swoje dane i usuwają kontekst jak najszybciej. WIELU ludzi uważa to za dopuszczalną praktykę. Zaletą tego jest zajmowanie zasobów bazy danych przez minimalny czas, ale wyraźnie poświęca całą pracę UnitOfWork i buforowanie EF cukierki .

Utrzymanie przy życiu pojedynczej, wielozadaniowej instancji DbContext maksymalizuje korzyści buforowania, ale ponieważ DbContext nie jest bezpieczny dla wątków, a każde żądanie WWW działa w swoim własnym wątku, DbContext na żądanie jest najdłuższy możliwych.

Tak więc zalecenie zespołu EF dotyczące używania kontekstu 1 Db na żądanie jest wyraźnie oparte na fakcie, że w aplikacji sieciowej UnitOfWork najprawdopodobniej znajdzie się w jednym żądaniu i to żądanie ma jeden wątek. Tak więc jeden DbContext na żądanie jest idealną zaletą UnitOfWork i buforowania.

Ale w wielu przypadkach nie jest to prawdą. I rozważyć Logging oddzielny UnitOfWork zatem o nowy DbContext dla post-Request zalogowaniu asynchronicznych wątków jest całkowicie akceptowalne

Wreszcie okazuje się, że czas życia DbContext jest ograniczony do tych dwóch parametrów. UnitOfWork and Thread


3
Szczerze mówiąc, twoje żądania HTTP powinny kończyć się dość szybko (kilka ms). Jeśli trwają dłużej, możesz pomyśleć o przetwarzaniu w tle za pomocą zewnętrznego harmonogramu zadań, aby żądanie mogło natychmiast wrócić. To powiedziawszy, twoja architektura nie powinna tak naprawdę polegać na HTTP. Ogólnie jednak dobra odpowiedź.
zmiażdżyć

22

Jestem pewien, że dzieje się tak, ponieważ DbContext wcale nie jest bezpieczny dla wątków. Dlatego dzielenie się tym nigdy nie jest dobrym pomysłem.


Czy masz na myśli udostępnianie go w żądaniach HTTP, nigdy nie jest dobrym pomysłem?
Andrew

2
Tak, Andrew to miał na myśli. Udostępnianie kontekstu dotyczy tylko aplikacji komputerowych z jednym wątkiem.
Elisabeth

10
Co powiesz na udostępnianie kontekstu dla jednego żądania. Czyli dla jednego żądania możemy mieć dostęp do różnych repozytoriów i dokonywać transakcji między nimi, udostępniając jeden i ten sam kontekst?
Lyubomir Velchev

16

Jedną z rzeczy, które tak naprawdę nie zostały poruszone w pytaniu lub dyskusji, jest fakt, że DbContext nie może anulować zmian. Możesz przesyłać zmiany, ale nie możesz wyczyścić drzewa zmian, więc jeśli używasz kontekstu na żądanie, masz pecha, jeśli chcesz odrzucić zmiany z jakiegokolwiek powodu.

Osobiście tworzę instancje DbContext w razie potrzeby - zazwyczaj dołączone do komponentów biznesowych, które mają możliwość odtworzenia kontekstu, jeśli jest to wymagane. W ten sposób mam kontrolę nad procesem, zamiast narzucania mi jednej instancji. Nie muszę też tworzyć DbContext przy każdym uruchomieniu kontrolera, niezależnie od tego, czy faktycznie zostanie wykorzystany. Następnie, jeśli nadal chcę mieć instancje na żądanie, mogę je utworzyć w CTOR (przez DI lub ręcznie) lub utworzyć je w razie potrzeby w każdej metodzie kontrolera. Osobiście zazwyczaj stosuję to drugie podejście, aby uniknąć tworzenia wystąpień DbContext, gdy nie są one faktycznie potrzebne.

To zależy, pod jakim kątem też na to spojrzysz. Dla mnie instancja na żądanie nigdy nie miała sensu. Czy DbContext naprawdę należy do żądania HTTP? Pod względem zachowania jest to złe miejsce. Komponenty biznesowe powinny tworzyć kontekst, a nie żądanie HTTP. Następnie możesz utworzyć lub wyrzucić komponenty biznesowe w razie potrzeby i nigdy nie martwić się o czas życia kontekstu.


1
To interesująca odpowiedź i częściowo się z tobą zgadzam. Dla mnie DbContext nie musi być powiązany z żądaniem internetowym, ale zawsze JEST wpisany do pojedynczego „żądania” jak w: „transakcji biznesowej”. A kiedy powiązasz kontekst z transakcją biznesową, anulowanie zmian stanie się naprawdę dziwne. Ale nie posiadanie go na granicy żądań internetowych nie oznacza, że ​​komponenty biznesowe (BC) powinny tworzyć kontekst; Myślę, że to nie jest ich odpowiedzialność. Zamiast tego możesz zastosować scoping przy użyciu dekoratorów wokół swoich BC. W ten sposób możesz nawet zmienić zakres bez zmiany kodu.
Steven

1
W takim przypadku wstrzyknięcie do obiektu biznesowego powinno zająć się zarządzaniem cyklem życia. Moim zdaniem obiekt biznesowy jest właścicielem kontekstu i jako taki powinien kontrolować czas życia.
Rick Strahl,

W skrócie, co masz na myśli, mówiąc „zdolność do odtworzenia kontekstu, jeśli jest to wymagane”? czy wyrzucasz własną zdolność wycofywania? potrafisz opracować odrobinę?
tntwyckoff,

2
Osobiście uważam, że wymuszenie DbContext na początku jest trochę kłopotliwe. Nie ma gwarancji, że musisz nawet trafić do bazy danych. Być może dzwonisz do innej firmy, która zmienia stan po tej stronie. A może faktycznie masz 2 lub 3 bazy danych, z którymi pracujesz w tym samym czasie. Na początku nie utworzyłbyś wiązki DbContexts na wypadek, gdybyś z nich skorzystał. Firma zna dane, z którymi współpracuje, więc do tego należy. Po prostu umieść TransactionScope na początku, jeśli jest to potrzebne. Nie sądzę, że wszystkie połączenia potrzebują jednego. To zajmuje zasoby.
Daniel Lorenz,

To pytanie, czy pozwalasz kontenerowi kontrolować żywotność kontekstu db, który następnie kontroluje żywotność kontroli rodzicielskiej, czasem niesłusznie. Powiedzmy, że jeśli chcę, aby do kontrolerów został wstrzyknięty prosty singleton usługi, to nie będę mógł użyć wstrzykiwania konstruktora z powodu semantycznego żądania.
davidcarr

10

Zgadzam się z wcześniejszymi opiniami. Dobrze jest powiedzieć, że jeśli zamierzasz udostępniać DbContext w aplikacji jednowątkowej, potrzebujesz więcej pamięci. Na przykład moja aplikacja internetowa na platformie Azure (jedna bardzo mała instancja) potrzebuje kolejnych 150 MB pamięci i mam około 30 użytkowników na godzinę. Udostępnianie aplikacji DBContext w żądaniu HTTP

Oto prawdziwy przykładowy obraz: aplikacja została wdrożona o godzinie 12:00


Być może chodzi o udostępnienie kontekstu dla jednego żądania. Jeśli uzyskujemy dostęp do różnych repozytoriów i klas DBSet i chcemy, aby operacje z nimi były transakcyjne, powinno to być dobre rozwiązanie. Spójrz na projekt mvcforum.com o otwartym kodzie źródłowym. Myślę, że zostało to zrobione podczas wdrażania wzorca projektowego Unit Of Work.
Lyubomir Velchev

3

Podoba mi się to, że wyrównuje jednostkę pracy (tak, jak widzi ją użytkownik - tj. Przesłanie strony) z jednostką pracy w sensie ORM.

Dlatego możesz sprawić, że przesyłanie całej strony będzie transakcyjne, czego nie możesz zrobić, jeśli ujawniasz metody CRUD przy każdym tworzeniu nowego kontekstu.


2

Innym niedocenianym powodem nie używania pojedynczego DbContext, nawet w aplikacji z pojedynczym wątkiem dla jednego użytkownika, jest wzorzec mapy tożsamości, którego używa. Oznacza to, że za każdym razem, gdy pobierasz dane za pomocą zapytania lub identyfikatora, zachowa on pobrane instancje w pamięci podręcznej. Następnym razem, gdy odzyskasz ten sam byt, otrzymasz buforowane wystąpienie bytu, jeśli jest dostępne, wraz ze wszelkimi modyfikacjami, które wprowadziłeś w tej samej sesji. Jest to konieczne, aby metoda SaveChanges nie kończyła się na wielu różnych instancjach tego samego rekordu (ów) bazy danych; w przeciwnym razie kontekst musiałby w jakiś sposób scalić dane ze wszystkich tych instancji.

Przyczyną tego problemu jest singleton DbContext może stać się bombą zegarową, która ostatecznie może buforować całą bazę danych + obciążenie obiektów .NET w pamięci.

Istnieje wiele sposobów na obejście tego zachowania przy użyciu tylko zapytań Linq z .NoTracking()metodą rozszerzenia. Również w dzisiejszych czasach komputery mają dużo pamięci RAM. Ale zwykle nie jest to pożądane zachowanie.


To prawda, ale musisz założyć, że Garbage Collector będzie działał, dzięki czemu ten problem będzie bardziej wirtualny niż rzeczywisty.
tocqueville

2
Garbage collector nie będzie zbierał żadnych instancji obiektów przechowywanych przez aktywny obiekt static / singleton. Skończą w drugiej generacji stosu.
Dmitry S.

1

Inną kwestią, na którą należy zwrócić uwagę w Entity Framework, jest szczególnie użycie kombinacji tworzenia nowych encji, leniwego ładowania, a następnie korzystania z tych nowych encji (z tego samego kontekstu). Jeśli nie używasz IDbSet.Create (w porównaniu z nowymi), Leniwe ładowanie tego obiektu nie działa, gdy zostanie pobrane z kontekstu, w którym został utworzony. Przykład:

 public class Foo {
     public string Id {get; set; }
     public string BarId {get; set; }
     // lazy loaded relationship to bar
     public virtual Bar Bar { get; set;}
 }
 var foo = new Foo {
     Id = "foo id"
     BarId = "some existing bar id"
 };
 dbContext.Set<Foo>().Add(foo);
 dbContext.SaveChanges();

 // some other code, using the same context
 var foo = dbContext.Set<Foo>().Find("foo id");
 var barProp = foo.Bar.SomeBarProp; // fails with null reference even though we have BarId set.
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.