Блог им. WinterMute

Торговая система своими руками. Часть 6. Работа с БД. Объектно-реляционное отображение.

    • 25 сентября 2017, 11:29
    • |
    • k100
  • Еще

– Привет! В предыдущий раз, ты рассказывал про дата-сервис, про отдельный слой доступа к данным. Расскажи теперь про сами сущности и репозитории. При помощь чего ты вытягиваешь данные из таблиц?

– Ок. Если необходимо сохранять сделки и статистику, или откуда-то брать исторические котировки для тестов, то неплохо использовать БД. Но, как с ней общаться? Есть несколько способов. В 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 в самом проекте.

★9
11 комментариев
я тоже получал удовольствие от Entity Framework. 
До этого много лет работал с Firebird и ревностно относился к новинкам Microsoft
avatar
Андрей К, dapper посмотрите. Работает существенно быстрее чем EntityFramework
avatar
_sg_, спасибо, запомню. Надобности в ежедневном БД давно отпали. Но почитал, интересно.
avatar
 
Сохранение, идёт чуть дольше – ~200тыс сделок сохраняются за 30-40 секунд. 
Наверное стоит оговориться, что за БД вы используете. По контексту понятно, что скорее всего Micosoft SQL Server, но все же для ясности.
avatar
Андрей К, я использую Oracle, но с SQL Server скорость будет та же. Кстати, советую посмотреть postgreSQL как серьёзная open source альтернатива.
avatar
Если на каждый тик выделять запись в базе данных, грузится это будет очень долго. Собрать дневки за год из тиков будет очень проблематично. Да и база будет занимать кучу гигабайт по одному символу.
avatar
Александр, месяц тиковых данных ~ от 25 до 70 мб. Собрать дневки — не проблема — в следующем посте я как раз про это напишу. А так, трейдинг вообще непростое дело )
avatar
k100, У меня по 9 ликвидным фьючерсам с 2014 года тиковая база занимает 3 гига. Тики хранятся в blob в бинарном формате, прожатом zip архивом. Каждый день — одна запись, в которой хранятся тики за день по одному инструменту. База будет 3 Гб. Без прожатия она занимала бы раз в 10 больше.
avatar
k100, В активные дни по си 25 мб тиков за один день накопится, там бывает больше 1 млн. тиков. Тащить все их из базы, ну прям не хилый массивчик будет.
avatar
Александр, Согласен, на срочке будет больше чем на споте.  А как Вы думаете hft'шники тестят, если нужна история стаканов? А вообще, хранение и манипулирование большими данными, это конечно отдельная тема, и, вопрос даже не в том «как тащить эти данные из базы», а «для чего это надо»? Т.е. для проверки гипотезы тестер не нужен, исследовательскую работу можно проводить на языке СУБД (например sql или R). В таком случае можно использовать специальные механизмы СУБД — индексирование, кластеризация, в общем оптимизация хранения и выборки. Это даёт возможность эффективнее манипулировать данными и быстрее получать результат. И тестер тут — последнее дело.
avatar
 А как Вы думаете hft'шники тестят, если нужна история стаканов?
Пилят свой формат хранения стаканов, хранят только изменения в стакане.
Т.е. для проверки гипотезы тестер не нужен
Получить горизонтальный объем, тики нужны.
avatar

теги блога k100

....все тэги



UPDONW
Новый дизайн