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

334

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

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

  • 8
    Gueddari в mehdi.me/ambient-dbcontext-in-ef6 вызывает экземпляр DbContext для каждого метода репозитория, вызывает антипаттерн. Цитата: «Делая это, вы теряете практически все функции, которые Entity Framework предоставляет через DbContext, включая его кэш 1-го уровня, его карту идентификации, его единицу работы, а также возможности отслеживания изменений и отложенной загрузки». «. Отличная статья с отличными предложениями для обработки жизненного цикла DBContexts. Определенно стоит прочитать.
Теги:
entity-framework
dependency-injection
inversion-of-control
dbcontext

9 ответов

468
Лучший ответ

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

Пусть начнется эхо Ian: наличие единственного 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 от метода к методу (который называется "Инъекция метода" ). Обратите внимание, что в некотором смысле это решение такое же, как подход с "областью", но теперь область управления контролируется в самом коде приложения (и, возможно, повторяется много раз). Это приложение отвечает за создание и удаление единицы работы. Так как DbContext создается после построения графика зависимостей, инсталляция конструктора выходит за рамки изображения, и вам нужно отложить до метода Injection, когда вам нужно передать контекст из одного класса в другой.

Метод Injection не так уж плох, но когда бизнес-логика становится более сложной, и задействованы все больше классов, вам придется передать ее из метода в метод и класс в класс, что может сильно усложнить код (I Я видел это в прошлом). Для простого приложения этот подход будет очень хорош, хотя.

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

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

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

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

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
    Вау - спасибо за подробный ответ. Если бы я мог дважды проголосовать, я бы сделал это. Выше вы говорите: «... нет намерения позволять целому набору операций работать в одном и том же контексте, в этом случае переходный образ жизни в порядке ...». Что вы подразумеваете под «переходным», в частности?
  • 0
    Для подхода CommandHandler (который я на самом деле использую) означает ли это, что DbContext будет совместно использоваться обработчиками? Это также означает, что существует только один DbContext для одного HTTP-запроса?
Показать ещё 15 комментариев
25

Ни один ответ здесь не отвечает на вопрос. OP не спрашивал об одном дизайне DbContext для одного приложения и для каждого приложения, он задал вопрос о дизайне запроса (web) и о возможных потенциальных преимуществах.

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

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

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

Он обеспечивает ленивую загрузку.

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

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

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

20

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

  • 0
    Вы имеете в виду, что разделение его между HTTP-запросами никогда не является хорошей идеей?
  • 2
    Да, Эндрю, вот что он имел в виду. Совместное использование контекста только для однопоточных настольных приложений.
Показать ещё 1 комментарий
13

Существуют две противоречивые рекомендации от Microsoft, и многие люди используют DbContexts полностью расходящимся образом.

  • Одна рекомендация - "Утилизировать DbContexts как можно скорее" потому что наличие DbContext Alive занимает ценные ресурсы, такие как db соединения и т.д.
  • Другое указывает, что Один DbContext для запроса Рекомендуемые

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

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

Они просто получают свои данные и располагают контекстом как можно скорее. Это считается MANY людьми приемлемой практикой. Хотя это имеет преимущество занять ваши ресурсы в течение минимального времени, он явно жертвует всеми UnitOfWork и кешированием, которые EF может предложить.

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

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

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

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

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

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

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

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

  • 1
    Это интересный ответ, и я частично согласен с вами. На мой взгляд, DbContext не обязательно должен быть привязан к веб-запросу, но он всегда набирается в одном «запросе», как в «бизнес-транзакции». И когда вы связываете контекст с бизнес-транзакцией, отмена изменений становится действительно странной. Но отсутствие его на границе веб-запроса не означает, что бизнес-компоненты (BC) должны создавать контекст; Я думаю, что это не их ответственность. Вместо этого вы можете применить определение области видимости с помощью декораторов вокруг ваших БЦ. Таким образом, вы даже можете изменить область видимости без изменения кода.
  • 0
    В этом случае внедрение в бизнес-объект должно быть связано с управлением жизненным циклом. На мой взгляд, бизнес-объект владеет контекстом и должен контролировать время жизни.
Показать ещё 2 комментария
8

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

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

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

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

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

2

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

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

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

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

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

 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.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню