Стандартные запросы

92

»» В ДАННОЙ СТАТЬЕ ИСПОЛЬЗУЕТСЯ ИСХОДНЫЙ КОД ДЛЯ ПРИМЕРОВ

Выполнение запросов с использованием LINQ to Entities подобно выполнению запросов с помощью LINQ to SQL. Тем не менее, есть некоторые нюансы и отличия.

Базовые запросы

Подобно LINQ to SQL, запросы LINQ to Entities возвращают IQueryable<T>. Результат запроса LINQ to Entities можно использовать точно так же, как запрос LINQ to SQL. Ниже приведен пример:

// Создание ObjectContext
            NorthwindEntities context = new NorthwindEntities();

            IQueryable<Customer> custs = context.Customers
                                                .Where(c => c.City == "London")
                                                .Select(c => c);

            foreach (Customer cust in custs)
                Console.WriteLine("Заказчик: {0} ",cust.CompanyName);

Как видите, запрос выполняется с использованием свойства Customers объекта ObjectContext в качестве источника, а результатом является IQueryable<Customer>. Ниже показан вывод после запуска кода:

Получение результата IQueryable<T> от LINQ to Entities

Скомпилированные запросы

Для повышения производительности LINQ to Entities поддерживает скомпилированные запросы. Статический метод CompiledQuerу.Compile получает запрос и возвращает Func, принимающий ObjectContext и до 16 параметров запроса. Лучше всего объяснить это на примере. В коде ниже содержатся два запроса LINQ to Entities, которые получают множество заказчиков, находящихся в Лондоне и Париже:

// Создание ObjectContext
            NorthwindEntities context = new NorthwindEntities();

            // Запросить лондонских заказчиков
            IQueryable<Customer> londonCustomers = from customer in context.Customers
                                                   where customer.City == "LONDON"
                                                   select customer;

            foreach (Customer cust in londonCustomers)
            {
                Console.WriteLine("Заказчик из Лондона: {0}", cust.CompanyName);
            }

            // Запросить парижских заказчиков
            IQueryable<Customer> parisCustomers = from customer in context.Customers
                                                  where customer.City == "PARIS"
                                                  select customer;
            
            foreach (Customer cust in parisCustomers)
            {
                Console.WriteLine("Заказчик из Парижа: {0}", cust.CompanyName);
            }

Для каждого города определяется один и тот же запрос, меняется только название. Запуск этого кода дает следующие результаты:

Похожие запросы LINQ to Entities

Чтобы создать скомпилированную версию запроса из предыдущего примера, вызывается метод CompiledQuery.Compile, как показано ниже. Первый аргумент — всегда ObjectContext для сущностной модели данных. Последний аргумент — это результат опроса, в данном случае IQueryable<Customer>. Прочие аргументы — параметры, которые необходимо передать запросу, чтобы сделать его многократно используемым. В конце концов, нет смысла компилировать запрос, если он не может применяться более одного раза. В рассматриваемом примере понадобится возможность указания разных городов, поэтому есть один аргумент string:

Func<NorthwindEntities, string, IQueryable<Customer>> compiledQuery
                = CompiledQuery.Compile<NorthwindEntities, string, IQueryable<Customer>>(
                    (ctx, city) =>
                        from customer in ctx.Customers
                        where customer.City == city
                        select customer);

Возвращаемым типом метода Compile является Func, строго типизированный в соответствии с типами, указанными для метода Compile. В нашем случае получается Func<NorthwindEntities, String, IQueryable<Customer>>. Теперь для повторного использования запроса просто вызывается функция и передаются параметры:

// Создание ObjectContext
            NorthwindEntities context = new NorthwindEntities();

            // Скомпилированный запрос
            Func<NorthwindEntities, string, IQueryable<Customer>> compiledQuery
                = CompiledQuery.Compile<NorthwindEntities, string, IQueryable<Customer>>(
                    (ctx, city) =>
                        from customer in ctx.Customers
                        where customer.City == city
                        select customer);

            // Интересующие города
            string[] cities = new string[] { "London", "Paris" };

            foreach (string city in cities)
            {
                IQueryable<Customer> custs = compiledQuery(context, city);
                foreach (Customer c in custs)
                    Console.WriteLine("Заказчик из {0} : {1}", c.City, c.CompanyName);
            }

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

Просмотр оператора SQL

Часто бывает полезно увидеть оператор SQL, в который транслируется запрос LINQ to Entities. К сожалению, не существует удобного пути сделать это для всех операторов SQL, которые создает экземпляр ObjectContext. Однако просмотреть оператор SQL, который генерирует одиночный запрос LINQ to Entities, можно, приведя результат IQueryable<T> от запроса LINQ to Entities к конкретному классу ObjectQuery и вызвав метод ToTraceString. Соответствующий код приведен ниже:

