Доброго времени суток, %username%! Вот и я решился на написание статьи на данном ресурсе. Речь пойдет о доступе к данным из приложений написанных на .NET, в частности на языке C#. Все мои мысли, и во что они в итоге вылились, я попытаюсь изложить под катом. Добро пожаловать!

Под СУБД в статье будем понимать реляционную СУБД. Забегая вперед, скажу, что представленная библиотека (фреймворк) не является заменой Entity Framework, не зависит от него и не имеет к нему никакого отношения.

Тем не менее, оттолкнёмся от вышеупомянутого фреймворка. Одна из его идей – попытка (и весьма удачная) введения абстракции доступа к данным. Иными словами уход от конкретной СУБД.

Каждая СУБД имеет свои плюсы и свои минусы: некоторые имеет одни возможности, некоторые другие; некоторые делают хорошо одно, другие – другое и т.д. Давайте теперь представим, что взвесив все «за» и «против» мы выбрали некую СУБД для реализации какого-то грандиозного (или не очень) проекта и решили писать всё это с использованием… ADO.NET.

Плюсы ADO.NET:

  1. полный контроль над запросами;
  2. относительная простота: создать подключение, создать транзакцию (опционально), создать команду, добавить текст запроса и параметры, запустить, считать данные (опционально);
  3. есть поддержка практически для любой СУБД.
  4. тесное взаимодействие с СУБД (например, поддержка таких «нестандартных» типов данных как координаты, JSON и т.д.).

Минусы ADO.NET (предпосылки к реализации проекта):

  1. для каждой модели необходимо каждый раз заново писать код для чтения, добавления и изменения записи в БД – проще говоря отображения объекта на запись в таблице, представлении т.д. и, наоборот, отображение записи на объект;
  2. Нет абстрагирования как в Java (хотя есть DbConnection/DbCommand и другие классы, но часто используются конкретные типы, например SqlConnection/SqlCommand);
  3. Нет универсальной поддержки работы с пакетом записей (добавление, обновление, добавление или обновление, удаление);

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

Начнем по порядку.

  1. Что же мы можем сделать для того, чтобы написать этот код один раз? В первую очередь заметим, что костяк для чтения одного или нескольких объектов остается неизменным вне зависимости от того что это за объект и какие поля он содержит. Это означает, что нам необходима универсальная функция чтения одного объекта. То же верно для добавления и обновления записи.

    Как такая функция должна быть написана? Первое что приходит на ум – Reflection. Допустим. Но у Reflection есть один существенный недостаток – он медленный. Если при чтении/добавлении/изменении/удалении одного объекта быстродействие просядет несущественно, то при большом количестве объектов накладные расходы будут ощутимы.

    На помощь нам придут Expression'ы и возможность их компиляции «на лету». Идея состоит в том, что тело функции генерируется, компилируется и ссылка на нее сохраняется в виде делегата. Делать это необходимо только один раз – при инициализации.

    С чем должны работать функции? С тремя сущностями:

    • объект как таковой (модель);
    • объект чтения данных (например, SqlDataReader);
    • коллекция параметров (например, SqlParameterCollection).

    Для того, чтобы точка генерация данных функций была едина, введены следующие интерфейсы обёрток: IDbParameterCollectionWrapper и IReaderWrapper (см. ссылку на репозиторий проекта ниже). Для каждой СУБД необходимо реализовывать эти интерфейсы индивидуально. Забегая вперед: фреймфорк нацелен в первую очередь на скорость работы, поэтому в ряде случаев используется отложенная («ленивая») инициализация. Так же фреймфорк содержит несколько вспомогательных атрибутов для бoльшей гибкости (например, вычисляемые поля, обязательные поля и т.д.).

  2. Вся общая часть фреймфорка вынесена в отдельный общий проект. Для пользователя видны в основном интерфейсы. Крайне рекомендуется пользоваться только интерфейсами.

  3. Пакетная работа с записями пока не реализована, но это уже «дело техники».

Проект уже можно опробовать (см. ссылки ниже). Имеется поддержка Linq! Проект в альфа-версии, поэтому код пока неидеален.

Что планируется добавить:

  • больше тестов;
  • поддержка других баз данных: в первую очередь SQL Server, MySQL;
  • поддержка Microsoft.AspNet.Identity.

Ссылки:

» Проект WildData на GitHub.
» Nuget-пакет WildData.
» Nuget-пакет WildData (реализация для PostgreSQL).
» Очень простой пример использования фреймворка.

