Один DbContext на веб-запит ... чому?


398

Я читав багато статей, що пояснюють, як налаштувати Entity Framework DbContextтак, щоб створити та використовувати лише один веб-запит HTTP, використовуючи різні рамки DI.

Чому це в першу чергу гарна ідея? Які переваги ви отримуєте, використовуючи такий підхід? Чи є певні ситуації, коли це було б гарною ідеєю? Чи є речі, які ви можете зробити за допомогою цієї методики, яку ви не можете зробити, коли інстанціює DbContextвиклик методу для кожного сховища?


9
Gueddari в mehdi.me/ambient-dbcontext-in-ef6 викликає екземпляр DbContext за методом репозиторію викликом антипаттера. Цитата: "Роблячи це, ви втрачаєте майже кожну функцію, яку надає Entity Framework через DbContext, включаючи кеш 1-го рівня, свою ідентифікаційну карту, свою одиницю роботи та відстеження змін та здатність до ледачого завантаження. . " Відмінна стаття з чудовими пропозиціями щодо обробки життєвого циклу DBContexts. Однозначно варто прочитати.
Крістоф

Відповіді:


564

ПРИМІТКА: Ця відповідь говорить про цілісні рамки Entity Framework DbContext, але вона застосовна для будь-якої реалізації Блоку роботи, наприклад LINQ для SQL DataContextта NHibernate ISession.

Почнемо з наголосу Ian: Мати сингла DbContextдля всієї програми - погана ідея. Єдина ситуація, коли це має сенс, коли у вас є однопотоковий додаток та база даних, яка використовується виключно цим єдиним екземпляром програми. Це DbContextне є безпечним для потоків, і, оскільки DbContextкешує дані, він стає несвіжим досить скоро. Це доставить вам всілякі проблеми, коли на цій базі даних одночасно працюють декілька користувачів / додатків (що, звичайно, дуже часто). Але я сподіваюся, що ви вже це знаєте, і просто хочете знати, чому б не просто ввести новий екземпляр (тобто з перехідним способом життя) того, DbContextхто цього потребує. (для отримання додаткової інформації про те, чому один DbContext- або навіть за контекстом на потоку - поганий, прочитайте цю відповідь ).

Почніть з того, що реєстрація DbContextяк перехідного періоду може працювати, але, як правило, ви хочете мати один екземпляр такої одиниці роботи в певному обсязі. У веб-додатку може бути практичним визначення такої сфери застосування на межах веб-запиту; таким чином спосіб життя за запитом на веб. Це дозволяє дозволити цілому набору об'єктів діяти в одному контексті. Іншими словами, вони діють в межах однієї ділової операції.

Якщо у вас немає мети, щоб набір операцій діяв у тому ж контексті, у такому випадку перехідний спосіб життя прекрасний, але слід спостерігати кілька речей:

  • Оскільки кожен об'єкт отримує власний екземпляр, кожному класу, який змінює стан системи, потрібно викликати _context.SaveChanges()(інакше зміни втратяться). Це може ускладнити ваш код, і додасть до нього другу відповідальність (відповідальність за контроль за контекстом) і є порушенням Принципу єдиної відповідальності .
  • Вам потрібно переконатися, що сутності [завантажені та збережені a DbContext] ніколи не залишають область такого класу, оскільки вони не можуть бути використані в контекстному екземплярі іншого класу. Це може значно ускладнити ваш код, адже коли вам потрібні ці сутності, вам потрібно знову завантажити їх за допомогою id, що також може спричинити проблеми з продуктивністю.
  • З моменту DbContextреалізації IDisposableви, ймовірно, все ще хочете розпоряджатись усіма створеними екземплярами. Якщо ви хочете це зробити, у вас в основному є два варіанти. Розпоряджувати їх потрібно тим же методом відразу після виклику context.SaveChanges(), але в цьому випадку бізнес-логіка приймає право власності на об'єкт, який він передає зовні. Другий варіант - розмістити всі створені екземпляри на кордоні Http-запиту, але в цьому випадку вам все-таки потрібне якесь визначення, щоб повідомити контейнер, коли ці екземпляри потрібно розмістити.

Інший варіант - взагалі не робити ін’єкцій DbContext. Натомість ви вводите ін, DbContextFactoryякий здатний створити новий екземпляр (я раніше використовував такий підхід). Таким чином бізнес-логіка чітко контролює контекст. Якщо це може виглядати так:

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

        context.Entities.InsertOnSubmit(entities);

        context.SaveChanges();
    }
}

Плюсом цього є те, що ви керуєте життям DbContextявно, і це легко налаштувати. Це також дозволяє використовувати один контекст у певній області, що має чіткі переваги, наприклад, запуск коду в одній бізнес-транзакції та можливість передачі об'єктів, оскільки вони походять від одного і того ж DbContext.

Мінусом є те, що вам доведеться пройти шлях DbContextвід методу до методу (який називається Методом ін'єкції). Зауважте, що в певному сенсі це рішення є тим самим, що і "підхідний" підхід, але тепер сфера застосування контролюється в самому коді програми (і можливо повторюється багато разів). Саме додаток відповідає за створення та розпорядження одиницею роботи. Оскільки значення DbContextстворено після побудови графіка залежності, інжекція конструктора виходить із зображення, і вам потрібно перейти до методу ін'єкції, коли вам потрібно передати контекст від одного класу до іншого.

Інжекція методів не така вже й погана, але коли бізнес-логіка стає більш складною, і більше класів буде задіяно, вам доведеться передати її від методу до методу та класу до класу, що може значно ускладнити код (я бачив це в минулому). Для простого застосування, цей підхід буде добре, хоча.

Через недоліки цей заводський підхід є для більш великих систем, інший підхід може бути корисним, і це той, де ви дозволяєте контейнеру або інфраструктурному коду / Composite Root керувати одиницею роботи. Це стиль, про який йдеться у вашому питанні.

Дозволяючи контейнеру та / або інфраструктурі впоратися з цим, ваш додаток не забруднюється необхідністю створювати, (необов'язково) виконувати і видаляти екземпляр UoW, який забезпечує простоту та чистоту бізнес-логіки (лише Єдина відповідальність). Є певні труднощі з таким підходом. Наприклад, чи вчинили ви та розпоряджалися екземпляром?

Розпорядження одиницею роботи може бути виконано в кінці веб-запиту. Однак багато людей помилково припускають, що це також місце для здійснення одиниці роботи. Однак, на цьому етапі програми ви просто не можете точно визначити, що одиниця роботи має бути фактично виконана. Наприклад, якщо код бізнес-шару викинув виняток, який потрапив вище в стопку виклику, ви точно не хочете взяти на себе зобов’язання.

Справжнє рішення знову полягає в тому, щоб явно керувати якоюсь сферою дії, але цього разу зробити це всередині Composite Root. Абстрагуючи всю ділову логіку за схемою команд / обробника , ви зможете написати декоратор, який можна обернути навколо кожного обробника команд, який дозволяє це зробити. Приклад:

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

Це гарантує, що цей код інфраструктури потрібно написати лише один раз. Будь-який твердий контейнер DI дозволяє вам налаштувати такий декоратор, щоб ICommandHandler<T>послідовно обертатися навколо всіх реалізацій.


2
Вау - спасибі за ретельну відповідь. Якби я міг висловити два рази, я б. Вище ви говорите: "... не має наміру дозволяти цілому набору операцій діяти в одному контексті; у цьому випадку перехідний спосіб життя прекрасний ...". Що конкретно ви маєте на увазі під перехідним?
Андрій

14
@Andrew: "Тимчасовий" - це концепція введення залежності, яка означає, що якщо служба налаштована на тимчасовий характер, кожен примірник послуги створюється кожного разу, коли вона вводиться споживачеві.
Стівен

1
@ user981375: Для операцій з CRUD ви можете створити загальний CreateCommand<TEnity>і загальний CreateCommandHandler<TEntity> : ICommandHandler<CreateCommand<TEntity>>(і зробити те ж саме для «Оновлення» та «Видалити» і мав один GetByIdQuery<TEntity>запит). Тим не менш, слід запитати себе, чи ця модель є корисною абстракцією для операцій CRUD, чи вона лише додає складності. Тим не менш, ви можете скористатися можливістю легко додавати наскрізні проблеми (через декораторів) за допомогою цієї моделі. Вам доведеться зважити плюси і мінуси.
Стівен

3
+1 Чи повірите ви, що я написав всю цю відповідь, перш ніж її прочитати? BTW IMO Я думаю, що для вас важливо обговорити видалення DbContext наприкінці (хоча це чудово, що ви залишаєтеся агностиком для контейнерів)
Рубен Бартелінк,

1
Але ви не передаєте контекст оформленому класу, як оформлений клас міг би працювати з тим самим контекстом, що перейшов до TransactionCommandHandlerDecorator? наприклад, якщо оформлений клас буде InsertCommandHandlerкласом, як він може зареєструвати операцію вставки в контекст (DbContext в EF)?
Масуд

34

Тут жодна відповідь насправді не відповідає на питання. ОП не запитувала про дизайн DbContext для одиночного / за додатком, він запитував про дизайн запиту на замовлення та в Інтернеті та які потенційні переваги можуть існувати.

Я посилаюсь на http://mehdi.me/ambient-dbcontext-in-ef6/, оскільки Мехді - фантастичний ресурс:

Можливі підвищення продуктивності.

Кожен екземпляр DbContext підтримує кеш першого рівня всіх об'єктів, які завантажують його з бази даних. Щоразу, коли ви запитуєте сутність за її первинним ключем, DbContext спочатку намагатиметься отримати її з кешу першого рівня, перш ніж дефолтувати запит із бази даних. Залежно від вашої схеми запитів даних, повторне використання одного і того ж DbContext для кількох послідовних бізнес-транзакцій може призвести до меншої кількості запитів у базі даних завдяки кешу DbContext першого рівня.

Це дає можливість ледачим завантаженням.

Якщо ваші служби повертають стійкі сутності (на відміну від повертаються моделей перегляду чи інших типів DTO), і ви хочете скористатись ледачим завантаженням цих об'єктів, термін служби екземпляра DbContext, з якого були отримані ці об'єкти, повинен перевищувати обсяг господарської операції. Якщо сервісний метод позбувся екземпляра DbContext, який він використовував перед поверненням, будь-яка спроба лінивого завантаження властивостей повернених об'єктів виявиться невдалою (незалежно від того, використання ленивого завантаження - хороша ідея - це зовсім інша дискусія, в яку ми не потрапимо тут). У нашому прикладі веб-додатків, ледачі завантаження зазвичай використовуються в методах дії контролера для об'єктів, повернених окремим службовим рівнем. В такому разі,

Майте на увазі, що є і мінуси. Це посилання містить багато інших ресурсів для читання з цього питання.

Просто опублікуйте це у випадку, якщо хтось інший натикається на це питання і не поглинеться відповідями, які насправді не вирішують питання.


Гарне посилання! Явне управління DBContext виглядає як найбезпечніший підхід.
аггсол

34

Існують дві суперечливі рекомендації Microsoft, і багато людей використовують DbContexts абсолютно розбіжно.

  1. Однією з рекомендацій є "розпорядження DbContexts якнайшвидше", тому що наявність DbContext Alive займає цінні ресурси, як db-з'єднання тощо.
  2. Інший стверджує, що один DbContext на запит дуже рекомендований

Вони суперечать один одному, тому що якщо ваш запит робить багато, що не стосується Db, то ваш DbContext зберігається без причини. Таким чином, марно тримати ваш DbContext живим, поки ваш запит просто чекає, коли випадкові речі будуть закінчені ...

Так багато людей, які дотримуються правила 1, мають свої DbContexts всередині " Шаблону сховища" і створюють новий екземпляр на запит бази даних, так що X * DbContext за запитом

Вони просто отримують свої дані і розміщують контекст якомога швидше. Багато людей це вважає прийнятною практикою. У той час як це має переваги займати свої ресурси БД за мінімальний час вона явно жертвує все UnitOfWork і Caching цукерки EF запропонувати.

Залишаючи в живих один багатоцільовий екземпляр DbContext максимізує переваги кешування, але оскільки DbContext не є безпечним для потоків, і кожен веб-запит працює на власній потоці, DbContext за запитом є найдовшим, коли ви можете його зберегти.

Отже, рекомендація команди EF щодо використання 1 Db Context на запит очевидно базується на тому, що у веб-додатку UnitOfWork, швидше за все, буде входити в один запит, і цей запит має один потік. Отож, один DbContext за запит є ідеальною перевагою UnitOfWork та кешування.

Але у багатьох випадках це неправда. Я вважаю, що ведення журналу є окремим UnitOfWork, завдяки чому новий DbContext для внесення запиту після запиту в потоки асинхронізації є цілком прийнятним

Отже, нарешті, виявляється, що термін служби DbContext обмежений цими двома параметрами. UnitOfWork and Thread


3
Чесно кажучи, ваші HTTP-запити повинні закінчуватися досить швидко (декілька мс). Якщо вони триватимуть довше, то, можливо, ви захочете подумати про те, щоб зробити обробку фону з чимось на зразок зовнішнього планувальника робіт, щоб запит міг повернутися негайно. При цьому ваша архітектура також не повинна покладатися на HTTP. Загалом, хоч і відповідь хороша.
розчавити

22

Я впевнений, це тому, що DbContext зовсім не є безпечним для потоків. Тож ділитися річчю ніколи не є хорошою ідеєю.


Ви хочете сказати, що обмін ним через HTTP-запити ніколи не є хорошою ідеєю?
Андрій

2
Так, Ендрю, це він мав на увазі. Спільний доступ до контексту призначений лише для настільних додатків з одним потоком.
Елізабет

10
Як щодо обміну контекстом для одного запиту. Отже, для одного запиту ми можемо мати доступ до різних сховищ та здійснювати транзакції через них, обмінюючись одним і тим же контекстом?
Любомир Велчев

16

Одне, що насправді не вирішено у питанні чи дискусії, - це той факт, що DbContext не може скасувати зміни. Ви можете надіслати зміни, але ви не можете очистити дерево змін, тому якщо ви використовуєте контекст запиту, вам не пощастить, якщо вам потрібно викинути зміни з будь-якої причини.

Особисто я створюю екземпляри DbContext при необхідності - зазвичай додаються до бізнес-компонентів, які мають можливість відтворити контекст, якщо це потрібно. Таким чином я маю контроль над процесом, а не на тому, щоб на мене примусовий один екземпляр. Мені також не потрібно створювати DbContext при кожному запуску контролера незалежно від того, чи він насправді звикає. Тоді, якщо я все ще хочу мати екземпляри запиту, я можу створити їх у CTOR (через DI або вручну) або створити їх у міру необхідності у кожному методі контролера. Я особисто використовую останній підхід, щоб уникнути створення примірників DbContext, коли вони насправді не потрібні.

Це залежить від того, під яким кутом ви також дивитесь. Для мене екземпляр на запит ніколи не мав сенсу. Чи дійсно DbContext належить до Http-запиту? З точки зору поведінки це неправильне місце. Компоненти вашого бізнесу повинні створювати ваш контекст, а не Http-запит. Тоді ви можете створити або викинути бізнес-компоненти за потребою і ніколи не турбуватися про життя контексту.


1
Це цікава відповідь, і я частково з вами згоден. Для мене DbContext не повинен прив'язуватися до веб-запиту, але він завжди набирається одним "запитом", як у: "бізнес-транзакція". І коли ви прив’язуєте контекст до ділової транзакції, скасування змін стає справді дивно. Але відсутність його на кордоні веб-запитів не означає, що бізнес-компоненти (BC) повинні створювати контекст; Я думаю, що це не їхня відповідальність. Натомість ви можете застосувати масштабування за допомогою декораторів навколо ваших ОВ. Таким чином ви навіть можете змінити масштабування без будь-якої зміни коду.
Стівен

1
Добре, що в цьому випадку введення в бізнес-об'єкт має стосуватися управління протягом усього життя. На мій погляд, бізнес-об’єкту належить контекст і як такий повинен контролювати все життя.
Рік Страль

Якщо коротко, що ви маєте на увазі, коли ви говорите "здатність відтворити контекст, якщо потрібно"? Ви прокатуєте свою здатність до відкату? Ви можете розробити теда?
tntwyckoff

2
Особисто я думаю, що змусити DbContext на старті там трохи клопітно. Немає гарантії, що вам навіть потрібно потрапити в базу даних. Можливо, ви телефонуєте сторонній службі, яка змінює стан на цій стороні. Або, можливо, у вас є одночасно 2 або 3 бази даних, з якими ви працюєте одночасно. Ви б не створили купу DbContexts на початку лише у випадку, якщо ви в кінцевому підсумку використовуєте їх. Бізнес знає дані, з якими працює, тому їм належить. Просто поставте TransactionScope на початок, якщо це потрібно. Я не думаю, що всі дзвінки потребують одного. Це забирає ресурси.
Даніель Лоренц

Це питання про те, чи дозволяєте ви контейнеру контролювати час життя dbcontext, який потім керує життям батьківських елементів управління, іноді безпідставно. Скажіть, якщо я хочу, щоб у мої контролери вводився простий сервісний сингл, тоді я не зможу використовувати введення конструктора через семантичний запит.
davidcarr

10

Я згоден з попередніми думками. Добре сказати, що якщо ви збираєтеся ділитися DbContext в додатку з одним потоком, вам буде потрібно більше пам’яті. Наприклад, моєму веб-застосунку в Azure (одному додатковому невеликому екземпляру) потрібно ще 150 Мб пам'яті, і у мене близько 30 користувачів на годину. Обмін додатками DBContext у запиті HTTP

Ось реальний приклад зображення: додаток було розгорнуто в 12:00


Можливо, ідея полягає у тому, щоб поділитися контекстом для одного запиту. Якщо ми отримуємо доступ до різних сховищ та - класів DBSet і хочемо, щоб операції з ними були транзакційними, це повинно бути хорошим рішенням. Погляньте на проект з відкритим кодом mvcforum.com Я думаю, що це робиться під час їхньої реалізації дизайнерської моделі Unit Of Work.
Любомир Велчев

3

Мені подобається, що він узгоджує одиницю роботи (як бачить користувач - тобто подання сторінки) з одиницею роботи в сенсі ORM.

Отже, ви можете зробити подання всієї сторінки транзакційною, чого ви не могли зробити, якби викривали методи CRUD при кожному створенні нового контексту.


2

Ще одна занижена причина не використання однопоточного DbContext навіть у єдиному потоковому додатку для одного користувача - через шаблон карти ідентичності, який він використовує. Це означає, що кожного разу, коли ви отримуєте дані за допомогою запиту або за допомогою id, вони зберігатимуть отримані екземпляри сутності в кеші. Наступного разу, коли ви отримаєте ту саму сутність, вона надасть вам кешований екземпляр об'єкта, якщо він доступний, з будь-якими модифікаціями, які ви зробили в одному сеансі. Це необхідно, щоб метод SaveChanges не закінчувався кількома різними екземплярами сутності одних і тих же записів бази даних; в іншому випадку контекст повинен був би якось об'єднати дані з усіх цих екземплярів сутності.

Причиною, що є проблемою, є однотонний DbContext, який може стати тимчасовою бомбою, яка в кінцевому підсумку може кешувати всю базу даних + накладні витрати об'єктів .NET в пам'яті.

Існують способи такої поведінки, використовуючи лише запити Linq .NoTracking()методом розширення. Також в ці дні ПК мають багато оперативної пам'яті. Але зазвичай це не бажана поведінка.


Це правильно, але ви повинні припустити, що колектор сміття працюватиме, роблячи цю проблему більш віртуальною, ніж реальною.
tocqueville

2
Збирач сміття не збирається збирати жодні екземпляри об'єктів, що утримуються активним статичним / однотонним об'єктом. Вони опиняться в ген 2 купи.
Дмитро С.

1

Ще одне питання, на який слід звернути увагу на Entity Framework, - це використання комбінації створення нових сутностей, ледачого завантаження та використання цих нових об'єктів (з того ж контексту). Якщо ви не використовуєте IDbSet.Create (порівняно з новим), Lazy завантаження цього об'єкта не працює, коли його витягнуто з контексту, в якому він створений. Приклад:

 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.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.