Под СУБД в статье будем понимать реляционную СУБД. Забегая вперед, скажу, что представленная библиотека (фреймворк) не является заменой Entity Framework, не зависит от него и не имеет к нему никакого отношения.
Тем не менее, оттолкнёмся от вышеупомянутого фреймворка. Одна из его идей – попытка (и весьма удачная) введения абстракции доступа к данным. Иными словами уход от конкретной СУБД.
Каждая СУБД имеет свои плюсы и свои минусы: некоторые имеет одни возможности, некоторые другие; некоторые делают хорошо одно, другие – другое и т.д. Давайте теперь представим, что взвесив все «за» и «против» мы выбрали некую СУБД для реализации какого-то грандиозного (или не очень) проекта и решили писать всё это с использованием… ADO.NET.
Плюсы ADO.NET:
- полный контроль над запросами;
- относительная простота: создать подключение, создать транзакцию (опционально), создать команду, добавить текст запроса и параметры, запустить, считать данные (опционально);
- есть поддержка практически для любой СУБД.
- тесное взаимодействие с СУБД (например, поддержка таких «нестандартных» типов данных как координаты, JSON и т.д.).
Минусы ADO.NET (предпосылки к реализации проекта):
- для каждой модели необходимо каждый раз заново писать код для чтения, добавления и изменения записи в БД – проще говоря отображения объекта на запись в таблице, представлении т.д. и, наоборот, отображение записи на объект;
- Нет абстрагирования как в Java (хотя есть DbConnection/DbCommand и другие классы, но часто используются конкретные типы, например SqlConnection/SqlCommand);
- Нет универсальной поддержки работы с пакетом записей (добавление, обновление, добавление или обновление, удаление);
Ввероятнее всего читатель уже догадался, что мы будем думать как избавляться от вышеупомянутых недостатков. Поговорим о том, что является ключом к реализации всего проекта.
Начнем по порядку.
- Что же мы можем сделать для того, чтобы написать этот код один раз? В первую очередь заметим, что костяк для чтения одного или нескольких объектов остается неизменным вне зависимости от того что это за объект и какие поля он содержит. Это означает, что нам необходима универсальная функция чтения одного объекта. То же верно для добавления и обновления записи.
Как такая функция должна быть написана? Первое что приходит на ум – Reflection. Допустим. Но у Reflection есть один существенный недостаток – он медленный. Если при чтении/добавлении/изменении/удалении одного объекта быстродействие просядет несущественно, то при большом количестве объектов накладные расходы будут ощутимы.
На помощь нам придут Expression'ы и возможность их компиляции «на лету». Идея состоит в том, что тело функции генерируется, компилируется и ссылка на нее сохраняется в виде делегата. Делать это необходимо только один раз – при инициализации.
С чем должны работать функции? С тремя сущностями:
- объект как таковой (модель);
- объект чтения данных (например, SqlDataReader);
- коллекция параметров (например, SqlParameterCollection).
Для того, чтобы точка генерация данных функций была едина, введены следующие интерфейсы обёрток: IDbParameterCollectionWrapper и IReaderWrapper (см. ссылку на репозиторий проекта ниже). Для каждой СУБД необходимо реализовывать эти интерфейсы индивидуально. Забегая вперед: фреймфорк нацелен в первую очередь на скорость работы, поэтому в ряде случаев используется отложенная («ленивая») инициализация. Так же фреймфорк содержит несколько вспомогательных атрибутов для бoльшей гибкости (например, вычисляемые поля, обязательные поля и т.д.).
- Вся общая часть фреймфорка вынесена в отдельный общий проект. Для пользователя видны в основном интерфейсы. Крайне рекомендуется пользоваться только интерфейсами.
- Пакетная работа с записями пока не реализована, но это уже «дело техники».
Проект уже можно опробовать (см. ссылки ниже). Имеется поддержка Linq! Проект в альфа-версии, поэтому код пока неидеален.
Что планируется добавить:
- больше тестов;
- поддержка других баз данных: в первую очередь SQL Server, MySQL;
- поддержка Microsoft.AspNet.Identity.
Ссылки:
» Проект WildData на GitHub.
» Nuget-пакет WildData.
» Nuget-пакет WildData (реализация для PostgreSQL).
» Очень простой пример использования фреймворка.
Строго прошу не судить. Это моя первая статья на хабрахабре. Спасибо за внимание!
P.S. По всем вопросам прошу в личку и комментарии.
Комментарии (17)
shibaev
19.12.2016 07:24+2Стоит изначально добавить поддержку .NET Standard Library — это позволит использовать ваш фреймворк из ASP.NET Core приложений. На ранней стадии это сделать проще.
aevolodin
19.12.2016 09:27Что значит нет абстрагирования? Если Вы его не используете, это не значит, что его нет.
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 сейчас еще не поддерживается фреймворком. Но это как раз является причиной использования конкретных реализаций.
ColdPhoenix
19.12.2016 10:06применив не общие типы вы уже отказались от абстрагирования.
apodavalov
19.12.2016 10:19-1Не совсем. Все-таки в интерфейсы оберток можно добавить любые методы, в реализациях выкинуть NotSupportedException, если СУБД по тем или иным причинам не поддерживает тип. Те интерфейсы которые объявлены в проекте будут использоваться одинаково, вне зависимости от того, какая реализация под капотом.
Cromathaar
19.12.2016 22:23И получите нарушение LSP, что чревато.
apodavalov
19.12.2016 22:57Почитал мат. часть. Да, похоже что нужно ввести метод, который проверяет поддерживается ли определенный тип обёрткой или нет. Спасибо за замечание, учту!
madnut
19.12.2016 10:15Есть очень похожий проект, Insight.Database, не смотрели? Использую его у себя в проекте, очень правильный на мой вкус градус удобства и черной магии под капотом.
Splo1ter
19.12.2016 11:16fox_anthony, советую для нормального linq использовать re-linq)
apodavalov
19.12.2016 19:24Спасибо! Посмотрел пока мельком — то чего не хватало когда писал (превращение в SQL подобное с Select/Where писал сам и в данный момент в этой части кода пока не очень все нравится. Посмотрю подробнее.
imanushin
19.12.2016 16:30На помощь нам придут Expression'ы и возможность их компиляции «на лету». Идея состоит в том, что тело функции генерируется, компилируется и ссылка на нее сохраняется в виде делегата. Делать это необходимо только один раз – при инициализации.
А почему не кодогенерация?
Поясню:
1. Генерированный код можно посмотреть и отладить, решарпер будет проверять его и делать подсказки. А вот у Expression такой штуки нет.
2. Expression требует обязательного прогрева, кодогенерация его не требует.apodavalov
19.12.2016 19:33Я использовал Expressions по следующим причинам:
1) компилируется минимум и функции просты до безобразия. Все, что можно не компилировать — не компилируется. Кроме того, кроме компиляцию создаются ещё некоторые вспомогательные структуры для конструирования как встроенных во фрейморк, так и кастомных запросов. Сгенерированные функции проверять каждый раз решарпером — думаю это лишнее, остальной код проверить конечно же можно.
2) их всего три и вряд ли предвидится больше.
3) не хочется никаких лишних действий кроме как использовать репозиторий и передать ему модель (в том числе унаследовать и добавить функции).
4) в силу природы отложенной инциализации репозиториев разогрев будет сущим пустяком по времени (я не думаю, что за один запрос будет использовано более 10 репозиториев).
Sybe
Рекомендую обновить readme на гитхабе. Сейчас там нет никакой информации об использовании. Я понимаю, что есть тесты, но первое впечатление о проекте складывается именно по readme-файлу, как мне кажется
apodavalov
Благодарю! Вы правы, readme стоит обновить. Столько всего хотелось сделать, что до него руки так и не дошли пока…
apodavalov
Обновил readme.