tscДоступ к данным

Раскрытие стратегий в Entity Framework: загрузка релевантных данных

Джули Лерман

Julie Lerman В прошлом выпуске рубрики я изложила высокоуровневый обзор по выбору стратегий рабочего процесса моделирования: Database First, Model First и Code First. На этот раз я расскажу о другом важном выборе, который вам придется делать: как извлекать релевантные данные из базы данных. Вы можете использовать интенсивную (eager loading), явную (explicit loading) и отложенную загрузку (lazy loading) или даже проекции в запросах (query projections).

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

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

В следующем номере я продолжу эту серию статей и расскажу о различных вариантах запросов через Entity Framework с применением LINQ to Entities, Entity SQL и их вариаций. Ну а в этой статье в каждом примере мы будем использовать только LINQ to Entities.

Интенсивная загрузка за один цикл обращения к базе данных

Интенсивная загрузка позволяет получить все данные из базы за один цикл. Для этого в Entity Framework есть метод Include, который принимает строку, представляющую навигационный путь к релевантным данным. Вот пример, где метод Include будет возвращать графы, каждый из которых содержит Family и набор Pets:

from f in context.Families.Include("Pets") select f

Если в вашей модели присутствует другая сущность — VetVisit, и она имеет связь «один ко многим» с Pet, то вы можете сразу получить все семьи, их домашних животных и информацию о визитах ветеринаров:

from f in context.Families.Include("Pets.VetVisits") select f

Результаты интенсивной загрузки возвращаются как графы объектов (табл. 1).

Табл. 1. Графы объектов, возвращаемые запросом с интенсивной загрузкой

Id Name Pets
    Id Name Type VetVisits    
2 LermanJ 2 Sampson Dog 1 2/1/2011 Excellent
5 4/1/2011 Nail Clipping
4 Sissy Cat 1 3/2/2011 Excellent
3 GeigerA 3 Pokey Turtle 3 2/5/2011 Excellent
4 Riki Cat 6 4/8/2011 Excellent

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

Интенсивная загрузка с помощью Include действительно очень удобна, но при чрезмерном использовании (если в один запрос помещается много вызовов Include или в одном вызове Include указывается много навигационных путей) она может довольно быстро привести к падению производительности обработки запросов. «Родной» запрос, формируемый Entity Framework, содержит много операций join, и, чтобы возвращать запрашиваемые графы, форма результатов от базы данных может оказаться намного сложнее, чем нужно, или вы можете получить больше результатов, чем хотели бы. Это не значит, что вы должны избегать применения Include, но вам стоит выполнить профилирование генерируемых запросов Entity Framework, чтобы проверить, не слишком ли мала скорость их выполнения. В тех случаях, когда «родные» запросы работают слишком медленно, вы должны заново продумать свою стратегию запросов в том сценарии использования приложения, при котором генерируются неэффективные запросы.

Отложенная загрузка с дополнительными циклами обращения к базе данных

Зачастую нет нужды в немедленном получении релевантных данных или извлечении релевантных данных для всех результатов. Например, вам может понадобиться извлечь все объекты-семьи в свое приложение, а затем получать информацию о домашних животных только по некоторым из этих семей. Имеет ли смысл в этом случае интенсивно загружать информацию обо всех домашних животных для каждой семьи с помощью метода Include? Скорее всего нет.

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

При отложенной загрузке вам нужно лишь сослаться на релевантные данные, и Entity Framework проверит, загружены ли они в память. Если их нет в памяти, Entity Framework создаст и выполнит запрос для получения этих данных.

Например, если вы выполняете запрос для получения объектов Family, то можете заставить Entity Framework вернуть набор Pets для одного из этих объектов, просто «упомянув» свойство Pets:

var theFamilies = context.Families.ToList();
var petsForOneFamily = theFamilies[0].Pets;

В Entity Framework отложенная загрузка включается/выключается через свойство ObjectContext ContextOptions.LazyLoadingEnabled. По умолчанию Visual Studio будет устанавливать LazyLoadingEnabled в только что созданных моделях в true, т. е. по умолчанию отложенная загрузка в новых моделях включена.

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

Отложенная загрузка управляется классами EntityCollection и EntityReference и поэтому недоступна по умолчанию при использовании POCO-классов (Plain Old CLR Object), даже если LazyLoadingEnabled равно true. Однако поведение Entity Framework как динамического прокси, инициируемое определением навигационных свойств как виртуальных или Overridable, приведет к созданию прокси периода выполнения, который позволит вашим POCO-объектам использовать отложенную загрузку.

