Сопоставление вычисляемых свойств в Linq-to-SQL с фактическими инструкциями SQL

#c# #.net #linq #linq-to-sql

Вопрос:

Я создал некоторые дополнительные функции в своих классах Linq-to-SQL, чтобы упростить разработку приложений. Например, я определил свойство, которое извлекает активные контракты из списка контрактов. Однако, если я попытаюсь использовать это свойство в лямбда-выражении или вообще в запросе, оно либо выдаст исключение, что нет оператора SQL, соответствующего этому свойству, либо создаст один запрос на элемент (= много обходов на сервер).

Сами запросы не являются чрезмерно сложными, например:

 var activeContracts = customer.Contracts.Where(w => w.ContractEndDate == null);
 

В то время как я хотел бы, чтобы это читалось как:

 var activeContracts = customer.ActiveContracts;
 

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

Есть ли способ указать в свойстве, какой SQL он должен генерировать. Или есть способ убедиться, что его можно использовать в запросе, как показано ниже?

 var singleContractCustomers = db.Customers.Where(w => w.ActiveContracts.Count() == 1);
 

Комментарии:

1. Каков тип вашей собственности?

2. Я использовал как простые, такие как bool, так и сложные, такие как IQueryable<Контракт>, и это дает примерно одинаковые результаты.

Ответ №1:

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

Тем не менее, я подозреваю, что вы могли бы немного его разбить. Попробуйте добавить (к клиенту):

     public static Expression<Func<Customer, bool>> HasActiveContract
    {
        get { return cust => cust.Contracts.Count() == 1; }
    }
 

Тогда вы должны быть в состоянии использовать:

     var filtered = db.Customers.Where(Customer.HasActiveContract);
 

Очевидно, что его трудно запустить (отсюда), чтобы увидеть, с каким TSQL он работает, но я был бы удивлен, если бы это выполняло циклы; Я бы ожидал, что это будет выполнять функцию COUNT() в TSQL. В качестве самого верхнего запроса вы также должны иметь возможность обернуть это:

     public IQueryable<Customer> CustomersWithActiveContract
    {
        get { return Customers.Where(Customer.HasActiveContract); }
    }
 

Работает ли что-нибудь из этого?

Ответ №2:

Это сработало как заклинание. Заявление SQL, сгенерированное CustomersWithActiveContracts, показалось мне прекрасным.

 {SELECT [t0].[CustomerID], [t0].[cFirstName], [t0].[cLastName]
FROM [dbo].[Customers] AS [t0]
WHERE ((
    SELECT COUNT(*)
     FROM [dbo].[Contracts] AS [t1]
    WHERE (([t1].[ContractEndDate] > @p0) OR ([t1].[ContractEndDate] IS NULL)) AND ([t1].[cId] = [t0].[cId])
    )) > @p1
}
 

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

Ответ №3:

Другим вариантом является использование Microsoft.Linq.Библиотека переводов. Это позволит вам определить ваше свойство и его перевод в Linq следующим образом:

 partial class Customer
{
    private static readonly CompiledExpression<Employee,IEnumerable<Contract>> activeContractsExpression
        = DefaultTranslationOf<Customer>
          .Property(c => c.ActiveContracts)
          .Is(c => c.Contracts.Where(x => x.ContractEndDate == null));

    public IEnumerable<Contract> ActiveContracts
    {
        get 
        { 
            // This is only called when you access your property outside a query
            return activeContractsExpression.Evaluate(this);
        }
    }
}
 

Затем вы можете запросить его следующим образом:

 var singleContractCustomers = db.Customers.WithTranslations()
                              .Where(w => w.ActiveContracts.Count() == 1);
 

Обратите внимание на звонок WithTranslations() . Это создает специальную оболочку IQueryable , которая заменит все ссылки на ваше вычисляемое свойство его переводом до того, как выражение запроса будет передано в Linq to SQL.

Кроме того, если вы включаете Microsoft.Linq.Translations.Auto пространство имен вместо System.Linq , то вам даже не нужно звонить WithTranslations() !