Выполнение запроса

После создания пользователем запрос LINQ преобразуется в дерево команд. Дерево команд является представлением запроса, совместимым с платформой Entity Framework. Затем дерево команд выполняется в источнике данных. Во время выполнения запроса вычисляются все включенные в него выражения (компоненты запроса), в том числе выражения для материализации запроса.

Выражения выполняются в различные моменты времени. Запросы LINQ всегда выполняются во время прохода по переменной запроса, а не в момент ее создания. Это называется отложенным выполнением. Существует возможность принудительно выполнить запрос немедленно - это может оказаться полезным для кэширования результатов запроса. Этот вопрос обсуждается ниже в данном разделе.

При выполнении запроса в технологии LINQ to Entities некоторые выражения запроса могут выполняться на сервере, а некоторые части — локально, на клиенте. Вычисление выражения на клиенте происходит до выполнения запроса на сервере. Если выражение вычисляется на клиенте, результат этого вычисления подставляется в запрос вместо выражения, после чего запрос выполняется на сервере. Запросы выполняются применительно к источнику данных, поэтому конфигурация источника данных переопределяет правила работы, заданные на клиенте. Например, порядок обработки значений NULL и точность числа зависят от параметров настройки сервера. Все исключения, возникшие на сервере во время выполнения запроса, передаются непосредственно клиенту.

Совет

