Фасад доступа к данным — Оптимизация, Заключение

Построение запросов

Уже традиционно покажу сразу UML схему, чтобы вы смогли ощутить какие классы будут рассмотрены и как они зависят друг от друга.

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

Linq2Sql позволяет гибко работать с запросами в стиле fluent interface переводя их в sql запросы. Было бы упущением не воспользоваться этой возможностью в полной мере. Особенно учитывая то, что мы уже работаем с логическими таблицами L2S, которые реализуют интерфейс IQueryable. Да, когда мы в адаптере чтения обращаемся к контексту с методом GetTable<TData>(), мы получаем объект, который можно донастраивать с помощью методов Where(), разбирая пользовательские спецификации. Собственно в этом и состоит идея.

Следуя только что сказанному, интерфейс для построения запросов может выглядеть следующим образом:

Модифицировали запрос, если надо, и отдали его обратно. Отлично, теперь рассмотрим как это может быть реализовано.

NullQueryBuilder

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

Данный класс сделан абстрактным в угоду специфики использования классов оптимизации. Как и большинство других шаблонных классов из одного семейства, данные классы удобно автоматически находить и регистрировать в проекте с помощью StructureMap. SM не позволяет попробовать взять шаблонный класс. Т.е. на примере текущей реализации:

В StructerMap нет метода TryGetInstanceAs(), благодаря которому мы могли бы протестировать наличие типа в коллекции для IQueryBuilder, и в случае чего вернули бы конкретный NullQueryBuilder. Таким образом, приходим к необходимости объявлять класс NullQueryBuilder абстрактным, а пустые оптимизаторы наследовать от него.

Теперь можно заняться настоящим оптимизатором запросов.

Реализация оптимизации запросов

Прежде чем приступить к описанию запросов, скажу пару слов про спецификации, которые используются для построения оптимизированного запроса. Такие спецификации, как и все остальные наследуются от класса Specification, однако они так же должны предоставлять доступ к информации, по которой идет определение условия (хотя и не всегда).

В противном случае построить запрос было бы проблематично, так как строятся они следующим образом:

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

Если в первом случае, со спецификацией ActiveCustomer, мы можем построить запрос исходя из логического значения спецификации, то во втором случае, без дополнительной информации, которая в себе содержит спецификация – не обойтись. Хотя, по задумке, спецификация не должна раскрывать условия, по которым она фильтрует данные. Но это не страшное изменение, главное, чтобы это условие нельзя было изменить после создания конкретной спецификации.

Например, в результате применения к исходному запросу спецификации ActiveCustomer, получаем такой запрос:

Получаемые запросы советую тоже порой пересматривать, так как порой встречается страшное. К сожалению так сразу не изобразишь «страшное» и то, как от него избавится. Так же советую изучить, что L2S может перевести в SQL.

Такое впечатление, что все ключевые технические моменты  описаны и можно рассматривать тестовый проект в деталях, а так же более-менее четко представлять, как все реализовано и связано в таком подходе по получению данных из БД.

Независимые оптимизации

Под независимыми оптимизациями, я понимаю оптимизации, которые не зависят от типа данных. К ним, например, можно отнести использование методов Top(), Take(), Skip(). Так как они не привязаны к конкретным типам данных, а накладывают ограничения на количество записей, то для реализации этих методов логично будет выделить базовый класс, в котором будет проходить анализ по наличию таких спецификация и применению их к запросам.

Ограничения и особенности

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

  • Данный подход никак не оптимизирует работу с хранимыми процедурами. Если у вас до сих пор пишутся CRUD процедуры, то это не облегчит работу с ними. Данный подход отменяет сами эти процедуры.
    Так же данный подход никак не регламентирует и не облегчает работу с большими наборами хранимых процедур, если вы их используете в клиентском коде.
  • При данном подходе сложно сделать зависимость двух адаптеров от одного доменного класса. Т.е. если вы хотите в зависимости от ситуации записывать данные о заказчиках то в таблицу Customer, а то в таблицу TempCustomer, то это будет проблематично. Проблема решается созданием дополнительного класса TempCustomer. Что, впрочем, мне видится куда как более правильным решением вообще.
  • Несмотря на то, что весь код пронизан использованием UnitOfWork, вполне возможно работать с описанной структурой и без него. Проверено на практике на текущем проекте. Работы по устранению UoW не так уж и много, как может показаться на первый взгляд.
  • Придется порой работать с настройками элементов таблиц в графическом редакторе dbml. Иногда приходится явно указывать по каким полям искать исходные данные для обновления, что учитывать при обновлении/записи, а что нет. Следует ли проверять значения перед записью или нет. К сожалению, общих рекомендаций по решению описанных  проблем выдать для пункта нельзя. Все зависит от ситуации, и решения нарабатываются долгим опытным путем.
  • Данный подход не очень подойдет для высоконагруженных проектов. Так как скорость чтения у L2S не самая большая, хотя побольше чем у EF. Основным плюсом L2S является легкость записи связанных данных одной транзакцией.

 

Так как я стараюсь избегать использования хранимых процедур для простых действий с данными, то для меня описанный подход (с вариациями) служит верой и правдой уже около 5 лет.

Areas for improvement

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

Фасад

Возможно, стоит дополнить интерфейс фасада асинхронным получением данных. Но не для того, чтобы дергать его асинхронно из ViewModel, презентеров или контроллеров. Упомянутые структуры не должны по идее дергать фасад, а работать с сервисами обработки данных.

Трансляторы

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

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

Адаптеры

Над адаптерами тоже можно потрудиться. Уже сейчас разделено получение первичных данных из базы и восстановление их в доменные объекты. При таком разделении можно восстанавливать объекты многопоточно, что может дать неплохой выигрыш в подготовке данных из базы. Конечно же  не для всех типов данных такое можно будет провернуть, но если данные могут восстанавливаться независимо, то почему бы и нет? Данным процессом, как и многими другими в данной реализации можно управлять с помощью конструирования специальных спецификаций. Кроме того, для того, чтобы не усложнять излишне схему, потребуется вынести код регистрации элементов в UoW в адаптер, так как в противном случае надо будет делать потокобезопасное регистрирование объектов. Создание потокобезопасных объектов тот еще труд и проблема, так что лучше всего просто перенести ответственность, на мой взгляд, это вызовет меньше проблем, так как чтение из UoW (если оно будет) разными потоками проблем создавать не должно.

Если создать строгие правила по именованию полей в базе данных, использовать те же имена, что и в коде, а для первичных ключей задавать имена по правилу: Имя таблицы + Id, то можно не описывать каждый раз метод GetPredicate(), в адаптерах записи.

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

Да, могут быть составные натуральные ключи, но помня статью о составлении ключей, можно сказать, что суррогатный ключ для связи данных все равно будет присутствовать, так что такая оптимизация имеет право на жизнь.

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

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

Заключение

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

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

Простота использования превыше всего, скрытое стремление к волшебному методу DoAllStuff().

 

 

Hard’n’heavy!

Violet Tape

Оставить комментарий