// Создание ObjectContext
            NorthwindEntities context = new NorthwindEntities();

            // Запросить лондонских заказчиков
            IQueryable<Customer> londonCustomers = from customer in context.Customers
                                                   where customer.City == "LONDON"
                                                   select customer;

            // Удостовериться, что соединение с базой данных открыто
            if (context.Connection.State != ConnectionState.Open)
                context.Connection.Open();

            // Отобразиь оператор SQL
            string sql = (londonCustomers as ObjectQuery).ToTraceString();
            Console.WriteLine(sql);

В этом коде определен запрос, который выберет всех заказчиков Northwind, находящихся в Лондоне. Затем осуществляется проверка наличия открытого соединения с базой данных. Используемые здесь члены ObjectContext подробно рассматриваются позже, а пока просто знайте, что, не имея открытого соединения, вы получите исключение, если попытаетесь получить оператор SQL из запроса.

Чтобы получить оператор SQL, результирующее перечисление из LINQ-запроса IQueryable<Customer> приводится к ObjectQuery и вызывается метод ToTraceString. Он возвращает строку, содержащую оператор SQL, в который транслируется запрос, и строка затем выводится на консоль. Компиляция и запуск кода даст следующий вывод:

Отображение оператора 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, генерируемых кодом. Данная проблема демонстрируется ниже:

// Создание ObjectContext
            NorthwindEntities context = new NorthwindEntities();

            IQueryable<Customer> custs = from c in context.Customers
                                         where c.Country == "UK" && c.City == "London"
                                         orderby c.CustomerID
                                         select c;

            foreach (Customer cust in custs)
            {
                Console.WriteLine("{0} - {1}", cust.CompanyName, cust.ContactName);
                Order firstOrder = cust.Orders.First();
                Console.WriteLine("    {0}", firstOrder.OrderID);
            }

В этом коде запрашиваются заказчики из Лондона с упорядочиванием результатов по полю CustomerID. Затем на консоль выводится название и контактное лицо в каждой компании, наряду с OrderID первого заказа, ассоциированного с заказчиком. Компиляция и запуск кода, приведенного выше, приводит к следующим результатам, включающим шесть заказчиков, соответствующих критерию запроса:

Эффект от ленивой загрузки объектов

Разумеется, объекты Order, связанные с каждым Customer, не загружаются, пока не будет произведено обращение к полю Customer.Orders. Когда это делается, Entity Framework незаметно запрашивает базу данных и загружает необходимые данные. Никакие другие объекты, связанные с типом Customer, не загружаются.

В зависимости от конкретного проекта, такой подход может быть либо гениальным, либо совершенно безумным. Гениальным он может быть потому, что из базы данных получается только то, что необходимо, и только тогда, когда нужно. Безумным же он может быть потому, что даже простой запрос LINQ может превратиться во множество запросов к базе данных.

В случае вышеуказанного примера, получаются семь сгенерированных запросов SQL: один для получения списка заказчиков из Лондона и шесть для получения заказов от каждого из заказчиков. Для некоторых проектов семь запросов для столь простого куска кода будет чересчур, и в последующих разделах будут продемонстрированы альтернативные подходы.

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

context.ContextOptions.LazyLoadingEnabled = false;

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

Немедленная загрузка

Когда точно известно, какие данные понадобятся в коде, как это было в предыдущем примере, можно в качестве части запроса LINQ to Entities использовать метод Include для загрузки связанных сущностных объектов. Метод Include применяется к запросу указанием имени свойства ассоциации между запрашиваемым типом и типом, который требуется загрузить в виде строки; в рассматриваемом случае свойством, ассоциирующим тип Customer с типом Order, будет Orders, так что необходимо вызвать метод Include со строковым аргументом "Orders".

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

...
                  
IQueryable<Customer> custs = from c in context.Customers
                                         .Include("Orders")
                                         where c.Country == "UK" && c.City == "London"
                                         orderby c.CustomerID
                                         select c;
...

Скомпилировав и запустив этот преобразованный пример, получится тот же результат, что и у кода из предыдущего примера, но в базу данных будет отправлен только один запрос!

Немедленно загружать можно любое количество связанных сущностных типов, применяя метод Include к каждому необходимому типу. В следующем примере показан запрос для Orders, который немедленно загружает связанные сущностные типы Shipper и Customer:

