Микромир ORM на примере Massive

12 февр. 2013 г. | | |

В мире больших и тяжеловесных ORM-решений, таких как NHibernate и Entity Framework, вполне предсказуемым стало появление течения микро-ORM: легковесных прослоек между БД и объектами приложения. Чем хороши microORM? Прежде всего, скоростью выполнения запросов: она сопоставима со скоростью чистых запросов через SqlDataReader. Потом - микро размерами: например, Massive - это всего лишь один подключаемый файл на 673 строки кода, в отличие от NHibernate или EF, которые тянут за собой целую dll-ку на несколько сот килобайт.  Наиболее известные из micro ORM:
1) Dapper, разработанный и активно использующийся в StackOverflow и StackExchange
2) Massive, разработанный Rob Conery, пример использования - HanselMinutes
3) PetaPOCO

Massive - это лучше, чем шоколад

Основа Massive - это dynamic-типы, появившиеся в .NET 4.0. Чтобы понять, что такое Massive, как и зачем он появился на свет, посмотриет видео  "Kill your ORM" с выступления Роба Конери на NDC 2011. А чтобы понять, насколько Massive лучше шоколада,  давайте напишем небольшое тестовое приложение, выводящее план счетов из БД в сгруппированом виде.

БД - MS SQL CE 3.5. Структура базы:
//таблица групп
CREATE TABLE [_accountGroups]
(
   [AccountGroupId] INT NOT NULL IDENTITY (1,1),
   [GroupName] NVARCHAR(100) NOT NULL DEFAULT N'',
   [ParentGroupId] INT,
   [GroupNum] INT NOT NULL DEFAULT0
);
//таблица счетов

CREATE TABLE [_accounts]
(
   [AccountId] INT NOT NULL IDENTITY (1,1),
   [AccountName] NVARCHAR(250) NOT NULL DEFAULT N'',
   [AccountNum] INT NOT NULL DEFAULT0,
   [GroupId] INT NOT NULL,  
   [ActualSaldo] MONEY NOT NULL DEFAULT0  
); 

Группы счетов могут содержать подгруппы. Каждый счет имеет определённую группу.
Теперь нужно добавить Massive в проект, взяв с github последнюю версию. На github-е можно найти разные версии Massive для работы с разными СУБД: PostgreSql, Oracle, Sqlite. Нам нужен стандарный Massive.cs. 

Как отобразить таблицу БД на объект приложения с помощью Massive? 

В Massive за это отвечает класс DynamicModel. Есть два способа получения данных из таблиц: 
1) создание класса таблицы, наследующего DynamicModel (который используется в тестовом приложении):
    public class Accounts : DynamicModel
    {
        public Accounts()
            : base("ChartOfAccount", "_accounts", "AccountId")
        {
        }
    }

    public class Groups : DynamicModel
    {
        public Groups()
            : base("ChartOfAccount", "_accountGroups", "AccountGroupId")
        {
        }
    }
Первым параметром в базовом конструкторе указывается имя connection string (об этом чуть ниже), вторым - имя таблицы, третьим - первичный ключ таблицы.
2) inline-определение через DynamicModel
var accounts = new DynamicModel("ChartOfAccount", tableName:"_accounts", primaryKeyField:"AccountId");

Откуда Massive берёт connection string? 

Massive смотрит в конфиг-файл приложения и ищет там секцию <connectionStrings>. Из всех вариантов выбирается тот, чьё имя указано в качестве первого параметра в конструкторе DynamicModel. В примере используется следующий app.config:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <connectionStrings>
    <add name="ChartOfAccount"
    connectionString="Data Source=demo.sdf;Password=blablabla;"
    providerName="System.Data.SqlServerCe.3.5" />
  </connectionStrings>
</configuration>

Заглянем на мгновение "под капот". Что происходит в конструкторе DynamicModel:
public DynamicModel(string connectionStringName, string tableName = "",
            string primaryKeyField = "", string descriptorField = "")
{
      /* не интересно */
      var _providerName = "System.Data.SqlClient";
      if (ConfigurationManager.ConnectionStrings[connectionStringName].ProviderName != null)
          providerName = ConfigurationManager.ConnectionStrings[connectionStringName].ProviderName;          
      _factory = DbProviderFactories.GetFactory(_providerName);      ConnectionString = ConfigurationManager.ConnectionStrings[connectionStringName].ConnectionString;
}
Тут интересен providerName,  по которому получается специфичная для СУБД DbProviderFactory. Если неправильно указать в конфиге providerName, можно получить исключение "Unable to find the requested .Net Framework Data Provider. It may not be installed.".
Собственно говоря, теперь можно делать select/insert/update/delete запросы к базе. Но рано радоваться. Давайте осуществим поставленную задачу: выведем план счетов по группам. Выводить будем используя WinForm в TreeView. Ничего лишнего.

