четверг, 25 февраля 2010 г.

Улучшение производительности LINQ запросов. Precompiled LINQ Queries.

Одна из возможностей LINQ to SQL и LINQ to Entities это транслятор запросов, которой из программного кода переводит запрос на язык, понимаемый источником, к которому вы посылаете запрос (если речь идет о БД). Процесс преобразования вашей цепочки вызовов, например, в T-SQL не дешевая операция. Каждый раз когда вы выполняете запрос, строится дерево выражений, операций, по которым ваш провайдер, в данном случае SQLProvider, будет транслировать запрос в T-SQL.

Если у вас есть веб сервисы, который дергаются на клиенте, то хочется чтоб они выполнялись быстро, чтоб продолжить выполнение каких-то операций на клиенте. А если у вас AJAX и Rich UI, то тем более хочется чтоб отклик был как можно быстрей. Конечно, это относится не только к сервисам, а и к приложению в целом.

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

Рассмотри пример. У вас есть форма, в который вы ищете страну, чтоб показать её на карте или тп., запрос будет выглядеть примерно так:

using (DBContext db = new DBContext())
{
    var countries = (from country in db.Countries
                     where country.Name.ToLower() == key.ToLower()
                     select country).ToList()

    // do actions with result.
}

Компилируется запрос так:
CompiledQuery.Compile<DBContext, string, IQueryable<Country>>(
    (db, key) => (from country in db.Countries
                  where country.Name.ToLower() == key.ToLower()
                  select country));

Compile принмиает Func<>, можете передать контекст, дополнительные параметры и возвращаемый результат. Потом по коду можно использовать его так:

IQueryable<Country> countries = GetCountryByName.Invoke(db, txtCountry.Text);

Если у вас web приложение можно сохранить запрос в статическую переменную. В другом случае - "шило на мыло".

public Func<DBContext, string, IQueryable<Country>> GetCountryByName =
            CompiledQuery.Compile<DBContext, string, IQueryable<Country>>(
    (db, key) => (from country in db.Countries
                  where country.Name.ToLower() == key.ToLower()
                  select country));
Если у вас есть Repository классы, можно компилировать запрос при первом обращении. Для того чтоб код не "вонял", можно вынести запрос в отдельный класс Queries. При желании можно создать и подклассы, например Queries.Location.GetCountryByName:

public static IQueryable<Country> GetCountryByName(int name)
{
    if (Queries.Location.GetCountryByName == null)
    {
        Queries.Location.GetCountryByName = 
            CompiledQuery.Compile<DBContext, string, IQueryable<Country>>(
    (db, key) => (from country in db.Countries
                  where country.Name.ToLower() == key.ToLower()
                  select country));
    }

    return Queries.Location.GetCountryByName.Invoke(_context, name);
}


Если у вас в результате запроса возвращается сложный тип (анонимный), нужно создать его проекцию, например CountryDTO.

С LINQ to SQL есть проблемы: вы не можете догрузить смежные таблицы используя один DataContext. Например у вас есть выборка по постам блога и вы хотите подтянуть авторов и коментарии, это можно сделать через DataLoadOptions. Но LoadOptions можно задать только один раз для одного экземпляра DataContext, в другом случае получите ошибку. Если вы используете DataContext атомарно для запроса, то проблем нет. Но если у вас DataContext создаете на один HttpContext - то провал. Как вариант, конкретно для методов где используется компилированый запрос, вы можете использовать DataContext атомарно.

В LINQ to Entities эта проблема решена с помощью Include, где вы можете прям в запросе указать, какие таблицы вы хотите загрузить сразу, а не по требованию.

Полезные ссылки:

среда, 24 февраля 2010 г.

Как получить параметры строки запроса в веб сервисе

Бывают ситуации, когда вы дергаете сервис с клиентской части и хотите сделать какие то вычисления учитывая параметры строки запроса, но в Request.QueryString всегда пусто.

Допустим у вас есть следующий функционал: вы можете выбрать страницу и добавить на неё какие-то виджеты. Страница будет выглядеть так - Navigation.aspx?PageId=123. На странице будет js функция, которая дергает сервис для добавления выбранного виджета.

[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ScriptService]
public class WidgetService : WebService
{
    [WebMethod]
    public int AddWidget(int widgetId)
    {
        NameValueCollection request = HttpUtility.ParseQueryString(this.Context.Request.UrlReferrer.Query);
        int pageId = int.Parse(request["PageId"]);

        int widgetInstanceId = AddWidgetInternal(widgetId, pageId);

        return widgetInstanceId;
    }
}

Решение находится в строке #8. Чтоб получить строку запроса, её можно получить у страницы, с которой был вызван сервис.