В мире больших и тяжеловесных ORM-решений, таких как NHibernate и Entity Framework, вполне предсказуемым стало появление течения микро-ORM: легковесных прослоек между БД и объектами приложения. Чем хороши microORM? Прежде всего, скоростью выполнения запросов: она сопоставима со скоростью чистых запросов через SqlDataReader. Потом - микро размерами: например, Massive - это всего лишь один подключаемый файл на 673 строки кода, в отличие от NHibernate или EF, которые тянут за собой целую dll-ку на несколько сот килобайт. Наиболее известные из micro ORM:
1) Dapper, разработанный и активно использующийся в StackOverflow и StackExchange2) Massive, разработанный Rob Conery, пример использования - HanselMinutes
3) PetaPOCO
Massive - это лучше, чем шоколад
Основа Massive - это dynamic-типы, появившиеся в .NET 4.0. Чтобы понять, что такое Massive, как и зачем он появился на свет, посмотриет видео "Kill your ORM" с выступления Роба Конери на NDC 2011. А чтобы понять, насколько Massive лучше шоколада, давайте напишем небольшое тестовое приложение, выводящее план счетов из БД в сгруппированом виде.
БД - MS SQL CE 3.5. Структура базы:
БД - 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
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;
}
Собственно говоря, теперь можно делать 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. Если в нашем примере опечататься и вместо
Если мы добавим немного функционала к приложению, например, поиск счета по номеру или названию, вскроется ещё один недостаток: подверженность slq-injection. Можно возразить, что это забота программиста, но вспоминая старый добрый Linq2Sql, о таком думать даже не приходилось. В Massive можно написать
и оно имеет право на жизнь. Хоть Massive возвращает объект, представляющий строку таблицы, но делает это особым образом: тип объекта (а соответственно, все его поля) становятся изветсны только во время исполнения кода, что имеет свои недостатки. Зато плюс - нет никаких огромных конфиг-файлов с описаниями объектов и связей между ними - не нужно создавать заново схему данных каждый раз, когда структура базы меняется (да, EF code-first - это круто). Нет отслеживания данных, вобще никаких фич, кроме select/update/insert/delete. Минималистично и быстро. Massive делает много рутинной работы: все эти датаридер, команды, параметры... - всё это скрыто в DynamicModel.
Стоит или не стоит использовать micro-ORM? Можно долго рассуждать на эту тему, разводить руками, пожимать плечами. В конечном итоге, это зависит от нужд проекта.
Вроде всё просто и понятно. Это если код не печатать, а использовать 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 коммент.:
Здравствуйте!
Я редактор бесплатного электронного журнала для программистов VR-Online (http://vr-online.ru). Хочу спросить вашего разрешения на публикацию данной статьи на страницах нашего журнала. Скажите, это возможно?
Да, конечно. Ссылку на номер с публикацией пришлёте? :)
Спасибо!
Мы временно не выпускаем pdf, поэтому материалы публикуем на сайте. Ссылку на публикацию обязательно пришлю. Еще раз спасибо!
Статья опубликована. http://www.vr-online.ru/blog/mikromir-orm-na-primere-massive-9381. Огромное вам спасибо!
Отправить комментарий