AcroDb. Новый провайдер данных

Автор оригинала: Oleksiy Glib
  • Перевод
Рождение новой CMS-системы часто начинается с проектирования архитектуры и реализации самых простых блоков этой архитектуры.
Сегодня, я начинаю цикл статей, посвящённых новой системе управления сайтом реализованной на языке C# и платформе .Net. Система планируется быть с открытым исходным кодом, и данными статьями, я постараюсь описать все её элементы в порядке их начального появления и проектирования ещё года 2 назад.
Началом был универсальный слой доступа к любому провайдеру данных (то ли реляционная БД, или не реляционная, или даже своя организация базы данных).
Кого заинтересовал, прошу под кат.

Как и всё, что мы хотим сделать гениальным и простым, должно быть ещё и удобным, быстрым, и гибким. Так хотелось сделать и со слоем доступа к данным. Прошу заметить, любым данным :)
Самый простой метод для программиста был всегда использование какой-либо ORM системы. В .Net Framework этого добра хватает, начиная с истоков — NHibernate, Nolics, LINQ2SQL, Entity Framework, и куча самописных… Какие шаги обычно должен сделать разработчик, чтобы подключить какую-либо ORM систему к проекту?
  • Выбор бд (обязательно реляционной)
  • Выбор ORM-системы для работы с бд
  • Создание таблиц и связей в бд
  • Создание прототипов таблиц в виде классов в коде (у LINQ2SQL этот шаг можно автоматизировать утилитой sqlmetal, а EF вообще сам делает это)
  • Для таких систем как Nolics.net нужен ещё шаг описания интерфейса для прототипа таблицы и описание структуры таблицы в его собственном языке, но отпадает нужда создавать таблицу в бд ручками. Nolics при старте сделает автоматическую миграцию, если его об этом хорошо попросить :)
  • Дальше разработчик пишет классы управления данными, как добавление, редактирование, удаление, выборка

И всё вроде как круто. Но! Приходит время, когда заказчику не нравится субд (может она дорогая или медленная а тут появляется MongoDB которая у всех на устах). И программисту приходится переписывать и переделывать весь слой доступа к данным. Опять почти всё те же шаги (прошу не пинать меня любителей гибкой архитектуры, в которой всё уже давно предусмотрено, мы не о тех задачах пока говорим:) ).
Вот и родилась мысль унифицировать доступ к разным провайдерам предоставляя интерфейс CRUD операций и не предоставляя отношений между таблицами (не все провайдеры бд знают об отношениях, а как проделать отношения в AcroDB я расскажу в следующих статьях). Чего я хотел добиться?
  • Создать гибкий движок-замену слою доступа к данным для среднего размера проектов и малых тоже
  • Создать метод описания таблицы только один раз и в одном месте
  • Чтобы использование ORM было скрыто от разработчика, и запросы проводились только выражениями LINQ
  • Чтобы при отстутствии описания таблицы в провайдере данных (субд) автоматически при запуске создавалась данная таблица, без участия разработчика

Что из этого получилось? Проект AcroDB вы можете скачать с сайта github'а
Давайте посмотрим, что вам нужно для использования любой таблицы с её автоматическим созданием (подробный пример есть в подпроекте AcroDbTest в сорцах).
Первое, вам нужно описать интерфейс будующей таблицы:
  [AcroDbEntity]
  public interface IUser
  {
    Guid ID { get; set; }
    string Name { get; set; }
  }


* This source code was highlighted with Source Code Highlighter.

помечая его аттрибутом AcroDbEntity вы указываете системе что на основе этого интерфейча нужно создать в памяти класс который его имплементирует и использовать этот класс для работы с внутренней ORM-системой. неплохо? :) идём дальше.
Далее нам нужно при запуске проекта настроить подключение к нужному провайдеру данных (пока поддерживается MsSql и MongoDB для тестирования):
    static string[] SettingsCallBack(string name)
    {
      if (name == "MsSql")
        return new[] { @"server=OLEKSIY-PC\SQLEXPRESS;Database=acrodbtest;Trusted_Connection=True;" };
      return new string[0];
    }
    static void Main()
    {
      DataContextFactory.SettingsCallback = SettingsCallBack;
      DataContextFactory.Instance.ScanAssembly(typeof(MsSqlDataContext).Assembly);
      //...
    }


