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

398

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

Почему это хорошая идея в первую очередь? Какие преимущества вы получаете, используя этот подход? Есть ли определенные ситуации, когда это было бы хорошей идеей? Есть ли вещи, которые вы можете сделать с помощью этой техники, которые вы не можете сделать при создании экземпляра DbContexts для каждого вызова метода репозитория?

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

Ответы:

565

ПРИМЕЧАНИЕ. В этом ответе говорится о Entity Framework DbContext, но он применим к любому виду реализации Unit of Work, например LINQ to SQL DataContextи NHibernate ISession.

Давайте начнем с того, что повторю Йену: иметь сингл DbContextдля всего приложения - плохая идея. Единственная ситуация, в которой это имеет смысл, - это когда у вас есть однопоточное приложение и база данных, которая используется только этим единственным экземпляром приложения. Он DbContextне является поточно-ориентированным и, поскольку DbContextданные кэшируются, довольно скоро устареет. Это доставит вам массу неприятностей, когда несколько пользователей / приложений будут работать с этой базой данных одновременно (что, конечно, очень часто). Но я ожидаю, что вы уже знаете это и просто хотите знать, почему бы просто не внедрить новый экземпляр (то есть с временным образом жизни) в того, DbContextкто в этом нуждается. (для получения дополнительной информации о том, почему одиночный DbContext- или даже контекст на поток - плохо, прочитайте этот ответ ).

Позвольте мне начать с того, что регистрация в DbContextкачестве переходного процесса может работать, но, как правило, вы хотите иметь один экземпляр такой единицы работы в определенной области. В веб-приложении может оказаться целесообразным определить такую ​​область на границах веб-запроса; таким образом, на веб-запрос стиль жизни. Это позволяет вам позволить целому набору объектов работать в одном контексте. Другими словами, они работают в рамках одной бизнес-операции.

