Следует ли отключать ленивую загрузку Entity Framework в веб-приложениях?

Я слышал, что вам следует отключить функцию отложенной загрузки EF в веб-приложениях. (ASP.NET). Здесь и здесь, для начала.

Теперь я действительно запутался, потому что всегда думал, что ленивая загрузка всегда должна быть включена, потому что она предотвращает ненужную выборку данных из базы данных. Итак, теперь мой вопрос: лучше ли в целом отключить ленивую загрузку в веб-приложениях с точки зрения производительности. Если да, не могли бы вы объяснить, почему?

Я считаю, что отложенная загрузка вводит еще один запрос к БД вместо одного. Таким образом, вы можете загрузить все данные одним запросом, используя, например, Include(). Кроме того, если вы используете одни и те же объекты в своих контроллерах API в выходных позициях, сериализатор всегда будет проходить весь объект, чтобы полностью его сериализовать, что, в свою очередь, вызовет ленивую загрузку этого объекта.

Amir Arbabian 27.01.2019 08:47

Примеры совсем не убедительны. Они показывают код, который никогда не должен загружаться лениво, и точка. Будь то веб-приложение или клиент с отслеживанием состояния, всегда следует избегать шаблона запроса n + 1. Возможно, ваш вопрос не основан на мнении. Если вы можете показать мне случай, когда вам абсолютно нужна отложенная загрузка, тогда да, вам нужна отложенная загрузка, без обсуждения. До тех пор единственный ответ: это зависит. т.е. основанный на мнении, или широкий, или неясный, что угодно, но не подходит для переполнения стека.

Gert Arnold 27.01.2019 13:05
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
11
2
3 284
6

Ответы 6

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

У меня был один старый проект. Я был очень удивлен, когда проверял в профиле sql, сколько запросов поступает в базу данных. Это было около 180(!) для домашней страницы! На главной странице всего два списка по 20-30 пунктов в каждом. Итак, вы должны очень хорошо понимать запросы N+1. Вы должны внимательно проверить его при просмотре кода. Для меня ленивая загрузка доставляет массу проблем. Вы никогда не знаете, сколько запросов поступает в базу данных, когда вы используете эту функцию.

Ваш аргумент имеет смысл, но что меня здесь смущает, так это то, что, допустим, у нас есть некоторые категории продуктов в нашей системе, и с каждым HTTP-запросом мы должны получать все эти категории из базы данных, если мы отключили ленивую загрузку, когда мы выбрать категорию, все связанные с ней продукты также будут выбраны, поскольку мы должны получить все категории, мы фактически получаем все продукты в нашей базе данных! И будет хуже, если у нас будут какие-то связанные сущности с товарами, например его «Бренд». Итак, основываясь на этом подходе, мы фактически извлекаем всю базу данных для каждого запроса, это нормально?

Arad 28.01.2019 10:41

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

Amir Arbabian 28.01.2019 10:54

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

Arad 28.01.2019 11:58

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

Amir Arbabian 28.01.2019 16:47

@AmirHosseinAhmadi, я использую T-SQL. Каждый младший разработчик может писать SQL-запросы. Или я могу преподавать это в течение нескольких недель. Это проще. Все понимают, что происходит. У меня есть два подхода. Если запрос SQL без объединений, я использую ORM. Или, если нужно вставить большой сложный объект, я использую ORM. В этом случае проще использовать ORM. Но я предпочитаю SQL, если мне нужен сложный SQL-запрос с одним или несколькими соединениями.

Dmitresky 28.01.2019 19:26

Ленивая загрузка создаст проблему N+1.

Что это означает?

Это означает, что он попадет в БД один раз (как минимум) для каждого загруженного объекта + начальный запрос.

Почему это плохо?

Предположим, у вас есть эти классы Movie и MovieGenre, и у вас есть 100 фильмов с 30 жанрами между ними в БД.

public class Movie
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public virtual MovieGenre MovieGenre { get; set; }

        public byte MovieGenreId { get; set; }
}

public class MovieGenre
    {
        public byte Id { get; set; }

        public string Name { get; set; }
    }

Теперь предположим, что вы переходите на страницу со всеми 100 фильмами (помните 30 жанров кино?), база данных выполнит 31 запрос (30 для каждого жанра фильма + 1 для фильмов), и запросы будут примерно такими:

Исходный запрос (+1 часть):

SELECT Id, Name, MovieGenreId
From Movie

Дополнительные запросы (часть N):

-- 1
SELECT Id, Name
From MovieGenre
Where Id = 1

-- 2
SELECT Id, Name
From MovieGenre
Where Id = 2

-- 3
SELECT Id, Name
From MovieGenre
Where Id = 3

-- 4
SELECT Id, Name
From MovieGenre
Where Id = 4
.
.
.

Стремительная загрузка позволяет избежать всего этого беспорядка и будет использовать один запрос с правильными соединениями.

поскольку вы используете C#, здесь есть инструмент под названием Glipse, который вы можете использовать для дальнейшего понимания проблемы.

Я понимаю, что вы говорите. Но вопрос, который у меня есть, заключается в том, что, например, я хочу получить данные одного жанра, и этот жанр имеет 10 000 связанных фильмов. При нетерпеливой загрузке, если я извлекаю этот единственный жанр, также извлекаются все данные его 10 000 фильмов. Разве это не проблема?

Arad 16.03.2019 11:57

