В данной статье мы рассмотрим вопросы по трассировке, оптимизации и компиляции генерируемого 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, а указание только нужных столбцов при выборке позволяет добиться высокой производительности.