Хочется работать с SQL-ем напрямую, и в тоже время хочется статической проверки sql statement-ов, параметров и результатов запросов. Intellisense тоже подтянется, если будет алгоритм статической проверки.
Собственно, в чем сложность? Если параметризованный sql statement известен в дизайн тайме, то упомянутая статическая проверка это лишь техническая проблема. Например, при использовании LINQtoSQL или Entity Framework можно запрос оформить в виде хранимой процедуры и сгенерировать строго типизированную обертку на C#-е. Правда, тут есть небольшое неудобство -- в хранимой процедуре надо вручную указывать типы параметров. Но это неудобство можно устранить, см. Приложение 1. Таким образом, со статическим SQL-ем проблем нет -- наше желание реализуемо.
Но потребности реальных проектов не укладываются в статический SQL. Причины бывают самые разнообразные.
Есть большое подозрение, что всю или почти всю динамичность, требуемую в реальных проектах, можно описывать следующим образом. Динамичность sql statement-а задается некоторым набором параметров, заданных на “конечных” множествах: Boolean, Enum, интерфейс с известным числом реализаций (вложенность конструкторов разрешается произвольная). Некоторая утилита (в post-build event-е) перебирает все комбинации таких параметров и генерирует sql statement-ы. Сгенерированные sql statement-ы проходят проверку. Еще динамичность может зависеть от массива неизвестной длинны, в этом случае в дизайн тайме будет генерироваться sql statement-ы для массива с Length=1,2,3,5, надо же где-то остановиться
. Последнее выглядит немного странным и похожим на кривизну, но при детальном рассмотрении выясняется, что кривизны нет.
Продемонстрирую описанную схему на вот этом
примереАвтор: Alexander Polyakov
Дата: 02.11.10
.
Создаем
Preprocessed Text Template с названием PartLinkQueryProvider (IntelliSense и Syntax Coloring обеспечивает
T4 Editor):
Дописываем partial класс к PartLinkQueryProvider и пишем вот такой код:
public partial class PartLinkQueryProvider : IQueryProvider<PartLinkInfo>
{
public readonly IParamManager<PartLinkParams> PartLinkParams;
private readonly IInnerPartLinkSwitcher innerPartLinkSwitcher;
public PartLinkQueryProvider(
IParamManager<PartLinkParams> partLinkParams, IInnerPartLinkSwitcher innerPartLinkSwitcher)
{
PartLinkParams = partLinkParams;
this.innerPartLinkSwitcher = innerPartLinkSwitcher;
}
}
public partial class PartLinkInfo
{
}
public partial class PartLinkParams
{
}
public interface IInnerPartLinkSwitcher
{
void AddSubQuery(Action<bool> action);
}
public class WithoutInnerPartLinkSwitcher : IInnerPartLinkSwitcher
{
public void AddSubQuery(Action<bool> action)
{
//no-op
}
}
public class WithInnerPartLinkSwitcher : IInnerPartLinkSwitcher
{
public readonly IParamManager<InnerPartLinkParams> InnerPartLinkParams;
private readonly bool latestPublicationOnly;
public WithInnerPartLinkSwitcher(
IParamManager<InnerPartLinkParams> innerPartLinkParams, bool latestPublicationOnly)
{
InnerPartLinkParams = innerPartLinkParams;
this.latestPublicationOnly = latestPublicationOnly;
}
public void AddSubQuery(Action<bool> action)
{
action(latestPublicationOnly);
}
}
public partial class InnerPartLinkParams
{
}
Запускаем build. В post build event-е запускается некоторая утилита. Утилита перебирает все классы, реализующие интерфейс IQueryProvider<T>. Утилита указывает на ошибки, если такие имеются. Если все хорошо, генерируются следующий код:
public partial class PartLinkParams
{
public readonly Guid GenericServicePartsPageID;
public readonly Guid ModelPublicationID;
public readonly Guid ServicePartsPageID;
public PartLinkParams(Guid genericServicePartsPageID, Guid modelPublicationID, Guid servicePartsPageID)
{
GenericServicePartsPageID = genericServicePartsPageID;
ModelPublicationID = modelPublicationID;
ServicePartsPageID = servicePartsPageID;
}
}
public partial class InnerPartLinkParams
{
public readonly Guid ModelID;
public readonly string LanguageCode;
public InnerPartLinkParams(Guid modelID, string languageCode)
{
ModelID = modelID;
LanguageCode = languageCode;
}
}
public partial class PartLinkInfo
{
public string PartNumber { get; set; }
}
Теперь мы можем выполнять в строго типизированной манере три варианта запроса, упомянутые в условии задачи:
public static void Main()
{
ExecutePartLinkQuery(
dbCommand =>
new PartLinkQueryProvider(
new PartLinkParams(
new Guid("..."),
new Guid("..."),
new Guid("...")
).AsQueryParams(dbCommand),
new WithoutInnerPartLinkSwitcher()
)
);
ExecutePartLinkQuery(
dbCommand =>
new PartLinkQueryProvider(
new PartLinkParams(
new Guid("..."),
new Guid("..."),
new Guid("...")
).AsQueryParams(dbCommand),
new WithInnerPartLinkSwitcher(
new InnerPartLinkParams(
new Guid("..."),
"en"
).AsQueryParams(dbCommand),
false
)
)
);
ExecutePartLinkQuery(
dbCommand =>
new PartLinkQueryProvider(
new PartLinkParams(
new Guid("..."),
new Guid("..."),
new Guid("...")
).AsQueryParams(dbCommand),
new WithInnerPartLinkSwitcher(
new InnerPartLinkParams(
new Guid("..."),
"en"
).AsQueryParams(dbCommand),
true
)
)
);
}
private static void ExecutePartLinkQuery(
Func<DbCommand, IQueryProvider<PartLinkInfo>> queryProviderFunc)
{
var dbCommand = ...
var queryProvider = queryProviderFunc(dbCommand);
queryProvider.FillDbCommand(dbCommand);
using (var reader = dbCommand.ExecuteReader())
{
while (reader.Read())
{
PartLinkInfo partLinkInfo = queryProvider.Materialize(reader);
Console.WriteLine(partLinkInfo.PartNumber);
}
}
}
Материализацию может осуществлять как самописная инфраструктура, так и метод
ObjectContext.Translate из Entity Framework.
Приложение 1.
Sql statement кладем в некоторое место (xml, cs, и т.д.), откуда его можно достать в дизайн тайме. Специальная утилита в дизайн тайме достает sql statement, объявляет все параметры типа binary и смотрит план запроса. В плане запроса будет соответствующий оператор неявного преобразования из binary в нужный тип
.