ASP.NET MVC — шаблон репозитория с Entity Framework

Когда вы разрабатываете приложение ASP.NET с использованием шаблона репозитория, каждый из ваших методов создает новый экземпляр контейнера сущностей (контекст) с блоком использования для каждого метода, или вы создаете экземпляр контейнера на уровне класса/частный для использовать любой из методов репозитория, пока сам репозиторий не будет удален? Помимо того, что я отмечу ниже, каковы преимущества/недостатки? Есть ли способ объединить преимущества каждого из них, которые я просто не вижу? Реализует ли ваш репозиторий IDisposable, что позволяет вам создавать с помощью блоков экземпляры вашего репо?

Несколько контейнеров (вместо одного)

Преимущества:

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

Недостатки:

  • Доступ к дочерним объектам в контроллере/представлении ограничен тем, что вы вызвали с помощью Include()
  • Для таких страниц, как индекс приборной панели, который показывает информацию, собранную из многих таблиц (множество различных вызовов методов репозитория), мы добавим накладные расходы на создание и удаление множества контейнеров сущностей.

person Keith    schedule 18.04.2012    source источник
comment
Не по теме? Шутки в сторону? Я мог бы посчитать закрытие неконструктивным (хотя я бы тоже возражал против этого, но, безусловно, более точным, чем не по теме)   -  person Erik Funkenbusch    schedule 18.04.2012


Ответы (3)


Если вы создаете экземпляр своего контекста в своем репозитории, вы всегда должны делать это локально и заключать его в оператор using.

Если вы используете Dependency Injection для внедрения контекста, то пусть ваш DI-контейнер обрабатывает вызов dispose в контексте, когда запрос выполнен.

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

person Erik Funkenbusch    schedule 18.04.2012
comment
Вам просто нужно удалить сам класс репозитория (и удалить контекст в методе Dispose репозитория), после чего вы можете безопасно использовать класс с контекстом в качестве члена. Он указал это уже в своем вопросе: Реализует ли ваш репозиторий IDisposable, что позволяет вам создавать блоки с использованием для экземпляров вашего репо? - person Slauma; 18.04.2012
comment
@Mystere Отличный ответ - я удивлен, что никогда не думал об использовании DI для этой цели, хотя он кажется идеально подходящим для этого. Простое решение, спасибо! - person Keith; 18.04.2012

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

На сайте Microsoft MSDN есть полезная информация о шаблоне репозитория. Надеюсь, это поможет прояснить некоторые вещи.

person JasCav    schedule 18.04.2012
comment
К сожалению, это означает, что Dispose не вызывается в вашем контексте, что означает, что вы можете иметь открытые соединения с базой данных (плюс значительный объем используемой памяти), пока сборщик мусора не очистит его. Вот почему вы всегда должны заключать свои контексты в блок using, чтобы они удалялись как можно быстрее. - person Erik Funkenbusch; 18.04.2012
comment
@MystereMan - Хороший вопрос, но как обойти другую проблему, которую я поднял в своем посте? (Теперь вы несете ответственность за его настройку для каждого отдельного метода, что значительно увеличивает накладные расходы на разработку/код.) Лично я никогда не видел, чтобы это делалось каким-либо другим способом. (Это не значит, что я прав, но это делает меня более скептичным.) - person JasCav; 18.04.2012
comment
Я не понимаю вашу вторую проблему. Если вы используете внедрение зависимостей, вы позволяете контейнеру DI обрабатывать вызов dispose. Классы разрабатываются по-разному, если они используют DI, а не создают свои собственные объекты. Переключение может потребовать больше усилий, но именно поэтому вы должны начать с DI. - person Erik Funkenbusch; 18.04.2012
comment
@MystereMan - Хорошо... Думаю, это имеет смысл. (Признаюсь, мне всегда нужно многому научиться в программировании. Спасибо за ваше терпение.) - person JasCav; 18.04.2012

Я не согласен со всеми четырьмя пунктами:

Предотвращение автоматического закрытия/удаления соединений (будет закрыто в конце блока использования).

