– Привет! В предыдущий раз, ты рассказывал про дата-сервис, про отдельный слой доступа к данным. Расскажи теперь про сами сущности и репозитории. При помощь чего ты вытягиваешь данные из таблиц?
– Ок. Если необходимо сохранять сделки и статистику, или откуда-то брать исторические котировки для тестов, то неплохо использовать БД. Но, как с ней общаться? Есть несколько способов. В C#, есть например традиционный ADO.NET, но речь пойдёт не о нём. В прошлый раз мы отделили работу с БД от бизнес-логики, это уже очень здорово, но можно пойти дальше! Есть способ общаться с самой БД на достаточно абстрактном уровне, инкапсулируя детали формирования самих запросов. Такой способ лучше вписывается в концепцию объектно-ориентированного проектирования, и называется он ORM (object relation mapping).
– Хм, я что-то слышал про ORM. У меня сложилось неоднозначное ощущение, вроде, есть целое сообщество, кто против них (OrmHate), и считает это антипаттерном. Все эти дополнительные уровни абстракции, и вообще, они наверно дико тормозные?
– Конечно, у любого инструмента или технологии есть своя область применения, вне которой они становятся неэффективными. ORM – это промежуточный слой между базой и объектами-сущностями. Он делает всю низкоуровневую работу – сам формирует запросы (select, update и пр.), в то время как мы работаем с коллекциями, выбираем по каким-то критериям или модифицируем их. В начало 2000x ORMы действительно были медленные, и даже в книжках, опытные авторы их не рекомендовали, и считали тупиковым путём. Но сейчас, они всё-таки с**ка крутые и быстрые!
– Да ладо?!
– На своём уровне конечно, опять же, hft’шники не будут их использовать непосредственно для торгов, скорее всего, это будут более низкоуровневые решения: чистый sql, или in-memory db. Но для большинства остальных случаев, это хорошее решение. ORM абстрагирует нас от специфики выбранной СУБД, т.е. если придётся мигрировать на другую СУБД, достаточно будет только поменять класс конфигурации, остальные части программы останутся прежними. Не нужно будет переписывать запросы. ORM является существенным подспорьем ООП, и суть его в том, чтобы не писать на чистом SQL, оставаясь в объектном контексте. Вообще, это очень обширная тема, в двух словах всего не рассказать.
– А каким ORM ты пользуешься?
– У Microsoft есть нативный Entity Framework, но я использую NHibernate, это форк с аналогичного проекта под Java. Но вообще их много.
— И как им начать пользоваться?
— Для работы нужно сконфигурировать доступ к базе и объектную модель – нужно указать ORM какие сущность на какие таблицы проецируются, какие правила связи, ограничения, правила именования таблиц и полей, правила генерации значений первичного ключа и т.д.
Один из вариантов настройки NHibernate, это расширение обобщённого интерфейса IAutoMappingOverride для настройки сущностей и расширение интерфейсов из набора соглашений (Convention’s). Приведу примеры. Заявка содержит коллекцию сделок прошедших по ней, следовательно и таблицы связанны отношением один ко многим. Нужно указать, что сделка ссылается на заявку, а заявка имеет коллекцию сделок. Код, приведённый ниже, настраивает такую связь:
public class TradeMapping : IAutoMappingOverride<Trade> { public void Override(AutoMapping<Trade> mapping) => mapping.References(t => t.Order); } public class OrderMapping : IAutoMappingOverride<Order> { public void Override(AutoMapping<Order> mapping) => mapping .HasMany(m => m.Trades).KeyColumn("ID_ORDER"); }
Ещё пример. В C# принята PascalCase нотация именования полей: OrderId, TradeNo и т.д., а колонки БД принято именовать через подчёркивание (snake_case): order_id, trade_no. Причём, зачастую, регистр не важен. Код соглашения, приведённый ниже, решает проблему сопоставимости полей классов и колонок БД:
public class ColumnNameConvention : IPropertyConvention { public void Apply(IPropertyInstance instance) { var column = Regex.Replace( instance.Property.Name, @"([A-Z])", "_$1") .Substring(1).ToUpper(); instance.Column(column); } }
Таких настроек может достаточно много. В итоге, мы имеем возможность описать все аспекты связи с БД.
– Да, понятнее меньше не стало. Допустим, мы настроили ORM, что дальше?
– Теперь можно перейти к репозиториям и посмотреть их внутреннюю реализацию. Например, получить список всех заявок и сделок по ним по определённой стратегии можно таким образом:
var result = session.QueryOver<Trade>() .JoinQueryOver(t => t.Order) .Where(o => o.IdStrategy == 1) .List();
Данная конструкция преобразуется в запрос вида:
SELECT this_.ID, this_.TRADE_NO, ... FROM TRADES this_, ORDERS order1_ WHERE this_.ID_ORDER=order1_.ID AND order1_.ID_STRATEGY = 1
После выполнения запроса, в переменной result окажется искомая выборка. Ещё пример: надо получить тиковые данные из БД по определённому символу:
TickDTO tickInfo = null; return session.QueryOver<Tick>() .Where(t => t.Symbol == "SBER") .OrderBy(t => t.Id).Asc() .SelectList(list => list .Select(t => t.DateTime).WithAlias(() => tickInfo.DateTime) .Select(t => t.Vol).WithAlias(() => tickInfo.Vol) .Select(t => t.Price).WithAlias(() => tickInfo.Price)) .TransformUsing(Transformers.AliasToBean<TickDTO>()) .List<TickDTO>();
Конструкция выше автоматически преобразуется ORM в запрос вида:
SELECT this_.DATE_TIME AS y0_, this_.VOL AS y1_, this_.PRICE AS y2_ FROM TICKS this_ WHERE this_.SYMBOL = "SBER" ORDER BY this_.Id ASC
TransformUsing используется для конвертации Tick в TickDTO, т.к. между слоями приложения лучше передать DTO объекты, а не сущности (об этом я уже говорил ранее). А вот так, например, в базу сохраняются заявки и сделки по ним:
public void SaveOrders(List<Order> orders) { var stopwatch = new Stopwatch(); stopwatch.Start(); using (var s = sessionFactory.OpenSession()) using (var tr = s.BeginTransaction()) { foreach (Order order in orders) { s.SaveOrUpdate(order); } tr.Commit(); } stopwatch.Stop(); }
Кстати, тут используется объект Stopwatch из стандартного пакета System.Diagnostics, для оценки времени выполнения операции.
– Неужели все запросы можно представлять в таком виде?
– Подобным образом, можно составлять достаточно сложные запросы: со вложенными подзапросами, с группировками, со сложной системой условий. Вначале такой подход кажется неудобным, но если привыкнуть, то это даст свои преимущества. Конечно, есть исключения – запросы, которые лучше (и быстрее) написать на чистом SQL, но, к слову сказать, в 90% случаев, очень сложный запрос, это признак плохой архитектуры схемы БД.
– А как на счёт производительности?
– В данном примере, речь не идёт о погоне за временем, и, в любом случае, за абстракцию в виде ORM надо платить. Но, на мой взгляд, при грамотном подходе, потери по сравнению с написанием на чистом SQL будут невелики. На моём, достаточно слабеньком компе (I3), получение месяца тиковой истории по Газпрому (~800000тыс тиков) занимает 7-8 секунд. За это время ORM выполняет запрос, получает все данные и маппит в DTO. Сохранение, идёт чуть дольше – ~200тыс сделок сохраняются за 30-40 секунд. На мой взгляд, это хороший результат!
– Да уж… сложно всё это. Так каков итог?
– Если бы мне, в момент торгов, было бы необходимо тянуть данные из БД, и скорость была бы важна, то, скорее всего, я бы отказался от ORM. А для периферийных вещей ORM – хороший вариант. В любом случае, в сложных проектах, эти решения часто переплетаются: слой доступа к БД делится на read model и write model, и, внутри read model, могут быть сложные запросы, использующие т.н. представления (view’s) из БД или чистый SQL в самом проекте.
До этого много лет работал с Firebird и ревностно относился к новинкам Microsoft