Если у вас нет цели, чтобы набор операций работал в одном и том же контексте, в этом случае переходный образ жизни хорош, но есть несколько вещей, на которые стоит обратить внимание:

  • Поскольку каждый объект получает свой собственный экземпляр, каждый класс, который изменяет состояние системы, должен вызываться _context.SaveChanges()(в противном случае изменения будут потеряны). Это может усложнить ваш код и добавить к нему вторую ответственность (ответственность за контроль контекста) и является нарушением принципа единой ответственности .
  • Вы должны убедиться, что сущности [загруженные и сохраненные 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от метода к методу (который называется инъекцией метода). Обратите внимание, что в некотором смысле это решение аналогично подходу «scoped», но теперь область действия контролируется в самом коде приложения (и, возможно, повторяется много раз). Это приложение, которое отвечает за создание и распоряжение единицей работы. Так как DbContextобъект создается после построения графа зависимостей, добавление в конструктор находится вне пределов видимости, и вам необходимо перейти к внедрению метода, когда вам нужно передать контекст из одного класса в другой.

Инъекция метода не так уж и плоха, но когда бизнес-логика усложняется и в нее вовлекается больше классов, вам придется передавать ее из метода в метод и из класса в класс, что может сильно усложнить код (я видел это в прошлом). Однако для простого приложения этот подход подойдет.

Из-за недостатков этот фабричный подход для больших систем может быть полезен другой подход, в котором вы позволяете контейнеру или коду инфраструктуры / Composition Root управлять единицей работы. Это стиль, о котором ваш вопрос.

Позволяя контейнеру и / или инфраструктуре обрабатывать это, код вашего приложения не загрязняется необходимостью создавать, (необязательно) фиксировать и утилизировать экземпляр UoW, что делает бизнес-логику простой и чистой (просто единая ответственность). Есть некоторые трудности с этим подходом. Например, вы фиксировали и утилизировали экземпляр?

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

Реальное решение снова состоит в том, чтобы явно управлять некоторой областью действия, но на этот раз сделайте это внутри корня композиции. Абстрагируясь от всей бизнес-логики, лежащей в основе шаблона команда / обработчик , вы сможете написать декоратор, который можно обернуть вокруг каждого обработчика команды, который позволяет это делать. Пример:

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: «Transient» - это концепция внедрения зависимостей, которая означает, что если служба настроена как временная, новый экземпляр службы создается каждый раз, когда она внедряется в потребителя.
Стивен
1
@ user981375: Для операций CRUD вы можете создать универсальный CreateCommand<TEnity>и универсальный CreateCommandHandler<TEntity> : ICommandHandler<CreateCommand<TEntity>>(и сделать то же самое для Обновления и Удалить и иметь один GetByIdQuery<TEntity>запрос). Тем не менее, вы должны спросить себя, является ли эта модель полезной абстракцией для операций CRUD, или она просто добавляет сложности. Тем не менее, вы можете извлечь выгоду из возможности легко добавить сквозные задачи (через декораторов) с помощью этой модели. Вам придется взвесить все за и против.
Стивен
3
+1 Верите ли вы, что я написал весь этот ответ, прежде чем прочесть это? КСТАТИ ИМО Я думаю, что для вас важно в конце обсудить утилизацию DbContext (хотя здорово, что вы остаетесь
вне зависимости от
1
Но вы не передаете контекст декорированному классу, как декорированный класс может работать с тем же контекстом, который был передан в TransactionCommandHandlerDecorator? например, если декорированный класс является InsertCommandHandlerклассом, как он может зарегистрировать операцию вставки в контекст (DbContext в EF)?
Масуд
35

Есть две противоречивые рекомендации от Microsoft, и многие люди используют DbContexts совершенно по-разному.

  1. Одна рекомендация состоит в том, чтобы «утилизировать DbContexts как можно скорее», потому что наличие DbContext Alive занимает ценные ресурсы, такие как соединения db и т. Д.
  2. В другом говорится, что рекомендуется использовать один DbContext на запрос.

Они противоречат друг другу, потому что, если ваш Запрос в значительной степени не связан с Db, то ваш DbContext сохраняется без причины. Таким образом, тратить ваш DbContext на жизнь бесполезно, пока ваш запрос просто ожидает выполнения случайных вещей ...

Так много людей, которые следуют правилу 1, имеют свои DbContexts внутри своего «шаблона репозитория» и создают новый экземпляр для каждого запроса к базе данных, поэтому X * DbContext для запроса

Они просто получают свои данные и располагают контекстом как можно скорее. Это, по мнению МНОГИХ людей приемлемой практики. Хотя это имеет преимущество в том, что вы занимаете ресурсы БД в течение минимального времени, оно явно жертвует всем, что может предложить EF UnitOfWork и Caching .

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

Таким образом, рекомендация команды EF об использовании 1 Db Context на запрос явно основана на том факте, что в веб-приложении UnitOfWork, скорее всего, будет в пределах одного запроса, и этот запрос имеет один поток. Таким образом, один DbContext на запрос подобен идеальному преимуществу UnitOfWork и Caching.

Но во многих случаях это не так. Я считаю Logging отдельным UnitOfWork, таким образом, наличие нового DbContext для Post-Request Logging в асинхронных потоках вполне приемлемо

Наконец, получается, что время жизни DbContext ограничено этими двумя параметрами. UnitOfWork and Thread

Анестис Кивраноглу
источник
3
Честно говоря, ваши HTTP-запросы должны заканчиваться довольно быстро (несколько мс). Если они идут дольше, вам может потребоваться выполнить некоторую фоновую обработку с помощью внешнего планировщика заданий, чтобы запрос мог немедленно вернуться. Тем не менее, ваша архитектура также не должна полагаться на HTTP. В целом, хороший ответ, хотя.
раздавить
34

Ни один ответ здесь на самом деле не отвечает на вопрос. ОП не спрашивал о дизайне DbContext для одного приложения / приложения, он спрашивал о дизайне запроса для каждого веб-сайта и о том, какие потенциальные выгоды могут существовать.

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

Возможное повышение производительности.

Каждый экземпляр DbContext поддерживает кэш первого уровня всех объектов, которые он загружает из базы данных. Всякий раз, когда вы запрашиваете сущность по ее первичному ключу, DbContext сначала пытается извлечь ее из кэша первого уровня, а затем по умолчанию запрашивает ее из базы данных. В зависимости от шаблона запроса данных повторное использование одного и того же DbContext в нескольких последовательных бизнес-транзакциях может привести к уменьшению количества запросов к базе данных благодаря кэш-памяти первого уровня DbContext.

Это позволяет ленивую загрузку.

Если ваши службы возвращают постоянные сущности (в отличие от возвращаемых моделей представлений или других типов DTO), и вы хотите воспользоваться отложенной загрузкой этих сущностей, время жизни экземпляра DbContext, из которого были извлечены эти сущности, должно выходить за пределы объем бизнес-транзакции. Если сервисный метод утилизирует экземпляр DbContext, который он использовал перед возвратом, любая попытка ленивой загрузки свойств в возвращаемых сущностях потерпит неудачу (является ли хорошая идея использовать ленивую загрузку - это совсем другой спор, в который мы не будем вдаваться) Вот). В нашем примере веб-приложения ленивая загрузка обычно используется в методах действия контроллера для сущностей, возвращаемых отдельным сервисным уровнем. В таком случае,

Имейте в виду, что есть и минусы. Эта ссылка содержит много других ресурсов для чтения на эту тему.

Просто разместите это на тот случай, если кто-то еще наткнется на этот вопрос и не будет поглощен ответами, которые на самом деле не касаются вопроса.

user4893106
источник
Хорошая ссылка! Явное управление DBContext выглядит как самый безопасный подход.
aggsol
22

Я почти уверен, что это потому, что DbContext не является потокобезопасным. Так что делиться вещами никогда не будет хорошей идеей.

Ян
источник
Вы имеете в виду, что разделение его между HTTP-запросами никогда не является хорошей идеей?
Андрей
2
Да, Эндрю, вот что он имел в виду. Совместное использование контекста только для однопоточных настольных приложений.
Элизабет
10
Как насчет совместного использования контекста для одного запроса. Таким образом, для одного запроса мы можем иметь доступ к различным репозиториям и совершать транзакции между ними, используя один и тот же контекст?
Любомир Вельчев
16

Одна вещь, которая на самом деле не рассматривается в вопросе или обсуждении, это то, что DbContext не может отменить изменения. Вы можете отправлять изменения, но вы не можете очистить дерево изменений, поэтому, если вы используете контекст для каждого запроса, вам не повезло, если вам по какой-то причине нужно выбросить изменения.

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

Это зависит от того, под каким углом ты смотришь на это тоже. Для меня экземпляр на запрос никогда не имел смысла. Действительно ли DbContext входит в Http-запрос? С точки зрения поведения, это не то место. Ваши бизнес-компоненты должны создавать ваш контекст, а не запрос Http. Затем вы можете создавать или отбрасывать свои бизнес-компоненты по мере необходимости и никогда не беспокоиться о времени существования контекста.

Рик Страл
источник
1
Это интересный ответ, и я частично согласен с вами. На мой взгляд, DbContext не обязательно должен быть привязан к веб-запросу, но он всегда набирается в одном «запросе», как в «бизнес-транзакции». И когда вы связываете контекст с бизнес-транзакцией, отмена изменений становится действительно странной. Но отсутствие его на границе веб-запроса не означает, что бизнес-компоненты (BC) должны создавать контекст; Я думаю, что это не их ответственность. Вместо этого вы можете применить определение области видимости с помощью декораторов вокруг ваших БЦ. Таким образом, вы можете даже изменить область видимости без изменения кода.
Стивен
1
В этом случае внедрение в бизнес-объект должно быть связано с управлением жизненным циклом. На мой взгляд, бизнес-объект владеет контекстом и должен контролировать время жизни.
Рик Страл
Вкратце, что вы имеете в виду, когда говорите «способность воссоздавать контекст, если это необходимо»? Вы катите свою собственную способность отката? Вы можете разработать немного?
tntwyckoff
2
Лично я думаю, что это немного хлопотно, чтобы заставить DbContext в начале там. Там нет никакой гарантии, что вам даже нужно попасть в базу данных. Возможно, вы звоните сторонней службе, которая меняет состояние на этой стороне. Или, может быть, у вас есть две или три базы данных, с которыми вы работаете одновременно. Вы не будете создавать кучу DbContexts в начале, только если вы в конечном итоге будете их использовать. Бизнес знает данные, с которыми он работает, поэтому он связан с этим. Просто поместите TransactionScope в начало, если это необходимо. Я не думаю, что все звонки нужны. Это требует ресурсов.
Даниэль Лоренц
Вот вопрос, позволяете ли вы контейнеру управлять временем жизни dbcontext, который затем контролирует время жизни родительских элементов управления, иногда излишне. Скажем, если я хочу, чтобы в мои контроллеры вставлялся простой сервисный синглтон, я не смогу использовать конструкторский ввод из-за семантики по запросу.
Дэвидкарр
10

Я согласен с предыдущими мнениями. Хорошо сказать, что если вы собираетесь делиться DbContext в однопоточном приложении, вам потребуется больше памяти. Например, моему веб-приложению в Azure (один очень маленький экземпляр) требуется еще 150 МБ памяти, и у меня около 30 пользователей в час. Совместное использование приложения DBContext в HTTP-запросе

Вот реальный пример изображения: приложение было развернуто в 12 вечера

Мирослав Холец
источник
Возможно, идея состоит в том, чтобы поделиться контекстом для одного запроса. Если мы обращаемся к различным репозиториям и классам DBSet и хотим, чтобы операции с ними были транзакционными, это должно быть хорошим решением. Взгляните на проект с открытым исходным кодом mvcforum.com Я думаю, что это сделано в их реализации шаблона проектирования Unit Of Work.
Любомир Вельчев
3

Что мне нравится в этом, так это то, что он выравнивает единицу работы (в том виде, в каком ее видит пользователь, то есть отправку страницы) с единицей работы в смысле ORM.

Таким образом, вы можете сделать передачу всей страницы транзакционной, чего нельзя было бы сделать, если бы вы представляли методы CRUD при каждом создании нового контекста.

RB.
источник
3

Другая недооцененная причина не использовать одноэлементный DbContext, даже в однопоточном однопользовательском приложении, заключается в используемом им шаблоне карты идентификации. Это означает, что каждый раз, когда вы извлекаете данные, используя запрос или по идентификатору, он будет хранить извлеченные экземпляры сущности в кэше. В следующий раз, когда вы получите тот же объект, он предоставит вам кэшированный экземпляр объекта, если он доступен, с любыми изменениями, которые вы сделали в том же сеансе. Это необходимо, чтобы метод SaveChanges не заканчивался несколькими экземплярами сущностей одной и той же записи базы данных; в противном случае контекст должен был бы каким-то образом объединить данные из всех этих экземпляров сущности.

Причиной проблемы является то, что одноэлементный DbContext может стать бомбой замедленного действия, которая может в конечном итоге кэшировать всю базу данных + накладные расходы объектов .NET в памяти.

Есть способы обойти это поведение, используя только запросы Linq с .NoTracking()методом расширения. Также в наши дни компьютеры имеют много оперативной памяти. Но обычно это не желаемое поведение.

Дмитрий Сергеевич
источник
Это правильно, но вы должны предположить, что сборщик мусора будет работать, делая эту проблему более виртуальной, чем реальной.
Токвиль
3
Сборщик мусора не собирается собирать экземпляры объектов, которые содержатся в активном статическом / одноэлементном объекте. Они окажутся во втором поколении кучи.
Дмитрий Сергеевич
1

Еще одна проблема, на которую следует обратить особое внимание в Entity Framework, - это использование комбинации создания новых объектов, отложенной загрузки и последующего использования этих новых объектов (из того же контекста). Если вы не используете IDbSet.Create (против только что нового), отложенная загрузка этого объекта не будет работать, когда его извлекают из контекста, в котором он был создан. Пример:

 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.
Тед Эллиотт
источник