// Создание ObjectContext
            NorthwindEntities context = new NorthwindEntities();

            IQueryable<Order> orders = context.Orders
                .Include("Shipper")
                .Include("Customer")
                .Where(c => c.ShipCountry == "France")
                .Select(c => c);

            foreach (Order ord in orders)
            {
                Console.WriteLine("OrderID: {0}, Shipper: {1}, Contact: {2}",
                    ord.OrderID,
                    ord.Shipper.CompanyName,
                    ord.Customer.ContactName);
            }

В запрос LINQ включены сущностные типы Shipper и Customer, что порождает один запрос к базе данных, который извлекает поля трех разных сущностных типов. Результат выполнения этого запроса выглядит следующим образом:

Немедленная загрузка множества связанных сущностных типов

Явная загрузка

Если необходим полный контроль, то лучше всего подойдет явная загрузка. Загружаемые связанные объекты указываются с использованием метода EntityCollection.Load. Ниже демонстрируется выборочная загрузка связанных объектов:

// Создание ObjectContext
            NorthwindEntities context = new NorthwindEntities();

            // Отключить ленивую загрузку
            context.ContextOptions.LazyLoadingEnabled = false;

            IQueryable<Customer> custs = context.Customers
                .Where(c => c.Country == "UK" && c.City == "London")
                .OrderBy(c => c.CustomerID)
                .Select(c => c);

            // Выполнить явную загрузку заказов для каждого заказчика
            foreach (Customer cust in custs)
            {
                if (cust.CompanyName != "North/South")
                {
                    cust.Orders.Load();
                }
            }

            foreach (Customer cust in custs)
            {
                Console.WriteLine("{0} - {1}", cust.CompanyName, cust.ContactName);
                // проверить загрузку заказов для каждого заказчика
                if (cust.Orders.IsLoaded)
                {
                    Order firstOrder = cust.Orders.First();
                    Console.WriteLine("    {0}", firstOrder.OrderID);
                }
                else
                {
                    Console.WriteLine("   Данные заказа не загружены");
                }
            }

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

В приведенном выше примере выполняется запрос LINQ для получения всех заказчиков из Лондона, а затем для всех заказчиков, кроме North/South, явно загружаются связанные объекты Order.

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

Опрос представлений

При генерации сущностной модели для базы данных можно включить в нее поддержку любых существующих представлений. Если вы следовали инструкциям из статьи по использованию исходного кода, то выбрали все представления базы данных Northwind во время генерации сущностной модели данных для примеров. Запрос к представлению подобен запросу к таблице. Ниже демонстрируется использование представления Customers and Suppliers by City из базы данных Northwind:

// Создание ObjectContext
            NorthwindEntities context = new NorthwindEntities();

            IQueryable<Customer_and_Suppliers_by_City> res
                = context.Customer_and_Suppliers_by_Cities
                .Where(c => c.City == "LONDON")
                .Select(c => c);

            foreach (Customer_and_Suppliers_by_City r in res)
            {
                Console.WriteLine("{0}, {1}", r.CompanyName, r.ContactName);
            }

Сущностная модель данных определяет сущностный тип по имени Customer_and_Suppliers_by_City, который собирается в свойстве Customer_and_Suppliers_by_City объекта ObjectContext. Имя представления принимает форму множественного числа посредством мастера Entity Data Model Wizard, что можно отключить при генерации модели. Помимо причудливых имен типов опрос представления во всем подобен опросу таблицы; результат компиляции и выполнения кода показан ниже:

Использование LINQ для запроса к представлению базы данных

Опрос хранимых процедур

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

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

Обозреватель моделей EDM

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

Диалоговое окно Add Function Import

В поле Function Import Name (Имя импорта функции) указывается имя свойства ObjectContext, которое будет использовано для вызова хранимой процедуры. В рассматриваемом примере вполне подойдет имя, предлагаемое по умолчанию.

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

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

Генерация нового сложного типа для поддержки хранимой процедуры

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

Импортируемая хранимая процедура

Итак, хранимая процедура импортирована, и новый метод Customers_By_City можно вызывать в классе-наследнике ObjectContext. В нашем случае хранимая процедура принимает единственный параметр (название города для запроса) и возвращает последовательность нового сложного типа, который был создан — IEnumerable<Customers_ By_City_Result>.

В следующем примере демонстрируется использование хранимой процедуры для получения деталей о заказчиках, находящихся в Лондоне:

// Создание ObjectContext
            NorthwindEntities context = new NorthwindEntities();

            IEnumerable<Customers_By_City_Result> custs = context.Customers_By_City("London");

            foreach (Customers_By_City_Result cust in custs)
            {
                Console.WriteLine("{0}, {1}", cust.CompanyName, cust.ContactName);
            }
Пройди тесты
Лучший чат для C# программистов