Отложенная загрузка — отличная функциональность в Entity Framework, но только в том случае, если вы четко понимаете, когда она активна, и учитываете, в каких случаях она подходит, а в каких — нет. Так, в статье «Using the Entity Framework to Reduce Network Latency to SQL Azure» (msdn.microsoft.com/magazine/gg309181) было показано, какие последствия для производительности влечет использование отложенной загрузки при обращении локального сервера к базе данных в облаке. Профилирование запросов к базе данных, выполняемых Entity Framework, — важная часть вашей стратегии, которая поможет вам выбрать подходящий для вас вариант загрузки.

Также важно понимать, когда отложенная загрузка отключается. Если свойствоLazyLoadingEnabled установлено в false, ссылка на theFamilies[0].Pets в предыдущем примере не инициирует запрос к базе данных; в итоге вы получили бы ложную информацию о том, что в данной семье нет никаких домашних животных. Поэтому, если вы полагаетесь на отложенную загрузку, обязательно проверяйте, включена ли она.

Явная загрузка с дополнительными циклами обращения к базе данных

Возможно, вы захотите оставить отложенную загрузку отключенной и получить более явный контроль над тем, как загружаются релевантные данные. Помимо явной загрузки с применением метода Include, Entity Framework позволяет избирательно и явным образом извлекать релевантные данные с помощью одного из методов Load.

Если вы создаете классы сущностей, используя шаблон генерации кода по умолчанию, эти классы будут наследовать от EntityObject и предоставлять релевантные данные через EntityCollection или EntityReference. У обоих этих типов есть метод Load, который можно вызывать для того, чтобы указывать Entity Framework извлечь релевантные данные. Вот пример загрузки набора EntityCollection, состоящего из объектов Pets. Обратите внимание на то, что Load не имеет возвращаемого значения:

var theFamilies = context.Families.ToList();
theFamilies[0].Pets.Load();
var petsForOneFamily = theFamilies[0].Pets;

Entity Framework создаст и выполнит запрос, который заполняет релевантное свойство (набор Pets для Family), после чего вы сможете работать с Pets.

Второй способ явного использования Load — вызов из ObjectContext, а не из EntityCollection или EntityReference. Если вы полагаетесь на поддержку POCO в Entity Framework, то EntityCollections или EntityReferences не будут вашими навигационными свойствами и поэтому в них не будет метода Load. В таком случае вы можете вызывать метод ObjectContext.LoadProperty. LoadProperty использует обобщения для идентификации типа, из которого вы осуществляете загрузку, а потом лямбда-выражение, через которое указывается, какое навигационное свойство следует загрузить. Вот пример использования LoadProperty для получения Pets применительно к экземпляру конкретной семьи:

context.LoadProperty<Family>(familyInstance, f => f.Pets)

Проекции в запросах как альтернатива загрузке

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

var famsAndPets=from family in context.Families
  select new {family,Pets=family.Pets.Any(
  p=>p.Type=="Reptile")};

Этот запрос вернет все семьи и всех домашних животных для любой из этих семей, где содержат рептилий, — все это произойдет за один цикл обращения к базе данных. Но вместо графа семей и их домашних животных запрос famsAndPets вернет набор анонимных типов с одним свойством для Family и другим для Pets (табл. 2).

Табл. 2. Спроецированные анонимные типы со свойствами Family и Pets

Family Pets
Id Name Id Name Type
2 LermanJ      
3 GeigerA 3 Pokey Turtle
4 Riki Cat

За и против

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

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

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

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

Использование проекций в запросах потенциально может дать лучшее из двух миров, позволяя избирательно извлекать релевантные данные одним запросом. Однако, если вы возвращаете из этих проекций анонимные типы, то можете столкнуться со сложностями работы с ними: так как эти объекты не отслеживаются диспетчером состояний Entity Framework, их нельзя обновлять.

На рис. 1 показана схема принятия решений, которой можно воспользоваться при первом проходе в выборе стратегии загрузки. Но вы также должны принимать во внимание вопросы производительности. Используйте профилирование запросов и средства проверки производительности, чтобы убедиться, правилен ли ваш выбор в загрузке данных.

Рис. 1. Ваш первый проход в выборе стратегии загрузки

Ваш первый проход в выборе стратегии загрузки


Джули Лерман (Julie Lerman) — Microsoft MVP, преподаватель и консультант по .NET, живет в Вермонте. Часто выступает на конференциях по всему миру и в группах пользователей по тематике, связанной с доступом к данным и другими технологиями Microsoft .NET. Ведет блог thedatafarm.com/blog и является автором очень популярной книги «Programming Entity Framework» (O’Reilly Media, 2010). Вы также можете читать ее заметки в twitter.com/julielerman.

Выражаю благодарность за рецензирование статьи эксперту Тиму Лэверти (TimLaverty).