если вы извлекаете все жанры, фильмы не извлекаются, если вы специально не попросите об этом. Зачем? потому что класс Movie ссылается на MovieGenre, а не наоборот

Hoshani 16.03.2019 12:32

В моем классе EF в классе MovieGenre у меня есть public List<Movie> Movies { get; set; }. Разве это не выбирается каждый раз, когда я выбираю жанр?

Arad 17.03.2019 08:27

Отключение ленивой загрузки предотвратит проблемы с производительностью Select N+1, а также спасение рекурсивной сериализации, однако заменяет их другим артефактом, нулевыми ссылками.

При работе с веб-приложениями я не отключаю ленивую загрузку, а скорее гарантирую, что мои контроллеры/API возвращают сущности нет, а скорее возвращают ViewModels или DTO. Когда вы применяете классы POCO для передачи своим представлениям только того объема данных и структуры, в которых они нуждаются, и используете .Select() или ProjectTo<TViewModel>() Automapper для их заполнения посредством отложенного выполнения, вы избегаете необходимости беспокоиться о ленивой загрузке и повышаете общую производительность. и использование ресурсов для вашего приложения. Технически, используя этот подход, ленивую загрузку можно отключить, поэтому на самом деле это не аргумент за или против ее отключения, а простое действие по отключению ленивой загрузки не сделает ваше веб-приложение «лучше».

Использование ViewModels дает ряд преимуществ:

  • Избегает отложенных вызовов загрузки или неожиданных ссылок #null.
  • Отправляет только данные, необходимые представлению/потребителю, и ничего больше. (Меньше данных по сети и меньше информации для хакеров с представлениями отладки.)
  • Создает эффективные индексируемые запросы к серверу базы данных.
  • Предоставляет место для вычисляемых столбцов, которые не сбивают с толку EF.
  • Помогает уменьшить проблемы с безопасностью (неожиданные модификации объектов) на обратном пути. (Ленивый разработчик не может просто повторно прикрепить модель представления и зафиксировать ее в БД.)

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

Например, при использовании модели представления первый вопрос звучит так: «Какие данные действительно нужны моему представлению?» Итак, учитывая продукт и категорию продукта, если я хочу отправить объект продукта, но мне также нужно имя категории продукта, например, мы столкнемся с неприятной проблемой, если наша категория продукта содержит набор продуктов, и каждый из этих продуктов имеет категория. Когда мы передаем наш продукт сериализатору, он попадет в эту циклическую ссылку и либо выйдет из строя (оставив ссылку или коллекцию #null), либо выдаст исключение. Но, работая с Select N+1, мы перебирали свойства продукта, нажимали ссылку ProductCategory, а затем «SELECT FROM ProductCategory WHERE ProductCategoryID = 3». Затем, когда мы перебираем эту категорию продуктов, мы попадаем в другую ссылку, и это еще один SELECT .... и так далее по цепочке.

Используя модель представления, вы ограничиваете данные, которые хотите получить, только тем, что нужно представлению. Я создаю модель представления продукта, в которой очерчены поля, которые меня интересуют, независимо от того, откуда берутся данные. Если я хочу что-то вроде продукта, его имя и название категории:

public class ProductViewModel
{
    public int ProductId { get; set; }
    public string ProductName { get; set; }
    public string CategoryName { get; set; }
}

затем загрузить его:

var viewModel = context.Products
    .Where(x => x.ProductId == productId) 
    .Select(x => new ProductViewModel
    {
        ProductId = x.ProductId,
        ProductName = x.Name,
        CategoryName = x.Category.Name
    }).Single();

Сделанный. Никаких ленивых загрузок или жадных загрузок не требуется. Это создает один запрос, который возвращает одну запись только с 3 необходимыми столбцами. (Вместо всей записи продукта и его категории продукта)

По мере усложнения требований мы можем ввести иерархию модели представления, но мы можем продолжать сглаживать связанные данные в зависимости от того, что действительно нужно представлению. В некоторых случаях это может означать выбор анонимного типа, а затем преобразование этих результатов в модели представления, где нам нужно использовать функции и т. д., которые EF не может преобразовать в SQL. Этот подход является мощной и быстрой альтернативой загрузке сущностей, но требует внимания с самого начала, чтобы понять, что конечному потребителю (представлению/API) потребуются данные.

Я думаю, вам нужно начать с нескольких вопросов, прежде чем выбрать один и отказаться от другого:

  1. Насколько велик мой набор данных и нужны ли мне связанные данные немедленно? (Это опять же зависит от ваших потребностей)
  2. Как уже говорили другие люди о выпуске N+1; однако это становится важным в зависимости от размера вашего набора данных.
  3. Будет ли прагматичным иметь обратный путь к серверу для получения связанных данных?
  4. Тогда нужны данные. Хотите ли вы, чтобы это было в реальном времени или в кэшированной версии?

Мой вклад во все важные вклады других.

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

Представьте, что у вас есть сценарий Master => Details в веб-приложении, и к тому времени вы поняли, что пользователи не так сильно интересуются деталями. (Вы можете анализировать и проверять запросы, инициированные пользователями). Не было бы необходимости заранее загружать детали для каждой основной записи.

С другой стороны, если детали являются основной частью взаимодействия, просто выполните активную загрузку и извлеките весь Master => Detail для каждого.

Помимо ленивой/нетерпеливой загрузки, убедитесь в следующем:

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

Если в основном запросы на чтение, отключите AutoTracking.

Другие вопросы по теме