На мой взгляд, не имеет значения, размещаете ли вы контекст на уровне метода, уровне экземпляра репозитория или уровне запроса. (Конечно, вы должны размещать контекст в конце одного запроса — либо обернув метод репозитория в оператор using, либо реализовав IDisposable в классе репозитория (как вы предложили) и обернув экземпляр репозитория в операторе using в действии контроллера, или путем создания экземпляра репозитория в конструкторе контроллера и удаления его в переопределении Dispose класса контроллера, или путем создания экземпляра контекста, когда начинается запрос, и удаления его, когда запрос заканчивается (некоторые зависимости Контейнеры для инъекций помогут сделать эту работу.) Почему контекст должен "автоудаляться"? В настольном приложении возможно и распространено иметь контекст для каждого окна/представления, который может быть открыт часами.

Помогает заставить вас извлекать в память только то, что вам нужно для конкретного представления/модели представления, и с меньшим количеством циклов (вы получите ошибку соединения для всего, что вы пытаетесь отложить).

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

Доступ к дочерним объектам в контроллере/представлении ограничен тем, что вы вызвали с помощью Include()

Что является скорее преимуществом, чем недостатком, потому что у вас нет нежелательных сюрпризов ленивой загрузки. Если вам нужно позже заполнить дочерние сущности в действиях контроллера, в зависимости от какого-то условия, вы можете загрузить их через дополнительные методы репозитория (LoadNavigationProperty или что-то подобное) с тем же или даже новым контекстом.

Для таких страниц, как индекс приборной панели, который показывает информацию, собранную из многих таблиц (множество различных вызовов методов репозитория), мы добавим накладные расходы на создание и удаление множества контейнеров сущностей.

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

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

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

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

person Slauma    schedule 18.04.2012
comment
Здесь мы говорим о веб-приложении, а не о настольном приложении. Настольное приложение обычно имеет много ресурсов и может оставлять контексты открытыми в течение нескольких часов. Веб-приложение, как правило, нуждается в освобождении ресурсов как можно скорее, потому что вы не знаете, под какой нагрузкой будет находиться сервер. Веб-приложения имеют принципиально другие требования к управлению ресурсами, чем настольные приложения. - person Erik Funkenbusch; 18.04.2012
comment
@MystereMan: Вы говорите, что веб-процесс может высвободить ресурсы, используемые в запросе, еще до того, как запрос будет полностью обработан? Я не говорил оставлять контекст (или несколько контекстов) открытым на время дольше, чем один запрос. Я не понимаю вашего критического замечания. - person Slauma; 18.04.2012
comment
Если вы поместите свой контекст на уровень класса, то контекст не удаляется при выполнении запроса. Даже если запрос больше не использует его, он по-прежнему будет потреблять ресурсы и, возможно, соединения, пока не произойдет сборка мусора. Если вы используете DI для внедрения контекста, тогда ваш DI-контейнер может вызывать dispose в конце запроса, или если вы создаете экземпляры объектов напрямую, вам нужно обернуть его в оператор using, чтобы освободить ресурсы, как только вы закончите с Это. - person Erik Funkenbusch; 18.04.2012
comment
@MystereMan: я добавил длинное предложение в скобках под первым пунктом. Я думал, это понятно, потому что в вопросе даже предлагалось реализовать IDisposable для класса репозитория. Конечно вы должны сделать это, когда создаете экземпляр контекста в качестве члена репозитория. - person Slauma; 18.04.2012
comment
@Slauma Много интересных и обоснованных моментов, хотя я должен признать, что некоторые из ваших аргументов, похоже, склоняются к тому, что вы соглашаетесь с некоторыми из высказанных мной моментов, а не не соглашаетесь на всех уровнях, как вы упоминаете. - person Keith; 18.04.2012
comment
@Keith: Я помню, что хотел перефразировать грубое первое предложение немного более вежливо. Забыл, извини :) - person Slauma; 18.04.2012
comment
Если ваш репозиторий реализует IDisposable, тогда да. используйте DI для контекста, так как это автоматически удалит контекст, когда запрос будет завершен (при условии, что ваш контейнер DI делает это, большинство может). - person Erik Funkenbusch; 18.04.2012
comment
@MystereMan: удаление репозитория поддерживается через Dispose переопределение класса контроллера, это довольно просто, и я до сих пор использую его в старых проектах. Я согласен с простотой DI, я сам использую его во всех новых проектах. Но вопрос был о двух других вариантах, а не о контексте для каждого запроса, поэтому я упомянул его только как примечание и третий вариант в своем ответе. Мне интересно, что оправдывает отрицательный голос, вы говорите почти то же самое в своем собственном ответе, просто с разным акцентом на трех вариантах. - person Slauma; 18.04.2012