Удобная сводка операторов запросов в формате таблицы, которая позволяет быстро определить поведение выполнения оператора, см. в разделе Классификация стандартных операторов запросов по способу выполнения (C#).

Отложенное выполнение запроса

В запросе, возвращающем последовательность значений, переменная запроса никогда не содержит результаты запроса, а только хранит его команды. Выполнение запроса откладывается, пока переменная запроса используется в циклах foreach или For Each. Это называется отложенным выполнением. То есть выполнение запроса происходит некоторое время после создания запроса. Это означает, что запрос можно выполнять настолько часто, насколько это необходимо. Такое свойство полезно, например, если имеется база данных, которая обновляется другими приложениями. В собственном приложении можно создать запрос, который регулярно выполняется, каждый раз получая последние обновленные данные.

Отложенное выполнение позволяет объединять несколько запросов или расширять один запрос. При расширении запроса он изменяется, включая в себя новые операции, а последующее выполнение отразит эти изменения. В следующем примере первый запрос возвращает все продукты. Второй запрос расширяет первый, используя предложение Where, чтобы возвратить все продукты с размером «L»:

using (AdventureWorksEntities context = new AdventureWorksEntities())
{
    IQueryable<Product> productsQuery =
        from p in context.Products
        select p;

    IQueryable<Product> largeProducts = productsQuery.Where(p => p.Size == "L");

    Console.WriteLine("Products of size 'L':");
    foreach (var product in largeProducts)
    {
        Console.WriteLine(product.Name);
    }
}
Using context As New AdventureWorksEntities()
    Dim productsQuery = _
        From p In context.Products _
        Select p

    Dim largeProducts = _
        productsQuery.Where(Function(p) p.Size = "L")

    Console.WriteLine("Products of size 'L':")
    For Each product In largeProducts
        Console.WriteLine(product.Name)
    Next
End Using

После выполнения запроса во всех последующих запросах будут использоваться операторы LINQ из памяти. Изменение переменной запроса в цикле с помощью инструкции foreach или For Each либо путем вызова одного из операторов преобразования LINQ приводит к немедленному выполнению запроса. Эти операторы преобразования включают: ToList, ToArray, ToLookup и ToDictionary.

Немедленное выполнение запроса

В отличие от отложенного выполнения запросов, возвращающих последовательность значений, запросы, возвращающие одноэлементное значение, выполняются немедленно. Примерами одноэлементных запросов являются Average, Count, First и Max. Они выполняются немедленно, потому что запрос должен создать последовательность, чтобы вычислить одноэлементный результат. Можно также принудительно вызвать немедленное выполнение. Это может оказаться полезным в тех случаях, когда результаты запроса необходимо поместить в кэш. Чтобы немедленно выполнить запрос, который не возвращает одноэлементное значение, можно вызвать метод ToList, ToDictionary или ToArray запроса или переменной запроса. В следующем примере метод ToArray вызывается для немедленного вычисления последовательности с получением массива.

using (AdventureWorksEntities context = new AdventureWorksEntities())
{
    ObjectSet<Product> products = context.Products;

    Product[] prodArray = (
        from product in products
        orderby product.ListPrice descending
        select product).ToArray();

    Console.WriteLine("Every price from highest to lowest:");
    foreach (Product product in prodArray)
    {
        Console.WriteLine(product.ListPrice);
    }
}
Using context As New AdventureWorksEntities
    Dim products As ObjectSet(Of Product) = context.Products

    Dim prodArray As Product() = ( _
        From product In products _
        Order By product.ListPrice Descending _
        Select product).ToArray()

    Console.WriteLine("The list price from highest to lowest:")
    For Each prod As Product In prodArray
        Console.WriteLine(prod.ListPrice)
    Next
End Using

Можно также принудительно вызвать выполнение, поместив цикл foreach или For Each сразу после выражения запроса, однако при вызове метода ToList или ToArray все данные будут помещены в кэш в одном объекте коллекции.

Выполнение в хранилище

Как правило, выражения LINQ to Entities вычисляются на сервере, и не следует ожидать, что поведение выражений будет соответствовать семантике среды CLR; вместо этого применяется семантика источника данных. Однако из этого правила существуют исключения, например, когда выражение выполняется на клиенте. Это может привести к непредвиденным результатам, например, когда сервер и клиент находятся в разных часовых поясах.

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

Некоторые операции всегда выполняются на клиенте, например, привязка значений, вложенные выражения, вложенные запросы из замыканий и материализация объектов в результаты запроса. В итоге получается, что эти элементы (например, значения параметров) не могут обновляться во время выполнения. Анонимные типы могут быть созданы прямо внутри источника данных, однако лучше этого не делать. В источнике данных можно также создавать встроенные группирования, однако не следует это делать в каждом экземпляре. Как правило, лучше не делать никаких предположений относительно объектов, создаваемых на сервере.

В этом разделе описаны сценарии, в которых код выполняется локально на клиенте. Дополнительные сведения о том, какие типы выражений выполняются локально, см. в разделе "Выражения" в запросах LINQ to Entity.

Литералы и параметры

Локальные переменные (например, переменная orderID в приведенном ниже примере) вычисляются на клиенте.

int orderID = 51987;

IQueryable<SalesOrderHeader> salesInfo =
    from s in context.SalesOrderHeaders
    where s.SalesOrderID == orderID
    select s;
Dim orderID As Integer = 51987

Dim salesInfo = _
    From s In context.SalesOrderHeaders _
    Where s.SalesOrderID = orderID _
    Select s

Параметры методов также вычисляются на клиенте. Например, ниже показан параметр orderID, передаваемый методу MethodParameterExample.

public static void MethodParameterExample(int orderID)
{
    using (AdventureWorksEntities context = new AdventureWorksEntities())
    {

        IQueryable<SalesOrderHeader> salesInfo =
            from s in context.SalesOrderHeaders
            where s.SalesOrderID == orderID
            select s;

        foreach (SalesOrderHeader sale in salesInfo)
        {
            Console.WriteLine("OrderID: {0}, Total due: {1}", sale.SalesOrderID, sale.TotalDue);
        }
    }
}
Function MethodParameterExample(ByVal orderID As Integer)
    Using context As New AdventureWorksEntities()

        Dim salesInfo = _
            From s In context.SalesOrderHeaders _
            Where s.SalesOrderID = orderID _
            Select s

        Console.WriteLine("Sales order info:")
        For Each sale As SalesOrderHeader In salesInfo
            Console.WriteLine("OrderID: {0}, Total due: {1}", sale.SalesOrderID, sale.TotalDue)
        Next
    End Using

End Function

Приведение литералов на клиенте

Приведение значения null к типу CLR выполняется на клиенте.

IQueryable<Contact> query =
    from c in context.Contacts
    where c.EmailAddress == (string)null
    select c;
Dim query = _
    From c In context.Contacts _
    Where c.EmailAddress = CType(Nothing, String) _
    Select c

Приведение к типу, допускающему значение NULL (например, Decimal), выполняется на клиенте.

var weight = (decimal?)23.77;
IQueryable<Product> query =
    from product in context.Products
    where product.Weight == weight
    select product;
Dim weight = CType(23.77, Decimal?)
Dim query = _
    From product In context.Products _
    Where product.Weight = weight _
    Select product

Конструкторы для литералов

Новые типы CLR, которые могут быть сопоставлены с типами концептуальной модели, выполняются на клиенте.

var weight = new decimal(23.77);
IQueryable<Product> query =
    from product in context.Products
    where product.Weight == weight
    select product;
Dim weight = New Decimal(23.77)
Dim query = _
    From product In context.Products _
    Where product.Weight = weight _
    Select product

Новые массивы также выполняются на клиенте.

Исключения в хранилище

Все ошибки хранилища, обнаруженные во время выполнения запроса, передаются клиенту без сопоставления и обработки.

Конфигурация хранилища

Когда запрос выполняется в хранилище, конфигурация хранилища переопределяет все правила, заданные на клиенте и для всех операций и выражений применяется семантика хранилища. Это может привести к тому, что выполнение будет производиться по-разному в среде CLR и в хранилище для таких областей, как сравнение со значением NULL, упорядочение идентификаторов GUID, точность операций, в которых участвуют неточные типы данных (например, типы с плавающей запятой или DateTime), а также строковые операции. Важно помнить об этом при изучении результатов запроса.

Например, ниже представлены некоторые отличия в работе среды CLR и SQL Server.

  • SQL Server упорядочивает идентификаторы GUID иначе, чем среда CLR.

  • Кроме того, возможны различия в точности результатов при работе с типом Decimal в SQL Server. Это происходит из-за требований к фиксированной к точности для десятичного типа данных в SQL Server. Например, арифметическое среднее трех значений типа Decimal - 0,0, 0,0 и 1,0 в памяти на клиенте будет равно 0,3333333333333333333333333333, а в хранилище - 0,333333 (согласно точности по умолчанию для типа Decimal в SQL Server).

  • Некоторые операции сравнения строк также обрабатываются в SQL Server иначе, чем в среде CLR. Это связано с тем, что порядок сравнения строк зависит от параметров сортировки на сервере.

  • Вызовы функций или методов, включенных в запрос LINQ to Entities, сопоставляются с каноническими функциями платформы Entity Framework, которые затем преобразуются в Transact-SQL и выполняются в базе данных SQL Server. Бывают ситуации, когда сопоставленные функции работают иначе, чем реализации в библиотеках базовых классов. Например, при вызове метода Contains, StartsWith или EndsWith с пустой строкой в качестве параметра при выполнении в среде CLR возвратит значение true, а при выполнении в SQL Server - значение false. Кроме того, при вызове метода EndsWith также могут вернуться различные результаты, поскольку SQL Server считает равными две строки, различающиеся только конечными пробелами, в то время как с точки зрения среды CLR эти строки не равны. Это продемонстрировано в следующем примере.

using (AdventureWorksEntities context = new AdventureWorksEntities())
{
    IQueryable<string> query = from p in context.Products
                               where p.Name == "Reflector"
                               select p.Name;

    IEnumerable<bool> q = query.Select(c => c.EndsWith("Reflector "));

    Console.WriteLine("LINQ to Entities returns: " + q.First());
    Console.WriteLine("CLR returns: " + "Reflector".EndsWith("Reflector "));
}
Using context As New AdventureWorksEntities()

    Dim query = _
        From p In context.Products _
        Where p.Name = "Reflector" _
        Select p.Name

    Dim q = _
        query.Select(Function(c) c.EndsWith("Reflector "))

    Console.WriteLine("LINQ to Entities returns: " & q.First())
    Console.WriteLine("CLR returns: " & "Reflector".EndsWith("Reflector "))
End Using