* This source code was highlighted with Source Code Highlighter.

Метод SettingsCallBack используется для подстановки конфигурационных данных для настройки провайдера данных. в нашем примере это строка подключения к СУБД.
Класс DataContextFactory это синглтон, который предоставляет по запросу контексты разных провайдеров данных. Для того чтобы DataContextFactory знал о новых провайдерах данных, его методу ScanAssembly нужно скормить сборку, где возможно есть информация о таком провайдере, и рефлексивно он выполнит поиск.
Теперь нам нужно настроить генератор контекстов доступа к данным на использование провайдера:
  AcroDataContext.DefaultDataContext = DataContextFactory.Instance.Get("MsSql");
  AcroDataContext.ScanAssemblyForEntities(Assembly.GetExecutingAssembly());


* This source code was highlighted with Source Code Highlighter.

AcroDataContext тоже синглтон, а метод ScanAssemblyForEntities ищет интерфейсы помеченные аттрибутом AcroDbEntity в сборке и создаёт классы-прототипы, для использования в ORM.
Как дальше пользоваться CRUD операциями:
  using (var manager = AcroDataContext.Go)
  {
    var usr = manager.Provide<IUser>().Create();
    usr.Name = "Some user name";
    manager.Provide<IUser>().Save(usr);
    manager.SubmitChanges();
  }


* This source code was highlighted with Source Code Highlighter.

Опишу тут только одну строку — AcroDataContext.Go. Это создание контекста подключения к провайдеру данных уже подключённого и готового к работе. Не забывайте его закрывать методом Dispose.
Для того, чтобы на основе описания вашего интерфейса в провайдере данных было создано описание таблицы (в данном случае в субд будет создана таблица Users с первичным ключём ID и полем Name с типом nvarchar(255)), нужно после сканирования сборок на интерфейсы вызвать метод «AcroDataContext.PerformMigrations()». Миграции делаются на основе SubSonic Migrations. Раньше они делались с помощью SQL SMO.

На этом не заканчивается функциональность данной библиотеки, но я с радостью опишу дополнительные элементы, если кому-то это будет нужно. А также, нетерпеливые могут прочитать исходный код :)
За первую статью прошу сильно не пинать, постараюсь ответить на все вопросы :)
Поделиться публикацией