Строго прошу не судить. Это моя первая статья на хабрахабре. Спасибо за внимание!

P.S. По всем вопросам прошу в личку и комментарии.
Поделиться с друзьями
-->

Комментарии (17)


  1. Sybe
    19.12.2016 01:29
    +1

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


    1. apodavalov
      19.12.2016 01:30

      Благодарю! Вы правы, readme стоит обновить. Столько всего хотелось сделать, что до него руки так и не дошли пока…


      1. apodavalov
        19.12.2016 04:02

        Обновил readme.


  1. shibaev
    19.12.2016 07:24
    +2

    Стоит изначально добавить поддержку .NET Standard Library — это позволит использовать ваш фреймворк из ASP.NET Core приложений. На ранней стадии это сделать проще.


    1. apodavalov
      19.12.2016 09:27

      Спасибо! Хорошая идея!


  1. aevolodin
    19.12.2016 09:27

    Что значит нет абстрагирования? Если Вы его не используете, это не значит, что его нет.


    1. apodavalov
      19.12.2016 09:39

      Я перечислил классы DbConnection/DbCommand. Да, через DbProviderFactory можно получить DbConnection, а через него DbCommand и т.д. Но я говорю вот о чем: в случае скажем с PostgreSQL не приводя DbDataReader к NpgsqlDataReader (http://www.npgsql.org/api/Npgsql.NpgsqlDataReader.html) невозможно получить например NpgsqlTimeSpan (функция NpgsqlDataReader.GetInterval). TimeSpan сейчас еще не поддерживается фреймворком. Но это как раз является причиной использования конкретных реализаций.


      1. ColdPhoenix
        19.12.2016 10:06

        применив не общие типы вы уже отказались от абстрагирования.


        1. apodavalov
          19.12.2016 10:19
          -1

          Не совсем. Все-таки в интерфейсы оберток можно добавить любые методы, в реализациях выкинуть NotSupportedException, если СУБД по тем или иным причинам не поддерживает тип. Те интерфейсы которые объявлены в проекте будут использоваться одинаково, вне зависимости от того, какая реализация под капотом.


          1. Cromathaar
            19.12.2016 22:23

            И получите нарушение LSP, что чревато.


            1. apodavalov
              19.12.2016 22:57

              Почитал мат. часть. Да, похоже что нужно ввести метод, который проверяет поддерживается ли определенный тип обёрткой или нет. Спасибо за замечание, учту!


  1. madnut
    19.12.2016 10:15

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


    1. apodavalov
      19.12.2016 10:41

      Посмотрел, да похоже, но концепция ближе к EF всё-таки.


  1. Splo1ter
    19.12.2016 11:16

    fox_anthony, советую для нормального linq использовать re-linq)


    1. apodavalov
      19.12.2016 19:24

      Спасибо! Посмотрел пока мельком — то чего не хватало когда писал (превращение в SQL подобное с Select/Where писал сам и в данный момент в этой части кода пока не очень все нравится. Посмотрю подробнее.


  1. imanushin
    19.12.2016 16:30

    На помощь нам придут Expression'ы и возможность их компиляции «на лету». Идея состоит в том, что тело функции генерируется, компилируется и ссылка на нее сохраняется в виде делегата. Делать это необходимо только один раз – при инициализации.


    А почему не кодогенерация?

    Поясню:
    1. Генерированный код можно посмотреть и отладить, решарпер будет проверять его и делать подсказки. А вот у Expression такой штуки нет.
    2. Expression требует обязательного прогрева, кодогенерация его не требует.


    1. apodavalov
      19.12.2016 19:33

      Я использовал Expressions по следующим причинам:
      1) компилируется минимум и функции просты до безобразия. Все, что можно не компилировать — не компилируется. Кроме того, кроме компиляцию создаются ещё некоторые вспомогательные структуры для конструирования как встроенных во фрейморк, так и кастомных запросов. Сгенерированные функции проверять каждый раз решарпером — думаю это лишнее, остальной код проверить конечно же можно.
      2) их всего три и вряд ли предвидится больше.
      3) не хочется никаких лишних действий кроме как использовать репозиторий и передать ему модель (в том числе унаследовать и добавить функции).
      4) в силу природы отложенной инциализации репозиториев разогрев будет сущим пустяком по времени (я не думаю, что за один запрос будет использовано более 10 репозиториев).