Как сделать select в Massive? 

Можно использовать преимущества DynamicModel, где есть  полезные методы на все случаи жизни (All(), Count(), Single() etc.) или ручками выполнять чистый sql через Massive.DB.Current.Query();. Наш путь - это Dynamic Model. В методе All() есть несколько интуитивно-понятных параметров: where, orderBy, limit, columns (по умолчанию *, что значит выбрать все столбцы), args (массив аргументов для where). Всё просто и понятно.
Вернёмся к форме. На событие Load формы будем строить дерево плана счетов:
private void Form1_Load(object sender, EventArgs e)
{
    LoadChartOfAccount();
    tvChartOfAccount.Nodes[0].Expand();
}
private void LoadChartOfAccount()
{
    var groups = new Groups();
    var accounts = new Accounts();
    foreach (var group in groups.All(orderBy: "GroupNum"))
    {
        TreeNode tn = new TreeNode(group.GroupName);
        tn.Name = group.AccountGroupId.ToString();

        foreach (var account in accounts.All(where: "GroupID=@0", args: new object[] { group.AccountGroupId }, orderBy: "AccountNum"))
        {
             tn.Nodes.Add(
                    account.AccountNum.ToString() + " " +
                    account.AccountName + " " +
                    account.ActualSaldo.ToString()
              );
        }
        TreeNode[] parentNode = tvChartOfAccount.Nodes.Find((group.ParentGroupId ?? "root").ToString(), true);
        parentNode[0].Nodes.Add(tn);
    }           

Результат:

Вроде всё просто и понятно. Это если код не печатать, а использовать copy-paste :) А если печатать, то сразу же обнаружится печальная (в данном случае) особенность dynamic-типов: идентификация типов только на этапе исполнения. Это значит, что обращаясь к свойству dynamic-объекта, я могу написать что угодно, даже не существующее свойство (account.Blablabla), а исключение словлю только в момент выполенения кода, а не на этапе компиляции.   Это следует учитывать, работая с dynamic. Если в нашем примере опечататься и вместо group.AccountGroupId написать group.AccountGroupID, то можно обеспечить себе увлекательный поиск ошибки, т.к. Massive для имён полей объектов берёт названия столбцов таблицы как есть, поэтому регистр важен.
Если мы добавим немного функционала к приложению, например, поиск счета по номеру или названию, вскроется ещё один недостаток: подверженность slq-injection. Можно возразить, что это забота программиста, но вспоминая старый добрый Linq2Sql, о таком думать даже не приходилось. В Massive можно написать  accounts.All(where: "AccountNum LIKE '@0' OR AccountName LIKE '@0'", args: new object[] { filter } ) или accounts.All(where: "AccountName LIKE '"+filter+"' OR AccountNum LIKE '"+filter+"') со всеми вытекающими последствиями. Понятно, что в полноценных ORM, так же позволяющих выполнять чистый sql, тоже можно написать такого странного кода, но всё же там неподготовленный пользователь максимально ограничен от соприкосновения с чистым sql. В micro ORM пользователь находится в пограничном состоянии: вроде и не чистый sql, но и не объекты.

Заключение

Есть такое мнение:
и оно имеет право на жизнь. Хоть Massive возвращает объект, представляющий строку таблицы, но делает это особым образом: тип объекта (а соответственно, все его поля) становятся изветсны только во время исполнения кода, что имеет свои недостатки. Зато плюс - нет никаких огромных конфиг-файлов с описаниями объектов и связей между ними - не нужно создавать заново схему данных каждый раз, когда структура базы меняется (да, EF code-first - это круто). Нет отслеживания данных, вобще никаких фич, кроме select/update/insert/delete. Минималистично и быстро. Massive делает много рутинной работы: все эти датаридер, команды, параметры... - всё это скрыто в DynamicModel.
Стоит или не стоит использовать micro-ORM? Можно долго рассуждать на эту тему, разводить руками, пожимать плечами. В конечном итоге, это зависит от нужд проекта.

4 коммент.:

Antonov Igor комментирует...

Здравствуйте!

Я редактор бесплатного электронного журнала для программистов VR-Online (http://vr-online.ru). Хочу спросить вашего разрешения на публикацию данной статьи на страницах нашего журнала. Скажите, это возможно?

Shemyakina Tatiana комментирует...

Да, конечно. Ссылку на номер с публикацией пришлёте? :)

Antonov Igor комментирует...

Спасибо!

Мы временно не выпускаем pdf, поэтому материалы публикуем на сайте. Ссылку на публикацию обязательно пришлю. Еще раз спасибо!

Antonov Igor комментирует...

Статья опубликована. http://www.vr-online.ru/blog/mikromir-orm-na-primere-massive-9381. Огромное вам спасибо!

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