Комментарии 10

    0
    1. В промышленных системах программа не имеет прав на изменение метаданных. Т.е. если программа не видит какой-то таблицы или столбца — она не может их динамически создать. А если может — DB админа уволят на следующий день. Для создания прототипов — да — удобно, для серьезного софта — нет. Малые и средние проекты тоже требуют серьезного подхода.

    2. Интерфейсы не могут содержать никакой вспомогательной логики. В этом плане POCO и partial classes намного удобнее, их можно расширять своими методами и данными.

    Короче, ваш фреймворк построен на любительских принципах, которые в индустриальных масштабах нежизнеспособны. Что-то рельсы напоминает.

      0
      Касательно №1. Тут и небыло ничего сказано о промышленных системах :). Само собой, что в больших и многих средних корпоративных системах, нужно проектировать свои слои и реализовывать их под нужную задачу. Есть сайты, программы, где работает только один человек и нужно создавать быструю и простую реализацию. На них данная штука и ориентирована. Также, вы можете атрибуту указать чтобы движок не изменял метаданные. Если вам того хочется :)

      Касательно №2. Логики в слое доступа к данным быть и не должно, но если вам так уже хочется добавить логику, используйте Extension методы для данного интерфейса. В системе есть даже вспомагательные классы для выделения прототипа из указателя на интерфейс. Если даже не хотите использовать методы расширений, в аттрибуте интерфейса укажите custom-класс для прототипирования, и он будет без динамического создания :)

      Попробуйте поговорить с разработчиками, которые работают с рельсами :) и скажите, что там любительские принципы :)

      Спасибо, за отзыв о статье.
        +1
        По поводу 1. Спорить не буду, у каждого свои понятия о прототипах как малых проектах.

        По поводу 2. Если есть возможность использовать нормальные классы, зачем заморачиваться интерфейсами вообще? Т.е. extensions methods — это хорошо, но extension fields и properties — еще, насколько мне известно, не придумали, а имитация их достаточно энергозатратна.

        По поводу рельсов. Я не говорил, что рельсы построены на любительских принципах. Возможно я плохо выразился, но я имел ввиду, что ваш фреймворк рельсы напоминает. А конкретно он рельсы напоминает тем, что база автоматом подстраивается под модель данных.
          0
          №1) я только предложил ещё один упрощённый вариант для начинающих) кому будет проще реализовывать слой доступа к данным так чем описывать всё ручками, хоть ручками они опыта наберут)

          №2) простой пример заморочки с интерфейсами: есть приложение, которое работает с MsSql базой данных. мы прописываем все аттрибуты Linq2Sql для прототипа. Затем, нужно поменять субд на MySql или MongoDB. и нам приходится ручками переписывать эти прототипы и их аттрибуты. а интерфейс — это простая замена своего языка описания прототипа таблицы, на основе которого будет создан класс с нужными аттрибутами на этапе выполнения приложения.

          Когда родилась данная идея основанная на лени (года 2 назад), я и понятия не имел о рельсах. А оказалось создал велосипед. Ну с кем не бывает?)
            0
            1) Сомневаюсь, что новички вообще найдут ваш проект и будут его использовать. Скорее те, кому интересно написание LINQ-провайдера.

            2) Если вам не нравится идея прописывания атрибутов от различных провайдеров на одних и тех же классах — никто не мешает
            а) вынести атрибуты/описание маппинга в отдельный файл, как это позволяют nHibernate, Linq2Sql, EF, etc.
            b) придумать свой формат описания классов и генерировать их c# код своим код-генератором на момент билда.
            c) придумать свои аттрибуты маппинга и PostSharp-ом преобразовывать их в конкретный вариант под выбранного провайдера.

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

            Опыт — вот главная цель.
              0
              Относительно опыта — согласен. Тут его было получено не мало. Даже глядя на ручную генерацию IL-кода. Касательно выноса маппинга в отдельный файл — в данном случае для linq2sql mssql в памяти и генерируется xml файл маппинга. А он всё равно для каждой ORM будет свой. Свой формат — я уже писал причину его отсутствия. PostSharp — уже стал платным, и неизвестно что с ним будет дальше.

              Касательно всего данного холивара: я не понимаю вашей агрессии на то, что это очередной велосипед. Времени на него было в сумме потрачено около пары недель. Не более. Как раз таки для опыта. И выложен он сюда в сорцах не для того, чтобы рассуждать что лучше использовать корпоративные решения и шаблоны, а для того, что кому-то могут пригодиться те грабли, на которые наступал и я. Проект не продаётся, и нигде не писалось, что это последняя инстанция правоты :)
              Кто захочет — возьмёт. Не захочет — конечно есть решения на порядок лучше и производительнее :)
                +1
                У PostSharpа есть и бесплатная редакция.

                Касательно какого холивара и какой-такой моей агрессии?

                ЗЫ. Да, и статью я плюсанул.
                Да, и зовут вас тоже забавно — Олексий.
                  0
                  Извините, значит так показалось :)
                  Олексий — украинская версия Алексея :)
                  0
                  Вместо PostSharp можно просто написать свой build task и использовать Mono.Cecil, например.
                    0
                    тоже вариант, но нужно настраивать среду проекта. а в предложенном варианте — цель уменьшить телодвижения разработчика.

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

      Самое читаемое