Введение в LINQ to Entities
Ранее мы использовали ряд операций для получения данных из БД. В основе подобных операций лежит технология LINQ (Language Integrated Query), или точнее LINQ to Entities. LINQ to Entities предлагает простой и интуитивно понятный подход для получения данных с помощью выражений, которые по форме близки выражениям языка SQL.
Хотя при работе с базой данных мы оперируем запросами LINQ, но база данных понимает только запросы на языке SQL. Поэтому между LINQ to Entities и базой данных есть проводник, который позволяет им взаимодействовать. Этим проводником является провайдер EntityClient . Он создает интерфейс для взаимодействия с провайдером ADO.NET для SQL Serverа.
Для начала взаимодействия с базой данных создается объект EntityConnection . Через объект EntityCommand он отправляет запросы, а с помощью объекта EntityDataReader считывает извлеченные из БД данные. Однако разработчику не надо напрямую взаимодействовать с этими объектами, фреймворк все сделает за него. Задача же разработчика сводится в основном к написанию запросов к базе данных с помощью LINQ.
Прежде чем приступить к обзору основных запросов в LINQ to Entities, для работы с материалом этой главы создадим новые модели по связи один-ко-многим:
У нас здесь модель телефона и модель компании-производителя. Теперь создадим контекст данных и инициализатор базы данных начальными данными:
Чтобы база данных уже содержала некоторые данные, в инициализаторе бд создается несколько объектов. Чтобы задействовать инициализатор, он вызывается в статическом конструкторе контекста данных: Database.SetInitializer(new MyContextInitializer());
Для создания запросов в Linq to Entities, так же, как и в Linq to Objects, мы можем применять операторы LINQ и методы расширения LINQ.
Например, используем некоторые операторы LINQ:
И тот же запрос с помощью методов расширений LINQ:
Оба запроса в итоге транслируются в одной выражение sql:
Важно понимать различие между Linq to Entities и Linq to Objects:
Здесь используются два метода Where, но их реализация будет различной. В первом случае, db.Phones.Where(p=> p.Company > транслируется в выражение SQL, которое было рассмотрено выше. Далее метод ToList() по результатам запроса создает список в памяти компьютера. После этого мы уже имеем дело со списком в памяти, а не с базой данных. И далее вызов Where(p=> p.Id будет обращаться к списку в памяти и будет представлять Linq to Object.
А теперь рассмотрим некоторые приемы применения LINQ к запросам из базы данных.
»» В ДАННОЙ СТАТЬЕ ИСПОЛЬЗУЕТСЯ ИСХОДНЫЙ КОД ДЛЯ ПРИМЕРОВ
Выполнение запросов с использованием LINQ to Entities подобно выполнению запросов с помощью LINQ to SQL. Тем не менее, есть некоторые нюансы и отличия.
Базовые запросы
Подобно LINQ to SQL, запросы LINQ to Entities возвращают IQueryable . Результат запроса LINQ to Entities можно использовать точно так же, как запрос LINQ to SQL. Ниже приведен пример:
Как видите, запрос выполняется с использованием свойства Customers объекта ObjectContext в качестве источника, а результатом является IQueryable . Ниже показан вывод после запуска кода:
Скомпилированные запросы
Для повышения производительности LINQ to Entities поддерживает скомпилированные запросы. Статический метод CompiledQuerу.Compile получает запрос и возвращает Func, принимающий ObjectContext и до 16 параметров запроса. Лучше всего объяснить это на примере. В коде ниже содержатся два запроса LINQ to Entities, которые получают множество заказчиков, находящихся в Лондоне и Париже:
Для каждого города определяется один и тот же запрос, меняется только название. Запуск этого кода дает следующие результаты:

Чтобы создать скомпилированную версию запроса из предыдущего примера, вызывается метод CompiledQuery.Compile, как показано ниже. Первый аргумент — всегда ObjectContext для сущностной модели данных. Последний аргумент — это результат опроса, в данном случае IQueryable . Прочие аргументы — параметры, которые необходимо передать запросу, чтобы сделать его многократно используемым. В конце концов, нет смысла компилировать запрос, если он не может применяться более одного раза. В рассматриваемом примере понадобится возможность указания разных городов, поэтому есть один аргумент string:
Возвращаемым типом метода Compile является Func, строго типизированный в соответствии с типами, указанными для метода Compile. В нашем случае получается Func >. Теперь для повторного использования запроса просто вызывается функция и передаются параметры:
Мы определяем функцию компилированного запроса и затем вызываем ее для каждого интересующего нас города. Запрос компилируется при первом его использовании, что обеспечивает повышение производительности, — особенно для сложных запросов. Вывод этого кода аналогичен предыдущему.
Просмотр оператора SQL
Часто бывает полезно увидеть оператор SQL, в который транслируется запрос LINQ to Entities. К сожалению, не существует удобного пути сделать это для всех операторов SQL, которые создает экземпляр ObjectContext. Однако просмотреть оператор SQL, который генерирует одиночный запрос LINQ to Entities, можно, приведя результат IQueryable от запроса LINQ to Entities к конкретному классу ObjectQuery и вызвав метод ToTraceString. Соответствующий код приведен ниже:
В этом коде определен запрос, который выберет всех заказчиков Northwind, находящихся в Лондоне. Затем осуществляется проверка наличия открытого соединения с базой данных. Используемые здесь члены ObjectContext подробно рассматриваются позже, а пока просто знайте, что, не имея открытого соединения, вы получите исключение, если попытаетесь получить оператор SQL из запроса.
Чтобы получить оператор SQL, результирующее перечисление из LINQ-запроса IQueryable приводится к ObjectQuery и вызывается метод ToTraceString. Он возвращает строку, содержащую оператор SQL, в который транслируется запрос, и строка затем выводится на консоль. Компиляция и запуск кода даст следующий вывод:

Получение этого SQL-оператора еще не означает немедленное выполнение запроса — просто запрос LINQ to Entities транслируется в оператор SQL. Понятно, что это не слишком элегантный прием.
Хотя он применяется в приведенном примере, все же предпочтительнее пользоваться инструментом профилирования SQL Server Profiler. Если этот инструмент отсутствует (например, он не входит в состав версии SQL Server Express Edition, поставляемой с Visual Studio 2010), рекомендуется воспользоваться бесплатным средством профилирования SQL с открытым исходным кодом от Anjlab. Инструмент профилирования позволит увидеть все операторы SQL, посылаемые базе данных, а не только от каждого конкретного запроса.
Загрузка связанных объектов
Сущностные типы ассоциируются, когда между ними устанавливается отношение внешнего ключа. Сущностные объекты (т.е. экземпляры сущностных типов) связаны друг с другом через специфическое значение внешнего ключа. Например, сущностные типы Customer и Order из базы данных Northwind ассоциированы, и также связаны объекты Customer для Round the Horn и Order для Round the Horn. LINQ to Entities облегчает навигацию по данным, автоматически отслеживая ассоциации между ними. Взаимосвязанные объекты загружаются "за кулисами", так что код работает прозрачно. Однако стоит уделить внимание тому, как загружаются связанные объекты.
"Ленивая" загрузка
"Ленивая" загрузка объектов является поведением по умолчанию LINQ to Entities. Связанные объекты загружаются из базы данных, только когда происходит обращение к ассоциированному свойству сущностного типа. То, что не нужно, никогда не загружается — это называется стратегией оперативной (just-in-time) загрузки, но это означает возможность получить неожиданно большое количество запросов SQL, генерируемых кодом. Данная проблема демонстрируется ниже:
В этом коде запрашиваются заказчики из Лондона с упорядочиванием результатов по полю CustomerID. Затем на консоль выводится название и контактное лицо в каждой компании, наряду с OrderID первого заказа, ассоциированного с заказчиком. Компиляция и запуск кода, приведенного выше, приводит к следующим результатам, включающим шесть заказчиков, соответствующих критерию запроса:

Разумеется, объекты Order, связанные с каждым Customer, не загружаются, пока не будет произведено обращение к полю Customer.Orders. Когда это делается, Entity Framework незаметно запрашивает базу данных и загружает необходимые данные. Никакие другие объекты, связанные с типом Customer, не загружаются.
В зависимости от конкретного проекта, такой подход может быть либо гениальным, либо совершенно безумным. Гениальным он может быть потому, что из базы данных получается только то, что необходимо, и только тогда, когда нужно. Безумным же он может быть потому, что даже простой запрос LINQ может превратиться во множество запросов к базе данных.
В случае вышеуказанного примера, получаются семь сгенерированных запросов SQL: один для получения списка заказчиков из Лондона и шесть для получения заказов от каждого из заказчиков. Для некоторых проектов семь запросов для столь простого куска кода будет чересчур, и в последующих разделах будут продемонстрированы альтернативные подходы.
Чтобы отключить ленивую загрузку, необходимо установить опцию в ObjectContext, как показано ниже:
При отключенной ленивой загрузке попытка обратиться к связанному сущностному объекту приводит к генерации исключения, если только не используется один из описанных ниже приемов обеспечения загрузки данных.
Немедленная загрузка
Когда точно известно, какие данные понадобятся в коде, как это было в предыдущем примере, можно в качестве части запроса LINQ to Entities использовать метод Include для загрузки связанных сущностных объектов. Метод Include применяется к запросу указанием имени свойства ассоциации между запрашиваемым типом и типом, который требуется загрузить в виде строки; в рассматриваемом случае свойством, ассоциирующим тип Customer с типом Order, будет Orders, так что необходимо вызвать метод Include со строковым аргументом "Orders".
В следующем демонстрируется немедленная загрузка для запроса, который использовался в предыдущем коде:
Скомпилировав и запустив этот преобразованный пример, получится тот же результат, что и у кода из предыдущего примера, но в базу данных будет отправлен только один запрос!
Немедленно загружать можно любое количество связанных сущностных типов, применяя метод Include к каждому необходимому типу. В следующем примере показан запрос для Orders, который немедленно загружает связанные сущностные типы Shipper и Customer:
В запрос LINQ включены сущностные типы Shipper и Customer, что порождает один запрос к базе данных, который извлекает поля трех разных сущностных типов. Результат выполнения этого запроса выглядит следующим образом:

Явная загрузка
Если необходим полный контроль, то лучше всего подойдет явная загрузка. Загружаемые связанные объекты указываются с использованием метода EntityCollection.Load. Ниже демонстрируется выборочная загрузка связанных объектов:
Для использования явной загрузки ленивая загрузка должна быть отключена. В противном случае Entity Framework все равно загрузит связанные объекты автоматически.
В приведенном выше примере выполняется запрос LINQ для получения всех заказчиков из Лондона, а затем для всех заказчиков, кроме North/South, явно загружаются связанные объекты Order.
После этого результаты снова перечисляются с выводом на консоль запрошенных данных. С помощью метода IsLoaded определяется то, были ли загружены связанные объекты. Использование явной загрузки может быть чревато ошибками, если только тщательно не проверить загруженные объекты перед тем, как к ним обращаться. Но если необходим полный контроль над загружаемыми данными, то это — идеальное решение.
Опрос представлений
При генерации сущностной модели для базы данных можно включить в нее поддержку любых существующих представлений. Если вы следовали инструкциям из статьи по использованию исходного кода, то выбрали все представления базы данных Northwind во время генерации сущностной модели данных для примеров. Запрос к представлению подобен запросу к таблице. Ниже демонстрируется использование представления Customers and Suppliers by City из базы данных Northwind:
Сущностная модель данных определяет сущностный тип по имени Customer_and_Suppliers_by_City, который собирается в свойстве Customer_and_Suppliers_by_City объекта ObjectContext. Имя представления принимает форму множественного числа посредством мастера Entity Data Model Wizard, что можно отключить при генерации модели. Помимо причудливых имен типов опрос представления во всем подобен опросу таблицы; результат компиляции и выполнения кода показан ниже:

Опрос хранимых процедур
Использование хранимых процедур несколько сложнее, чем использование представлений. Хранимая процедура должна быть явно импортирована в сущностную модель данных. Однако волноваться не стоит, т.к. большую часть работы Visual Studio сделает самостоятельно.
Первый шаг для импорта хранимой процедуры — это открытие окна Model Browser (Браузер модели) в Visual Studio 2010. Для этого выберите сначала файл *.edmx на панели SolutionExplorer, а затем выберите пункт меню View —> Other Windows —> Entity Data Model Browser (Вид —> Другие окна —> Обозреватель моделей EDM). На рисунке ниже показан внешний вид браузера для сущностной модели данных Northwind:

Планируется импортировать и использовать хранимую процедуру Customers_By_City, которая выделена на этом рисунке. Чтобы приступить к импорту, дважды щелкните на имени хранимой процедуры для открытия диалогового окна Add Function Import (Добавить импорт функции):

В поле Function Import Name (Имя импорта функции) указывается имя свойства ObjectContext, которое будет использовано для вызова хранимой процедуры. В рассматриваемом примере вполне подойдет имя, предлагаемое по умолчанию.
Наиболее важный момент — установка типа возврата для хранимой процедуры. Если процедура возвращает поля, необходимые для наполнения имеющегося сущностного типа, то можно выбрать этот тип в раскрывающемся списке. Если же типа возврата нет либо процедура возвращает коллекцию скалярных типов, можно указать это поведение в диалоговом окне.
Хранимая процедура, которую необходимо использовать, не отображается удобно на имеющийся сущностный тип, поэтому часть импорта процедуры будет создание нового типа. Для этого щелкните на кнопке Get Column Information (Получить данные о столбце), а затем — на кнопке Create New Complex Type (Создать новый сложный тип). Диалоговое окно должно выглядеть примерно так:

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

Итак, хранимая процедура импортирована, и новый метод Customers_By_City можно вызывать в классе-наследнике ObjectContext. В нашем случае хранимая процедура принимает единственный параметр (название города для запроса) и возвращает последовательность нового сложного типа, который был создан — IEnumerable .
В следующем примере демонстрируется использование хранимой процедуры для получения деталей о заказчиках, находящихся в Лондоне:
»» В ДАННОЙ СТАТЬЕ ИСПОЛЬЗУЕТСЯ ИСХОДНЫЙ КОД ДЛЯ ПРИМЕРОВ
LINQ to SQL — это система объектно-реляционного отображения начального уровня. LINQ to Entities — это часть платформы ADO.NET Entity Framework, предоставляющая более высокую гибкость и больше средств, чем LINQ to SQL, но следующая за LINQ to SQL в отношении адаптации, из-за повышенной сложности и ранних выпусков, которым пока недостает ключевых средств.
API-интерфейс Entity Framework спроектирован для работы с любыми базами данных, поддерживающими ADO (а не только с SQL Server), и даже включает собственный диалект независимого от поставщика языка SQL, который можно применять в качестве альтернативы LINQ. Фактически Entity Framework обладает настолько широким набором средств, что для их описания понадобилась бы отдельная книга. Здесь будет показано, как запустить и использовать только важнейшие части Entity Framework, относящиеся к LINQ to Entities.
Имеется некоторая путаница с терминами. API-интерфейсы LINQ to SQL и Entity Framework делают нечто схожее, а потому не удивительно, что в них используется общая терминология.
Подобно LINQ to SQL, LINQ to Entities позволяет работать с объектами, которые представляют информацию из базы данных, выполнять LINQ-запросы, изменять значения, добавлять и удалять объекты. И так же, как LINQ to SQL, первый шаг в направлении использования этих средств предусматривает генерацию классов, отображающих содержимое базы данных на объекты — то, что делается при создании сущностной модели данных (entity data model — EDM). Модель EDM состоит из набора объектов и свойств, которые используются для взаимодействия с данными.
Например, в большинстве последующих примеров создается экземпляр класса NorthwindEntities. Этот класс унаследован от класса System.Data.Objects.ObjectContext, который подробно рассматривается позже. Это точка входа в EDM, похожая на класс DataContext для LINQ to SQL. Класс NorthwindEntities устанавливает соединение с базой данных при создании его нового экземпляра и берет на себя ответственность за сохранение изменений при вызове метода SaveChanges.
Давайте рассмотрим простейший пример:
Здесь из базы данных извлекается одиночный заказчик, который помещается в объект Customer. Объект Customer — это экземпляр класса Customer, являющегося частью сущностной модели данных. Позже будет показано, как генерировать EDM для базы данных Northwind.
После извлечения Customer обновляется одно из его свойств и вызывается метод SaveChanges для сохранения изменений в базе данных. Вызов метода SaveChanges помещен в блок try/catch, чтобы можно было разрешить любые потенциальные конфликты, связанные с параллелизмом.

Как это делалось с LINQ to SQL, начнем с обзора ключевых частей LINQ to Entities. Кое-что из того, что будет рассказано о LINQ to Entities, излагается в форме сравнения с LINQ to SQL.
В первом примере, использовался класс-наследник ObjectContext по имени NorthwindEntities, сущностный класс Customer, средства обнаружения и разрешения конфликтов, а также обновление базы данных через метод SaveChanges. Для начала рассмотрим некоторые основы этих компонентов, чтобы обеспечить базовое понимание основ LINQ to Entities и ADO.NET Entity Framework в целом.
ObjectContext
Класс ObjectContext — это ключ для доступа к сущностной модели данных и эквивалент класса DataContext из LINQ to SQL. Класс ObjectContext отвечает за создание и управление соединением с базой данных, отслеживает изменения и управляет постоянством. Подробности будут изложены позднее, а пока достаточно знать, что именно класс ObjectContext соединяет с базой данных, когда создается новый экземпляр NorthwindEntities, и этот же класс отслеживает изменения, которые вносятся в объект Customer, а также транслирует их в оператор SQL, сохраняющий изменения по вызову метода SaveChanges.
Обычно используется класс, унаследованный от ObjectContext, который создается при генерации EDM из базы данных. Далее будет показано, как это делается для базы данных Northwind. Имя класса выбирается на основе имени базы данных — в форме [База данных]Entities. В приведенном выше коде для базы данных Northwind предусмотрен класс NorthwindEntities.
Производный класс [База_данных]Entities для каждой таблицы базы, выбранной при создании EDM, будет иметь свойство ObjectSet , представляющее таблицу, причем T здесь — это тип сущностного класса, созданного для представления записи в таблице. Например, класс NorthwindEntities, использованный в предыдущем примере, имеет общедоступное свойство Customers типа ObjectSet . Оно применялось для выполнения запроса LINQ к множеству заказчиков.
Сущностные классы Entity
Сущностные классы в Entity Framework имеют много общего с классами LINQ to SQL. Это типы .NET, которые представляют собой отображение на реляционную структуру базы данных.
Платформа Entity Framework позволяет выполнять очень сложные отображения между сущностными классами и реляционными данными, которые могут охватывать разные базы данных и быть абстрагированы различными интересными способами. Здесь все эти тонкости не рассматриваются, потому что внимание сосредоточено на аспектах LINQ, но если нужны развитые средства ORM, то определенно следует обратиться к Entity Framework.
Сущностные классы в примерах обнаруживаются по наличию классов и объектов, имеющих имена таблиц базы данных в форме существительного единственного числа. Например, в приведенном примере используется класс по имени Customer. Поскольку Customer — форма единственного числа от Customers, и в базе данных Northwind есть таблица Customers, это намек на то, что Customer является сущностным классом, который представляет записи из таблицы Customer базы данных Northwind.
Мастер построения сущностной модели имеет опцию поддержки имен таблиц во множественном числе при создании сущностных классов, поэтому, когда он находит таблицу базы под названием Customers, то создает сущностный класс по имени Customer для представления элемента таблицы. Тот же подход обеспечивается опцией /pluralize утилиты SQLMetal и это обеспечивает значительное повышение читабельности кода.
Ассоциации Entity
Ассоциация — термин, применяемый для обозначения отношения первичного ключа к внешнему ключу между двумя сущностными классами. При отношении "один ко многим" результатом ассоциации является то, что родительский класс, включающий в себя первичный ключ, содержит коллекцию дочерних классов, имеющих внешний ключ.
Коллекция сохраняется в EntityCollection , где T — тип дочернего сущностного класса. В отношении "многие ко многим" каждый сущностный класс поддерживает EntityCollection , где T — тип противоположной сущности в отношении.
Коллекции сущностей доступны через общедоступные свойства по имени внешнего ключа. Поэтому, например, чтобы добраться к заказам, ассоциированным с заказчиком в базе данных Northwind, следует обратиться к свойству Customer.Orders, которое вернет EntityCollection .
Преимущество ассоциаций между сущностными типами заключается в том, что они позволяют прозрачно выполнять навигацию по данным, не принимая во внимание того факта, что они могут быть распределены среди множества таблиц или даже множества баз данных.





