#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()
!