Pull to refresh

Lazy Loading в Entity Framework

Reading time3 min
Views25K
Хочу рассказать о Lazy Loading в Entity Framework и почему использовать его надо с осторожностью. Сразу скажу, что я предполагаю что читатель использует Entity Framework или хотя бы читал про него.

Что такое Lazy Loading?


Lazy loading это способность EF автоматически подгружать из базы связанные сущности при первом обращении к ним. Например, рассмотрим класс Trade:

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

        public virtual Counterparty Buyer { get; set; }
        public virtual Counterparty Seller { get; set; }
        public decimal Qty { get; set; }
    }

При первом обращении к свойствам Buyer или Seller они будут автоматически загружены из базы. Технически это реализовано через создание экземпляров прокси классов-наследников класса Trade. У прокси класса обращение к свойствам переопределено и содержит логику по загрузке данных из базы. Соответственно что бы механизм работал свойства должны быть виртуальными.

Что это дает?


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

А что же в этом плохого?


Как обычно за все надо платить, вот некоторые минусы которые обязательно следует учесть при использование lazy loading.

Нарушает согласованность данных.


Рассмотрим следующий пример:

public class Order
{
	public int Id {get;set;}	
	public virtual IList<OrderLine> OrderLines {get;set;}
}

public string GetOrderInfo(int orderId)
{
	using(var context = new Context())
	{
		var order = context.Orders.First(x=>x.orderId == 1);
		
		// some logic
		
		var info = string.Join(",", order.OrderLines.Select(x=>x.Name));
				
		return $"{order.Id} {info}";
	}
}

Представим что в промежутке между загрузкой заказа из базы и обращением к свойству OrderLines содержимое заказа было изменено. В результате мы получим содержимое заказа на текущий момент, а не на момент загрузки самого заказа из базы.

Может ухудшать производительность.


Как же так, спросите вы, он же предназначен для ускорения?? В теории да, а вот на практике мне не раз приходилось решать проблемы, появившиеся в результате неграмотного использования данной фичи. Простой пример:

//Рассмотрим метод загружающий информацию о сделке
public string GetTradeDescription(int tradeId)
{
	using (var context = new Context())
	{
		var trade = context.Trades.First(x => x.Id == tradeId);

		var result =  $"{trade.Id}:{trade.Buyer.Name}-{trade.Seller.Name}:{trade.Qty}";
		 
		return result;
	}
}

Вопрос, сколько запросов будет отправлено в базу? Правильно, 3. В данном случае, возможно, это и не очень то страшно.

Но давайте представим что теперь мы хотим получать информацию сразу по произвольному количеству сделок. Модифицируем наш метод:

public List<string> GetTradeDescription(params int[] tradeIds)
{
	using (var context = new Context())
	{ 
		var trades = context.Trades.Where(x => tradeIds.Contains(x.Id));

		var result = trades 
			.AsEnumerable()
			.Select(trade => $"{trade.Id}:{trade.Buyer.Name}-{trade.Seller.Name}:{trade.Qty}")
			.ToList(); 

		return result;
	}
}

В результате количество запросов возрастет пропорционально количеству запрашиваемых сделок. Представьте что при запросе информации по сотне сделок в базу полетит >100 запросов, хотя хватило бы и одного. При этом подобные ошибки не так то просто отловить на этапе разработки/ревью и подобный код может выстрелить уже в боевой среде.

Аналогичная проблема возникает при попытке сериализовать объекты с поддержкой lazy loading, сериализатор будет дергать объект за каждое свойство генерируя таким образом дополнительные обращения к базе данных.

Текучая абстракция


При использовании lazy loading наши POCO объекты перестают быть POCO, ведь теперь мы работаем с прокси которые хранят ссылку на контекст и сами ходят в базу. Что будет если пользоваться этими объектами вне контекста который их загрузил?

public void DoSomeLogic(int tradeId)
{
	var trade = LoadTrade(tradeId);
	
	Console.WriteLine(trade.Buyer.Name);// Oops, здесь мы получим ошибку The ObjectContext instance has been disposed and can no longer be used for operations that require a connection
}

private Trade LoadTrade(int tradeId)
{
	using (var context = new Context())
	{ 
		var trade = context.Trades.First(x => tradeId == x.Id);
		return trade;
	}
} 

Заключение


Лично я в своих проектах принял решение не использовать lazy loading совсем, возможно это слишком категорично, но основываясь на своем опыте скажу, что проблем от него я получил куда больше чем пользы.

Есть альтернативный вариант, оставить lazy loading включенным но строго следить за тем что бы все навигационные свойства не были виртуальными (за тем редким исключением где руки чешутся очень необходим lazy loading).
Only registered users can participate in poll. Log in, please.
А вы используете lazy loading?
37.39% Использую83
40.54% Не использую90
22.07% Использую только редких случаях49
222 users voted. 47 users abstained.
Tags:
Hubs:
+3
Comments33

Articles