В данной статье мы рассмотрим вопросы по трассировке, оптимизации и компиляции генерируемого SQL-кода двух ORM-фреймворков: LINQ to SQL и Entity Framework.
Предисловие
Object-Relational Mapping фреймворки на сегодняшний день являются весьма популярными инструментами для быстрой разработки data-layer’а приложения. Но у них есть «подводные камни» – не всегда эффективный SQL-код. Поэтому в этой статье мы попытаемся решить данную проблему.
Будут использоваться вышеперечисленные инструменты, так как:
- оба входят в комплект поставки .NET Framework 4
- EF весьма схож с LINQ to SQL, что делает простым рассмотрение и сравнение обоих фреймворков
Мы рассмотрим следующие проблемы:
- Трассировка SQL-кода
- Эффективное создание SQL-запросов
- Компиляция LINQ выражений
Перед тем как начать, сразу отмечу – для работы с LINQ to SQL потребуется Visual Studio 2008 и выше, а EF – только VS 2010.
Трассировка SQL-кода
Прежде чем приступить к рассмотрению оптимизации запросов, необходимо сначала просмотреть сгенерированный данными инструментами SQL-код. Пусть у нас есть две таблицы Profiles и Users. Создадим две модели на основе LINQ to SQL и EF - DatabaseContext и DatabaseEntities, соответственно.
Модель DatabaseEntities для Entity Framework
Модель DatabaseContext для LINQ to SQL
В EF этот вопрос решается использованием класса ObjectTrace.
using (DatabaseEntities context = new DatabaseEntities())
{
var query = from p in context.Profiles
where p.ProfileID == 100
select p;
Console.WriteLine(((ObjectQuery<Profile>)query).ToTraceString());
}
В LINQ to SQL код запроса мы можем получить через свойство Log.
using (DatabaseContext context = new DatabaseContext())
{
context.Log = Console.Out;
var query = from p in context.Profiles
where p.ProfileID == 100
select p;
}
Эффективное создание SQL-запросов
Порой ORM-решения неэффективно производят запросы – происходит выборка данных всех столбцов соответствующих таблиц, что – не лучший вариант. Однако это можно достаточно быстро исправить. Чтобы этого избежать, можно воспользоваться следующим:
- использовать анонимные классы
- использовать прокси-классы
- в Entity Framework использовать eSQL
Так будет выглядеть запрос к обеим моделям:
//LINQ to SQL
using (DatabaseContext db_context = new DatabaseContext())
{
var query = from profile in db_context.Profiles
where profile.ProfileID == 100
select profile;
}
Сгенерированный SQL-код:
SELECT [t0].[ProfileID], [t0].[Body]
FROM [dbo].[Profiles] AS [t0]
WHERE [t0].[ProfileID] = @p0
-- @p0: Input Int (Size = -1; Prec = 0; Scale = 0) [100]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 4.0.30319.1
//EF
using (DatabaseEntities db_entity = new DatabaseEntities())
{
var query = from profile in db_entity.Profiles
where profile.ProfileID == 100
select profile;
}
Сгенерированный SQL-код:
SELECT
[Extent1].[ProfileID] AS [ProfileID],
[Extent1].[Body] AS [Body]
FROM [dbo].[Profiles] AS [Extent1]
WHERE 100 = [Extent1].[ProfileID]
Как видно, из таблицы Profiles будут выбраны все столбцы, что не очень нам нужно, если учитывать, что свойство Body может иметь большой размер. Для этого попробуем использовать анонимные классы.
//LINQ to SQL
using (DatabaseContext db_entity = new DatabaseContext())
{
var query = from profile in db_entity.Profiles
where profile.ProfileID == 100
select new { ID = profile.ProfileID };
}
//EF
using (DatabaseEntities db_entity = new DatabaseEntities())
{
var query = from profile in db_entity.Profiles
where profile.ProfileID == 100
select new { ID = profile.ProfileID };
}
Можно использовать и прокси-класс:
class ProfileProxy
{
public int ProfileID { get; set; }
}
Замечу, что могут использоваться как свойства, так и обычные поля для хранения данных. Теперь используем его:
//LINQ to SQL
using (DatabaseContext db_context = new DatabaseContext())
{
var query = from profile in db_context.Profiles
where profile.ProfileID == 100
select new ProfileProxy() { ProfileID = profile.ProfileID };
}
Сгенерированный SQL-код:
SELECT [t0].[ProfileID]
FROM [dbo].[Profiles] AS [t0]
WHERE [t0].[ProfileID] = @p0
-- @p0: Input Int (Size = -1; Prec = 0; Scale = 0) [100]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 4.0.30319.1
//EF
using (DatabaseEntities db_entity = new DatabaseEntities())
{
var query = from profile in db_entity.Profiles
where profile.ProfileID == 100
select new ProfileProxy { ProfileID = profile.ProfileID };
}
Сгенерированный SQL-код:
SELECT
[Extent1].[ProfileID] AS [ProfileID]
FROM [dbo].[Profiles] AS [Extent1]
WHERE 100 = [Extent1].[ProfileID]
Теперь перейдем к Entity SQL или просто eSQL. Данный встроенный в EF язык очень схож с обычным SQL с той лишь главной разницей, что вместо таблиц в запросе используются классы. Для более детального изучения посетите MSDN: http://msdn.microsoft.com/en-us/library/bb387118.aspx Использование eSQL
//eSQL query
using (DatabaseEntities context = new DatabaseEntities())
{
var query = @"select r.UserID, r.UserName from DatabaseEntities.Users as r";
var result1 = context.CreateQuery<User>(query);
}
Сгенерированный SQL-код:
SELECT
[Extent1].[UserID] AS [UserID],
[Extent1].[UserName] AS [UserName]
FROM [dbo].[Users] AS [Extent1]
Компиляция LINQ-запросов
Наибольшее время при выполнении запроса данными фреймворками приходится на генерацию SQL-кода. Причем при каждой выборке данное действие происходит повторно. Во избежание этого необходимо использовать одноименные классы CompiledQuery, находящиеся в:
- System.Data.Linq – для LINQ to SQL
- System.Data.Objects – для EF
Прежде чем приступить к их рассмотрению необходимо понимать лямбда-выражения. Чтобы скомпилировать запрос необходимо у обоих классов вызвать функцию Compile(). Разница между ними лишь в типе первого аргумента: для EF – это System.Data.Objects.ObjectContext, а для LINQ to SQL – это System.Data.Linq.DataContext.
//EF CompiledQuery
var q = System.Data.Objects.CompiledQuery.Compile<DatabaseEntities, IQueryable<Profile>>
(ctx =>
from p in ctx.Profiles
where p.ProfileID == 1
select p);
var context = new DatabaseEntities();
var result = q(context).ToList();
//LINQ to SQL CompiledQuery
var q = System.Data.Linq.CompiledQuery.Compile<DatabaseContext, IQueryable<Profile>>
(ctx =>
from p in ctx.Profiles
where p.ProfileID == 1
select p);
var context = new DatabaseContext();
var result = q(context).ToList();
Хотелось бы заметить следующие моменты:
- При компиляции LINQ-запросов Вы не можете использовать анонимные классы
- В EF отсутствует возможность компиляции eSQL. Зато eSQL всегда кешируется, причем это даже можно отключить.
Заключение
Таким образом, используя вышеописанные техники при работе ORM-фреймворками, можно добиться довольно производительных решений. Например, при использовании одной только компиляции скорость возрастает более 5x, а указание только нужных столбцов при выборке позволяет добиться высокой производительности.