Строго типизированная работа с SQL-ем напрямую без LINQ
От: Alexander Polyakov  
Дата: 10.11.10 23:49
Оценка:
Хочется работать с 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 в нужный тип .
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.