Нет, речь не про кэш в памяти. Так было бы слишком просто. У нас сегодня будет препарирован ORM, который честно запрашивает данные у реляционной СУБД, маппит в объекты, подключает связи и отдаёт в логику приложения в виде объектов. И всё на порядки быстрее, чем прямой запрос из кода приложения.
Да, здесь есть нюанс. Об этом нюансе, а также о том, зачем я написал в пятый раз кастомный ORM и будет эта статья. Эта разработка тесно переплетена с моей личной историей, когда я переходил с одной работы на другую, а затем был уволен. Я не хочу оставлять сухой технический текст, поэтому эта статья будет скорее рассказом моей работе в этой компании.
Код в статью я старался включать по минимуму. Он точно не полный и возможно ошибочный, потому что дорабатывался по мере написания статьи. Полный и исправленный вариант будет доступен по ссылке в конце статьи.
Летом 2022 года я устроился в другую компанию в качестве десктоп-разработчика с большим стажем. В причины перехода я углубляться не буду, они более чем стандартные.
Там как раз внутренняя система была двухзвенной, с клиентом на C# winforms и базой данных Oracle в облаке яндекса. Это важная деталь. Был главный офис и несколько поменьше, всем нужен был доступ к данным, поэтому там была какая-то железка, ходящая в VPN с облачным сервером.
Предыдущая команда, которая написала информационную систему, почему-то в полном составе покинула компанию за год до моего прихода. Насколько я понял, их там было четверо. Они выбрали именно такой стек технологий и написали всё с нуля. Почему они ушли, мне так и не сказали. Рассказывали аналогию про тонущий корабль, но я так её и не понял. Наверное, с какой-то притчей связано.
Последующие пару месяцев я провёл за изучением работы системы и правкой небольших багов. Попутно пытаясь выяснить, а чего же от меня хотят.
Устройство
Система была построена следующим образом: логика в БД, а шарп использовался как фронтенд. Для показа табличных данных берём запрос из ресурсов винформс, ну или вьюшку с той стороны, а результат получаем в DataTable, которые целиком передаётся в грид девэкспресса. И этот подход вызывал дикий перерасход памяти, так как все поля хранятся в виде object. Например Int32 будет занимать в забоксенном виде 24 байта вместо четырёх.
Логика в шарпе была в тех местах, где объекты изменялись или добавлялись. Это были обычные формы. Обычная логика для таких мест - получить данные, распихать по комбобоксам и текстбоксам, потом в обработчике OnOK() собрать всё, провалидировать, отправить в БД. Ничего такого сверхъестественного. Только очень неряшливо было сделано. Например, на каждое изменение поля формы могло вызваться перезаполнение полей, причём зачем-то дважды. Возможно, это для каких-то полей имело смысл, но я этого так и не узнал. Перезаполнение - это перезапрос данных у БД, пересчёт всего считаемого и распихивание свежих данных по субконтролам. Впринципе, понятно - ребята просто не заморачивались с такими мелочами. Работает же.
Несмотря на обилие комментариев, их полезность стремится к нулю, потому что они описывают, что делает следующая строчка (я это и так вижу), а не почему так сделано. А иногда комментарии просто врут. То есть буквально код выглядит вот так:
// если передавали проект - выберем его
if (LocalProjectCode != null)
cbProject.SetEditValue(Convert.ToDecimal(LocalProjectCode));
// если передавали проект - выберем его
if (LocalWarehouseCode != null)
cbWarehouse.SetEditValue(Convert.ToDecimal(LocalWarehouseCode));
// выполноим запрос и поддтянем данные
qDates.Open();
//
this.MdiParent = AMdiParent;
(авторская орфография сохранена)
Общее впечатление о коде: грязно. Очень грязно. Кода много, он писался второпях и никогда не приводился в порядок. Комментарии только увеличивают энтропию кода, не привнося упорядоченности.
Сама архитектура была спонтанно-хаотичной. Предыдущая команда отталкивалась от списков и окошек редактирования. Списки были в одной папке, формочки - в другой.
Мне очень хотелось сразу сделать акцент на бизнес-процессах, а не на терминах разработки. У меня вообще свой подход к архитектуре, основанный на верхнеуровневом разделении по терминам бизнеса, включая процессы, отделении плана от факта, фиксации данных на момент перевода статусов. Когда-то мне сказали, что я поклонник DDD. Не знаю, возможно. Но разделение на формочки и списочки на самом верхнем уровне мне было чуждо.
База данных
Я никогда раньше не сталкивался с тем, что всю логику приложения выносят в базу данных. Я и с ораклом-то никогда раньше не сталкивался. Ну БД и БД. Там циферки какие-то и буковки хранятся, что там ещё может быть?
Не в этом случае. Здесь я увидел нагромождение вьюшек и хранимых процедур. Процедура берёт вьюшку и что-то делает. Вьюшка берёт вьюшку, а та - другую. Названия вьюшек состоят из сокращений и аббревиатур, ничего не говорящих постороннему наблюдателю. Что постоянно в нейминге - так это смурфовый префикс по названию компании. Он везде.
Внутри вьюшех - хаос из CROSS LATERAL JOIN, UNION, WITHIN GROUP, CONNECT BY и всё обильно присыпано NVL, LISTAGG, REGEXP и COALESCE. Никаких промежуточных переменных, никаких комментариев, только суровый декларативный синтаксис. В хранимках немного лучше, там есть комментарии, но в том же стиле, что и в коде шарпа. Хранимки в основном занимаются внесением сущностей в БД и их изменением.
Чтобы вы понимали, насколько там была смещена логика в сторону БД, приведу один яркий пример. В одной таблице был булевый флаг, отвечающий за срочность. В гриде шарпа он отображался в виде юникод-значка огня перед текстовым полем. Так вот, этот значок встраивался в текст на стороне БД.
Кое-что мне в самой структуре базы не нравилось по субъективным причинам. Например сквозной айди через все таблички. Есть вспомогательная табличка на пять строк, айдишники там идут так: 97, 98, 16962746234, 16962746235, 45712155667. Оно, конечно, дело вкуса, но на эту маленькую табличку ссылается двухколоночная таблица-связка в 40 млн записей. И вместо байта на один айди там будет бигинт в 8 байт (ну я так, примерно). Помножить оверхед на 40 млн - и получится уже существенно. А база данных там была реально распухшей, и это было проблемой.
Кстати, айдишник там везде назывался "Code". Это меня слегка раздражало, как и постоянное использование "DAT" вместо "DATE". И почему-то было смешно читать название "DAT_SUPPLY".
Я ещё обнаружил репозиторий с файлами Liquibase, куда предыдущая команда пару раз закоммитила структуру БД вместе с хранимками. К сожалению, потом они забили на коммиты в дальнейшем и актуальная версия хранимок была только на прод-сервере.
Задачи
На второй месяц багфиксов этой системы меня начали посвящать в задачи по этой системе. Это, скорее, был набор боли, которая мешала компании работать, а не задачи. Выглядит это примерно так:
система тормозит и глючит
люди работают в экселе, им неудобно
очень медленно работает система
не хватает интеграции с экселем, когда сотрудники сделали несколько сверхсложных таблиц в экселе и надо это как-то поддержать в информационной системе
очень сильно тормозит
часто сваливается с ошибкой в критичной операции
сама операция длится 20-30 минут
операция вылетает по OutOfMemory на клиенте
некоторые окошки открываются 2-3 минуты
Впрочем, одна глобальная задача всё-таки была. Поставщики постоянно скидывали таблицы с перечнем товаров, которые нужно было обработать, поправить, согласовать и превратить в список закупок у этого поставщика. Вроде простая задача, но были пункты со звёздочкой: у крупного поставщика этот список в среднем был 60к строк, обрабатывать должен был целый отдел в 10-15 менеджеров под распределением координатора. Происходило это в экселе, в однопоточном режиме. Сначала работал координатор, потом по очереди передавали файлик по менеджерам. Естественно, это было долго, нужно было операцию распараллелить, чтобы драматически снизить время обработки.
Глобальной мечтой менеджмента был некий "disaster check". По русски - свод неких валидаторов, которые на этапе планирования предупреждают о грядущих эпик фейлах. Эх, не получилось по-русски.
Проблема была в том, что в информационной системе попросту не было данных планирования. Всё, что было, было по факту. Планирование всё было в экселе. Так как этот пункт без пункта №1 не делался, я его опущу.
Со мной в паре работал аналитик, совсем недавно нанятый компанией для нужд команды разработки. Парнишка грамотный, въедливый, трудолюбивый и иногда до раздражения упёртый. То, что надо для аналитика. Вот он и занимался документацией "as is", допросом бизнеса, фиксацией хотелок, проектированием процессов под эти хотелки и превращением всего этого добра в подробную структуру сущностей, припудривая распределением ролей - кто что может, а кто что не может.
План
Ну что-ж. Проблемы с тормозами я решу как-нибудь. Со временем. Там просто очень грязно код написан, нужно детально разобраться, сделать аккуратно, что-то закешировать и всё будет летать. Яж профессионал. Синьёр. Ага.
Кодовую базу тоже причешем. Не может это быть сложно. Ну большие портянки SQL, подумаешь! Понемногу будем вникать, упрощать, упорядочивать. Преобразования перекидывать на сторону шарпа, на стороне БД по возможности оставлять только хранение данных.
Прикрутим сбоку Entity Framework и будем постепенно переводить логику на него. Это снизит потребление памяти, а самое главное - сделает код сильно короче и понятнее. Бонусом отслеживание использования каждого поля, что очень пригодится в наведении порядка в структуре самой БД.
Фичу напишем, тоже ничего сложного. Стек понятный, задача понятна, чего ждать-то?
Я всё время помнил про NiH-синдром и про то, что я могу ошибочно воспринимать чужую кодовую базу как очень плохую и требующую переписывания. Поэтому старался такие мысли гнать подальше и сосредоточиться на плавных изменениях. Бизнес очень не любит переписывать заново то, что такими затратами ему далось ранее.
Вот с таким планом я к бизнесу и пришёл. Сказал, что ни в коем случае не будем всё переписывать, будем постепенно улучшать то, что у нас есть. Вы в надёжных руках профессионала. Ага.
Тормоза
К середине второго месяца синьёр-помидор догадался поставить брейкпоинт в метод инициализации окошка, где заполняются все поля. Очень уж тяжело было отлаживать это окошко - каждый раз после начала его открытия мозг давал команду ногам топать в сторону кофемашины. А столько кофе пить вредно. Надо что-то делать.
Так вот, пошаговый дебаг показал, что тормозит каждый запрос. Каждый. Сраный. Запрос. Вот выборка из вью вариантов для комбобокса, всего 6 тыс строк, содержащих айди и короткое имя. Сколько выполняется? 5500 мс. Вот ещё выборка из другой таблички, тоже немного, но уже 10 секунд. Ну и так далее.
Дальше как в тумане. Мозг категорически отказывался верить увиденному. Были испробованы другой коннектор и Entity Framework. Картина та же. Были перепробованы все менеджеры баз данных: бобр, жаба, грип, печка и девелопер. Все тормозили совершенно одинаково.
В результате была написана небольшая тулза-бенчмарк. Для чистоты эксперимента она выбирала напрямую из таблицы, а не вью (чтобы не учитывалась нагрузка на CPU). Табличку искал пожирнее в плане байт на строку, чтобы снизить влияние количества строк. И чтобы поменьше столбцов. Нашёл небольшую табличку, где были айди, небольшие XML-ки и ещё парочка столбцов. Идеально.
Запустил тест, отлимитив на 8000 записей. В результате 359 секунд. Ещё раз. Триста. Пятьдесят. Девять. Секунд. Я, конечно, всё понимаю, но я привык к несколько другим скоростям. Да, XML. Да, восемь тысяч. Но это не over 9000! По моим представлениям о прекрасном, такой объём нельзя грузить в конструкторе окошка, потому что сходу получаешь тормоза гуя на пару секунд. На пару секунд, хех. Я правда это сказал?
При пересчёте пэйлоада таблицы получилась скорость в 21 килобайт в секунду. Чуть быстрее модема конца прошлого века. Пэйлоад я считал исходя из UTF-16, то есть два байта на символ, что не обязательно и было в реальности. Собственно, если это был UTF-8, то результат ещё хуже, потому что XML чуть менее, чем полностью, состоял из латинских символов.
Были проверены разные варианты: из офиса по вай-фай, из офиса по проводу, из дома по проводу, из дома коллеги по проводу, из другой страны по проводу, из другой страны по вайфаю. Даже через мобильный интернет пробовали. Потом всё то же самое с тестовым сервером. Были испробованы разные настройки VPN, разные MTU. Результаты примерно одинаковые. Примерно. Но была интересная корреляция. Чем дальше хост, тем толще партизаны тормоза. Это оказалось важно, и в дальнейшем именовалось "пингозависимостью фетча".
По результатам тестирования был создан подробный тикет технарям, с результатами замеров и с приложенным бенчмарк-тулом. Также приложил тесты скачивания файлов в разные периоды рабочего времени. Большой файл и пачка маленьких - это разные тесты. У файлов была определённо зависимость от времени суток, в рабочее время оно тормозило сильней. Возможно, это как-то связано.
Спойлер: через год тикет закроют по "Won't fix". Но я тогда этого не знал, я думал, что починят. Не может же сама база данных так тормозить, дело наверняка где-то в сети. Ещё один спойлер: может.
Осознание
Через два месяца мне прямо сказали, что скорость никто не починит, ребятам некогда этим заниматься, они по уши в рутине и тушении пожаров. И вот тут накрыло осознание, что при таких вводных быструю систему не построить. Да, можно распараллелить операции, можно что-то закешировать, что-то оптимизировать. Но у этих методов есть свой предел. Если нужные данные грузятся 10 секунд, то за две секунды ты их никак не загрузишь.
Тормозит сам протокол передачи данных. И с этим ничего не поделать. Будь ты самый помидористый синьёр, самый прокаченный оптимизатор, умеющий вслепую писать красно-чёрные деревья, ты сделаешь ровно ничего.
А можно ли перенести БД поближе к офису? Да, у меня сразу возник такой вопрос. Оказалось, нельзя. Помните такую юмористическую передачу "маски-шоу"? Хоть передача давно была закрыта, руководство почему-то её боялось и ставило причиной категорического отказа от переноса БД в офис.
Копаться в настройках корпоративной сети и, тем более, базы данных мне никто не позволит. Что логично. Да я и не особо хотел, что не менее логично. Яж программист, оно мне надо?
Ну вот и всё. Приплыли. Что будем делать?
Будем писать свой ORM на три звена.
Этот отличный план, надёжный, как швейцарские часы, был выработан далеко не сразу. Сначала была проверка гипотезы. Нет, сначала был консилиум и долгие совещания. На них мы решили гипотезу проверить.
Для проверки мы арендовали в том же облаке яндекса, в той же подсети тестовый VPS с убунтой на борту. С неё я запускал тестовый запрос к БД на те же тестовые 8000 строк. Получалось очень быстро - гораздо быстрее, чем через VPN. Затем был выбор протокола передачи с сервера на клиент. Я хотел бинарный протокол, чтобы как можно быстрее передавались большие объёмы. В итоге взял простой советский протобуф третьего поколения.
Это тоже было отдельное исследование, по которому меня заставили писать целую статью в конфлюенсе. Там были сравнения скоростей в разных направлениях, от клиента к серверу, от сервера к БД, потом всё вместе, потом отдельно задержки от подачи запроса к первой полученной строке, потом к передаче целиком. Все тесты в двух вариантах - синхронная передача и асинхронная.
Синхронная передача выигрывала в одиночных запросах и во времени загрузки целиком (чуть-чуть). Асинхронная разгромно выигрывала в задержке получения первой строки. Но была сложнее. В итоге оставили обе.
И да. Тесты показали скорость загрузки тестовых 8000 строк ровно в 3 секунды. От начала запроса из офиса до получения последней строки в том же офисе. Ровно в 120 раз быстрее, чем прямой запрос, что я и вывел в заголовок статьи.
Был ещё вопрос, который у большинства читателей вертится на языке.
Почему не веб?
Краткий ответ: потому что эксель.
Слишком много было взаимодействия с экселем. Много таблиц и много данных. Экспорт в эксель на каждом шагу и импорт хитрых табличек из него же. Вся компания, по сути, жила в экселе. Мне предстояло обрабатывать около 60к строк одновременно на нескольких машинах, их надо было загрузить из экселя и поддерживать актуальность данных в каждом клиенте. Это очень сложно делается для веба. Да, гугл написал спредшит, полностью вебовый и интерактивный. Но я не гугл. Я так не умею.
А ещё никто не умел писать веб. Я когда-то этим занимался, но это было давно и неправда. То, что я знал, на тот момент было дико устаревшим. Из фронтенда мы знали только винформс. Ну как "мы"... Я и ещё полтора программиста. Но это было заметно больше, чем знающих веб. Я ещё умею в WPF и хотел именно его, но остановились таки на винформс+девэкспресс, потому что если что, то хоть кто-то может это понять. Это и стало решающим фактором.
Забегая вперёд скажу, что никто с проектом мне не помогал, я писал его один. Но я до последнего надеялся, что остальные ребята будут со мной работать, ибо в одно лицо такой объём не вытащить. Зря надеялся. Ребята находились в состоянии перманентного аврала, разгребая несрастушечки в данных все полтора года, проведённых мной в этой компании.
Там действительно было что разгребать. Предыдущей командой был написан механизм синхронизации данных между 1С и ораклом. Оракл сам, своими хранимками подбирал выброшенные 1Ской джейсончики и распихивал по своим таблицам. Работало это крайне через ж... ненадёжно, почему-то какие-то джейсончики он пропускал, какие-то вносил дважды. Привереда какая.
И плюс ещё, незадолго до появления меня в компании там произошло стихийное бедствие. Стихийное бедствие было в виде апгрейда 1С до 8.3. Эска была затюнингованная вусмерть и после апгрейда тюнинг отвалился. Дальше был постоянный бег трусцой за данными. Сверки чинились медленнее, чем появлялись новые данные. Если на момент моего прихода отставание по сверкам было два месяца, то к моему уходу - полгода. Я могу что-то здесь путать, ибо с 1С я не работал никогда и как оно там происходит не знаю. Всё по памяти и с чужих слов.
Согласования.
Я просто написал "выбрали винформс". На самом деле не просто. Мы совещались, долго спорили, я писал кросс-демо, где было 4 варианта: винформс и WPF, с каждым вариантом был grid и spreadsheet. Словами я не смог убедить непосредственного руководителя, что спредшит не подходит ну совсем никак. Зачем писал вариант с WPF, когда всё упёрлось в скиллы разрабов? Да кто его знает.
Писал предполагаемую структуру, обязанности всех троих: БД, сервера и клиента. Ещё что-то писал. Ах, да, расписывал структуру механизма историзации. Мы только на этапе проектирования историзации застряли на месяц, хотя по факту эта историзация проекту как пятая нога собаке. Или полярному лису, если аналогия с Пелевиным не нравится.
Мы с руководителем спорили аж до хрипоты по каждой мелочи. Вадим, ну ты, блядь, серьёзно? Ты сам брал на работу профи с огромным стажем разработки. Зачем ты споришь с ним по каждому полю базы данных?
Позже я пришёл к другой тактике. Я уже знал, что любой даже самый мелкий вопрос вызовет непременное и непреклонное своё мнение со стороны Вадима. Я просто молча делал, как считал нужным, а спорили мы уже потом, по факту. И у меня был аргумент "ну я уже так сделал". Это работало гораздо эффективнее, чем остальные аргументы, потому что Вадим хочет премию.
Тем временем наступил март 2023. Работать мне оставалось девять месяцев. А мы только составили план дальнейшей работы.
План Б.
Новый план заключался в одной простой вещи: мы выкатываем менеджменту рабочую версию информационной системы 2.0 к новому году просто с отдельным функционалом для закупки. Вадим получает премию в размере месячного оклада, я - двух. Все счастливы и смеются. Естественно, никто ничего не получил, но это отдельная история.
А дальше менеджмент видит, насколько она удобная, быстрая и экономит время сотрудникам. И мы потихоньку перекидываем функционал старой системы в новую. В этом был наш план, да. Просто сделать хороший продукт для внутреннего потребления.
Я накидал список задач, которые нужно решить для достижения этой цели, Вадим выделил чекпоинты, превратил их в диаграмму Ганнта и понёс менеджменту. Менеджменту такое блюдо понравилось и оно дало добро. Приближались майские.
Не сказать, что я ничего за это время не сделал. Я делал. Я потихоньку пилил ORM.
Зачем ORM?
Мне хотелось привести данные в порядок. Мне хотелось избавиться от бесконечных портянок в клиентском коде, которые занимаются тривиальными вещами, вроде получения данных и их распихиванием по полям объектов. Мне хотелось по "Find usages" находить все места, где используется это поле таблицы.
Но готовых ORM под трёхзвенку просто не существует. Существующие работают напрямую с базой данных, а мне нужна смена протокола.
К марту меня настигло осознание, что я буду это делать один. Поэтому срезаем углы где только можно. Мы уже поняли, что без трёхзвенки не будет смены протокола, а без смены протокола - вменяемой скорости. Для ускорения разработки нужно, чтобы запросы можно было отправлять прямо из клиента. То есть как бы двухзвенка, но "с бенефитами". Просто потому, что истинную трёхзвенку очень дорого делать. Нужно под каждый чих составлять симметричные поинты на клиенте и сервере, что мне не очень на руку. Но очень хочется местами ограничивать поля для клиента, например ему совсем не надо знать хэши паролей других пользователей.
Логика предполагается преимущественно на клиенте, но с постепенным переползанием в сторону сервера. Сейчас и так требуется некая серверная логика для координации работы над одним документом и принятии решения об переводе документа из статуса в статус, но планируется в будущем расширить функционал сервера для обработки тяжёлых операций, плюс создать полный контроль над правами пользователей.
Мы решили пойти по пути model-first. Не сказать бы, что у нас был широкий выбор при таких вводных данных. Вадим хотел обязательно картинки. Ну вот эти ER-диаграммы со стрелочками от таблицы к таблице. Оно без model-first не делается.
Следующий шаг - выбор инструмента, на который можно скинуть всю работу с моделью данных. Требования у меня были простые:
оно умеет работать с ораклом
оно умеет сохранять дополнительную информацию о полях и таблицах
его формат можно прочитать, чтобы потом нагенерить код по модели
при открытии таблицы видно комментарии к столбцам и их легко менять
Требования Вадима:
оно умеет рисовать ER-диаграммы
ОБЯЗАТЕЛЬНО чтобы стрелочки шли от поля с FK к ключу. Никак иначе. Просто от таблицы к таблице не пойдёт.
Я нашёл такой инструмент. Он платный. Но удовлетворяет всем требованиям и мы его купили, потому что в бесплатном варианте он не работает с моделью. Его я рекламировать не буду. Позже я создал свой собственный database management tool специально для таких случаев. Его я рекламировать буду, но не сейчас. Потом.
Мне очень нравится подход Entity Framework. Вот эта лаконичность его запросов, вот эта вся гибкость и join-on-demand. Короче, когда тебе нужен джоин, пишешь .Include(x => x.Field), когда не нужен - не пишешь. Плюсом ещё знакомый всем синтаксис. Если разраб шарпист, то почти наверняка знает, как запрашивать данные через EF.
Итого, требования для ORM:
Include, как в EF
поддержка Where, в том числе в джойненных таблицах
поддержка OrderBy и OrderByDesc
поддержка Take/Skip
поддержка OR, AND, NOT в условиях WHERE
маппинг в объекты
Начинаем.
Resurrection.
Для написания этой статьи я накатил оракл линукс 8 на виртуалку. А на оракл линукс поставил Oracle 21c XE. Не делайте так. Я потратил кучу нервов и времени, чтобы перезапустить его после перезагрузки машины. Возьмите лучше обычный докер-файл. И жрёт эта зараза 7 гигабайт памяти абсолютно пустая.
Но мне было интересно воссоздать максимально приближенные условия к тем, что были. Заодно понять, это действительно в протоколе проблема или у меня лыжи не едут.
После установки я накатил тестовые данные из одного репозитория. У меня так и не получилось вкатить большие таблички из csv файлов и мне даже не помогла гопота. Очень уж всё замудрено. Тем не менее, мелкие таблички всё-таки вносились и я взял схему sales_history:

Вадим, если ты это читаешь, смотри: тут стрелочки идут от поля внешнего ключа к полю первичного ключа. И никак иначе! Видишь? Всё для тебя.
Я отвлёкся. Берём табличку Products из нашей тестовой базы и запускаем фетч:

Здесь 288 рядов, которые зафетчились за 9 мс. Ничего удивительного, оракл запущен на локальной машине. Теперь надо проверить, будет ли эта версия тормозить. Нужен пинг как минимум 70мс, как было на работе. Как?
Совершенно случайно у меня есть VPS где-то в Манчестере, а на нём совершенно случайно есть WireGuard. Допустим. Можно было бы вкатить туда оракл, но я больше не хочу идти на такие жертвы. Да и памяти там не хватит. Вот что я придумал. Я подключаю рабочую машину к вайргарду, оставляя там подсеть 10.10.0.0/16. Подключаю виртуалку с оракл линуксом к той же подсети и вуаля! Я теперь могу обращаться к своей виртуалке по адресу 10.10.1.34 через Манчестер! Пинг до неё аж 230! Хорошо!
Давайте попробуем зафетчить ту же самую табличку, но уже через впн.

4,4 секунды! О как! Похоже, я перестарался с замедлением канала. Что стало видно сейчас - так это то, что данные поступают рывками. Это не тормоза OrmFactory, она в норме показывает плавный скролл на 60fps. А тут видно, что прямо слайд-шоу, как крайзис на встройке. Что любопытно, даже на глаз видно, что интервал "подёргиваний" примерно равен четверти секунды, то есть те самые 230мс пинга за небольшим плюсом на передачу очередного пакета.
Чтож. У нас получилось скукожить пропускную способность примерно в 400-500 раз. Теперь нужно раскукожить её обратно, не выходя за пределы нашего тормозного канала.
В 120 раз не ускорится, так как нашей табличке нужно будет побывать в Англии. За 40 мс не получится туда-обратно, законы физики запрещают. Должно получиться 240-250. Впрочем, увеличив табличку мы можем рисовать практически любые цифры прироста скорости.
Я сейчас пишу статью в 2025 по мотивам 2023 года. И буду писать, как будто OrmFactory уже существует и с его помощью я делаю ORM для этой компании. Прошу отнестись с пониманием к таким временным парадоксам.
Итак, это было вступление. А сейчас начинаем самое интересное - написание кода.
Технологии
Раз мы используем protobuf, то сделаем общение между клиентом и сервером по gRPC. Идея такая: клиент парсит дерево выражения запроса, сериализирует его в иерархичный XML, отправляет на сервер через gRPC, сервер составляет SQL запрос, получает данные и отправляет асинхронный поток в ответ клиенту опять же через gRPC. Вроде просто.
Сервер сделаем на .NET 8, клиент тоже будет .NET 8, а в качестве интерфейса будет Avalonia UI. Да, в оригинале клиент был на .NET Framework 4.7.2 и winforms, но нам бы лучше что-то кроссплатформенное.
Сервер в последствии выложим прямо на Oracle Linux и запустим сервисом. Докер не будем ставить, оно того не стоит. Можем собрать билд в виде Self-Contained, чтобы не ставить дотнет рантайм, или вообще через Native AOT. Рефлекшена на сервере всё равно не будет. Но это не точно.
Обратный поток - передача сущностей из клиента в БД будет умышленно отсутствовать в сгенерированном коде. Каждый инсерт и апдейт будет написан ручками на сервере и клиенте по мере необходимости. ORM будет поддерживать изменение объектов на сервере, но не на клиенте. Почему? Потому что изменение объектов - это бизнес-логика. Она должна быть под управлением сервера, а не клиента.
Создаём проекты для клиента и сервера, назвав их неожиданно Client и Server. Также я создам папку в корне проекта для модели. Назову Schema. В OrmFactory создаю новый проект, добавляю туда коннекшен к БД, импортирую нужную мне схему. Сохраняю как model в папке Schema. Начальная структура готова.
Генератор
Начнём с генерации сущностей. А вокруг сущностей уже будем строить механизмы их передачи.
Можно было бы использовать C# для написания генератора и использовать его как command line generator, но в этом есть неудобство. Нужно будет при каждом изменении пересобирать его. А изменять будем часто и хаотично. Конечно, для отладки есть ещё XML generator, который выдаст нам xml-модель (она отличается от структуры файла проекта) и позволит отлаживать наш генератор на статичном файле, но всё равно удобнее будет править и запускать скрипт, не вылезая из менеджера базы данных.

И так, заводим себе домашнюю змею с официального сайта. Будем питонить на программировании. В качестве отправной точки возьмём генератор для EF. Он будет опцией при добавлении нового генератора в проект. Сохраняем рядом с моделью. В опциях генератора сразу включаем конвертацию имён в CamelCase, оракловский стиль чуждо смотрится в коде шарпа. Теперь можно его править и пробовать запускать.
Так как образец генератора сделан под типы MySql, первым делом перелопачиваем конвертацию типов. У оракла нет нативного инта и все айдишники запиханы в NUMBER, в том числе в нашем примере. В одной таблице (Products) айдишник ограничен NUMBER(6,0), но в остальных это просто NUMBER. Ну да пусть. Будем всё намберное, что заканчивается на ID рассматривать как Int32. В остальных случаях рассмотрим precision и scale, после чего запихнём туда, куда влезает. Для этого придётся имя колонки пробросить в наш метод:
def resolve_type(db_type: str, column_name: str) -> str:
db_type = db_type.lower().strip()
if db_type.startswith("number"):
if column_name.endswith("ID"):
return "int"
precision = None
scale = None
if "(" in db_type and ")" in db_type:
args = db_type[db_type.find("(") + 1 : db_type.find(")")]
parts = [p.strip() for p in args.split(",")]
if len(parts) >= 1 and parts[0].isdigit():
precision = int(parts[0])
if len(parts) == 2 and parts[1].isdigit():
scale = int(parts[1])
elif len(parts) == 1:
scale = 0 # default if only precision is given
if scale is not None and scale > 0:
return "decimal"
if precision is not None:
if precision <= 9:
return "int"
elif precision <= 18:
return "long"
else:
return "decimal"
return "decimal"
if db_type.startswith("float") or db_type.startswith("binary_float"):
return "float"
if db_type.startswith("binary_double"):
return "double"
if any(db_type.startswith(t) for t in ["varchar2", "nvarchar2", "char", "nchar", "clob", "nclob"]):
return "string"
if db_type.startswith("timestamp"):
return "DateTime"
if db_type.startswith("date"):
return "DateOnly"
if any(db_type.startswith(t) for t in ["blob", "raw", "long raw"]):
return "byte[]"
raise ValueError(f"Unknown type: {db_type}")
В схеме есть странные таблички вроде DR$SUP_TEXT_IDX$C
, и я не знаю, что это такое, поэтому просто отключил их в partial settings генератора. Иначе мне придётся поддерживать тип данных ROWID, который в этих табличках используется.
Убираю аннотацию EF, немного правлю генерацию сущностей, добавляю генерацию класса-репозитория. Я решил делать без контекста, просто статические репозитории. Кому надо - возьмите свой любимый DI или сделайте с объявлением контекста, как в EF. Можно даже как и там, сделать IDisposable, правда внутри диспозить нечего.
В итоге получается вот такая простая модель:
using System;
using Grpc.Core;
using Microsoft.EntityFrameworkCore;
using Client;
namespace Client.Data;
public static class Tables
{
/// <summary>
///small dimension table
/// </summary>
public static ChannelTable Channels = new();
public static CostTable Costs = new();
...
}
/// <summary>
///small dimension table
/// </summary>
public partial class Channel
{
/// <summary>
///primary key column
/// </summary>
public int ChannelId { get; set; }
/// <summary>
///e.g. telesales, internet, catalog
/// </summary>
public string ChannelDesc { get; set; }
/// <summary>
///e.g. direct, indirect
/// </summary>
public string ChannelClass { get; set; }
public int ChannelClassId { get; set; }
public string ChannelTotal { get; set; }
public int ChannelTotalId { get; set; }
}
public partial class ChannelTable
{
}
public partial class Cost
{
public int ProdId { get; set; }
public DateOnly TimeId { get; set; }
public int PromoId { get; set; }
public int ChannelId { get; set; }
public decimal UnitCost { get; set; }
public decimal UnitPrice { get; set; }
public Channel Channel { get; set; }
public Product Prod { get; set; }
public Promotion Promo { get; set; }
public Time Time { get; set; }
}
...
Комментарии тупые и не везде. Это вообще-то была важная часть нашей парадигмы, поэтому даже были в требованиях к менеджеру БД. Комментируются поля в БД и затем попадают через генератор в код и видны в интеллисенс, что добавляет консистентности к схеме данных. Ладно, когда-нибудь перепишу на нормальные (нет).
Теперь нужно сгенерировать proto-файл. Очень удачно, что у нас и клиент и сервер на одной версии .NET, мы можем сделать общий контракт. Создаём в корне папку Shared, в ней проект GrpcContracts. В него подключаем библиотеки Grpc и Protobuf, создаём файл Generated.proto. У меня все генерированные файлы называются Generated. Это я так свою оригинальность выражаю. Зато вопросов не должно возникнуть, можно ли это править. Не должно же, правда?
Все протофайлы нужно специальным образом подключать к проекту в файле .csproj:
<ItemGroup>
<Protobuf Include="Generated.proto" GrpcServices="Both" />
</ItemGroup>
GrpcServices="Both"
значит, что будут сгенерированы и серверные, и клиентские интерфейсы.
Генерируем протофайлы по модели в том же генераторе. Я в генераторе конвертирую базаданновый тип в шарповый и дальше из него делаю протобуфный. Так проще оказалось.
syntax = "proto3";
option csharp_namespace = "GrpcContracts";
import "Common.proto";
import "google/protobuf/timestamp.proto";
package orm;
service Orm {
rpc SelectChannel (SelectRequest) returns (ChannelReply);
rpc SelectChannelStream (SelectRequest) returns (stream ChannelProto);
rpc SelectCost (SelectRequest) returns (CostReply);
rpc SelectCostStream (SelectRequest) returns (stream CostProto);
}
message SelectChannelReply {
repeated ChannelProto Objects = 1;
int32 ErrorCode = 2;
string ErrorMessge = 3;
}
message ChannelProto {
int32 ChannelId = 1;
string ChannelDesc = 2;
string ChannelClass = 3;
int32 ChannelClassId = 4;
string ChannelTotal = 5;
int32 ChannelTotalId = 6;
}
message SelectCostReply {
repeated CostProto Objects = 1;
int32 ErrorCode = 2;
string ErrorMessge = 3;
}
message CostProto {
int32 ProdId = 1;
DateProto TimeId = 2;
int32 PromoId = 3;
int32 ChannelId = 4;
ProtoDecimal UnitCost = 5;
ProtoDecimal UnitPrice = 6;
optional ChannelProto Channel = 7;
optional ProductProto Prod = 8;
optional PromotionProto Promo = 9;
optional TimeProto Time = 10;
}
То есть да, мы генерируем proto файлы, по которым кодогенератор генерирует файлы .generated.cs для проекта, по которым компилятор собирает IL, который затем компилируется в JIT.
Для поддержки decimal и DateOnly я сделал свои структуры и вынес их в Common.proto:
syntax = "proto3";
message DecimalProto {
sint32 v1 = 1;
sint32 v2 = 2;
sint32 v3 = 3;
sint32 v4 = 4;
}
message DateProto {
uint32 year = 1;
uint32 month = 2;
uint32 day = 3;
}
Сюда же буду добавлять простенькие структуры, не имеющей чёткой привязки к конкретному процессу.
Теперь нам надо сделать XML запрос на выборку сущностей от клиента к серверу, используя LINQ. Соответственно, нужно унаследовать класс-репозиторий от базового класса, а в нём реализовать поддержку IQueryable
.
Но начнём мы с простого: с формата XML файла. Так как это общий протокол обмена между клиентом и сервером, я запихнул работу с этим форматом в GrpcContracts. Не буду ничего выдумывать, возьму стандартный сериализатор дотнета и расставлю аннотации, чтобы оно там само из экземпляра класса сделало XML. Сам класс выглядит простенько:
public class RequestExpression
{
[XmlAttribute, DefaultValue(OrderBy.Ascending)]
public OrderBy OrderBy = OrderBy.Ascending;
[XmlAttribute, DefaultValue("")]
public string OrderByField = "";
public Limit Limit = new Limit();
public bool ShouldSerializeLimit() => Limit.Skip > 0 || Limit.Take > 0;
[XmlElement]
public List<string> Include = new List<string>();
public bool ShouldSerializeInclude() => Include.Any();
[DefaultValue(null)]
public WhereCondition Condition;
}
public enum OrderBy
{
Ascending,
Descending
}
public enum WhereOperator
{
//unary
Parameter,
Value,
Not,
//binary
And,
Or,
Equal,
GreaterThan,
GreaterThanOrEqual,
LessThan,
LessThanOrEqual,
Contains,
}
public class Limit
{
[XmlAttribute, DefaultValue(0)]
public int Skip;
[XmlAttribute, DefaultValue(0)]
public int Take;
}
public class WhereCondition
{
public WhereCondition() { }
public WhereCondition(WhereCondition left, WhereOperator op, WhereCondition right)
{
Left = left;
Operator = op;
Right = right;
}
[XmlAttribute]
public WhereOperator Operator;
[XmlAttribute, DefaultValue("")]
public string Value = "";
[DefaultValue(null)]
public WhereCondition Left;
[DefaultValue(null)]
public WhereCondition Right;
}
Здесь надо обратить внимание на рекурсивный WhereCondition. Туда будет складываться дерево условий, которые мы будем писать в логике клиента. Так как есть поддержка унарных операций, то или левый или правый операнд могут быть null.
Конечно, можно было бы и под эту структуру сделать proto-файл, но мне показалось, что через XML проще. Да и смысла экономить байтики на структуре запроса совершенно не вижу.
Теперь базовый репозиторий.
public abstract class TableBase<TEntity> : IQueryable<TEntity>
{
public Expression Expression => Expression.Constant(this);
public Type ElementType => typeof(TEntity);
public IQueryProvider Provider => new LinqProvider<TEntity>(this);
public IEnumerator<TEntity> GetEnumerator()
{
return new LinqProvider<TEntity>(this).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
Нам надо будет его вызывать цепочкой модификаторов вроде:
Products.Where(...).OrderBy().Take().Skip().Include(...)
и получать на выходе IEnumerable
.
На самом деле никакого IEnumerable не будет. На каждом этапе будет новый IQueryable, а когда будет вызов цепочки, вот тогда и произойдёт отправка запроса и конвертация ответа в IEnumerable. Асинхронкой займёмся потом.
Проблема в том, что наш базовый тип будет только в самом начале цепочки, после первого Where это превратится в IQueryable
и никакого Include там в методах не будет. Я посмотрел в коде EF Core, как сделано там. Там тупо повесили extension method на IQueryable. Ну и мы не будем париться.
public static IQueryable<T> Include<T>(this IQueryable<T> queryable, Expression<Func<T, object>> expr)
{
var member = expr.Body as MemberExpression;
var name = member.Member.Name;
var provider = queryable as LinqProvider<T>;
if (provider == null)
{
var table = queryable as TableBase<T>;
if (table == null) throw new Exception("Can't include: repo must be LinqProvider or TableBase");
provider = new LinqProvider<T>(table);
}
provider = provider.Clone();
provider.IncludeFields.Add(name);
return provider;
}
Теперь мы можем ставить Include в любом месте нашей цепочки. Делаем тестовый запрос:
var list = Tables.Costs
.Include(c => c.Promo)
.Where(c => c.ProdId == 0 && c.UnitCost > 10.0m)
.OrderBy(c => c.ChannelId)
.Include(c => c.Prod)
.Take(10)
.ToList();
Для того, чтобы получить заветный XML, нам осталось добавить ExpressionSerializer, который вызывается в провайдере, а тот уже должен распарсить дерево выражения и превратить в наши сериализуемые объекты.
public class ExpressionSerializer
{
private static XmlSerializer serializer;
private readonly List<Expression> expressions;
private readonly List<string> include;
public ExpressionSerializer(List<Expression> expressions, List<string> include)
{
this.expressions = expressions;
this.include = include;
}
public static RequestExpression Deserialize(string request)
{
if (serializer == null) serializer = new XmlSerializer(typeof(RequestExpression));
using var reader = new StringReader(request);
return serializer.Deserialize(reader) as RequestExpression;
}
public bool IsDefaultNull { get; internal set; }
public string GetXml()
{
if (serializer == null) serializer = new XmlSerializer(typeof(RequestExpression));
var exp = GetRequestExpression();
var settings = new XmlWriterSettings();
settings.IndentChars = "\t";
settings.Indent = true;
settings.NewLineChars = "\n";
settings.Encoding = Encoding.UTF8;
using (var writer = new StringWriter())
using (XmlWriter xw = XmlWriter.Create(writer, settings))
{
serializer.Serialize(xw, exp);
return writer.ToString();
}
}
public RequestExpression GetRequestExpression()
{
var exp = new RequestExpression();
foreach (var e in expressions)
{
AddExpression(exp, e);
}
exp.Include.AddRange(include);
return exp;
}
private void AddExpression(RequestExpression re, Expression e)
{
var method = e as MethodCallExpression;
var name = method.Method.Name;
if (name == "Where")
{
AddWhereCondition(re, method);
return;
}
if (name == "OrderBy")
{
AddOrderBy(re, method);
re.OrderBy = OrderBy.Ascending;
return;
}
if (name == "OrderByDescending")
{
AddOrderBy(re, method);
re.OrderBy = OrderBy.Descending;
return;
}
if (name == "Skip")
{
re.Limit.Skip = GetIntArgument(method);
return;
}
if (name == "Take")
{
re.Limit.Take = GetIntArgument(method);
return;
}
throw new NotImplementedException("method " + name);
}
private int GetIntArgument(MethodCallExpression method)
{
var args = method.Arguments;
if (!args.Any()) throw new Exception("no args");
var arg = method.Arguments.Last();
if (arg is ConstantExpression ce)
{
return (int)ce.Value;
}
if (arg is MemberExpression ex)
{
var objectMember = Expression.Convert(ex, typeof(object));
var getterLambda = Expression.Lambda<Func<object>>(objectMember);
var getter = getterLambda.Compile();
var value = getter();
return (int)value;
}
throw new Exception("must be member expression");
}
private void AddOrderBy(RequestExpression re, MethodCallExpression method)
{
var args = method.Arguments;
if (!args.Any()) return;
var arg = args.Last();
if (!(arg is UnaryExpression lambda)) return;
if (arg.NodeType != ExpressionType.Quote) return;
if (lambda.Operand is not LambdaExpression predicate) return;
if (predicate.Body is not MemberExpression ex) return;
if (ex.Expression.NodeType != ExpressionType.Parameter) return;
re.OrderByField = ex.Member.Name;
}
private void AddWhereCondition(RequestExpression re, MethodCallExpression method)
{
var args = method.Arguments;
if (!args.Any()) return;
var arg = args.Last();
if (arg is not UnaryExpression lambda) return;
if (arg.NodeType is not ExpressionType.Quote) return;
if (lambda.Operand is not LambdaExpression lambdaExpression) return;
var wc = new LambdaExpressionParser(lambdaExpression).GetWhereCondition();
if (re.Condition == null)
{
re.Condition = wc;
return;
}
re.Condition = new WhereCondition(re.Condition, WhereOperator.And, wc);
}
}
Достаточно простая и расширяемая конструкция. Можно добавлять поддержку шарпового Contains, например, превратив его в оператор IN() на той стороне. Или какого-нибудь унарника вроде DateDime.Date. Но пока что нам не надо. Такими вещами можно заниматься бесконечно и всё равно не поддержать весь синтаксис.
Теперь парсер дерева выражений:
public class LambdaExpressionParser
{
private readonly Expression expression;
private string parameterName;
public LambdaExpressionParser(LambdaExpression lambda)
{
expression = lambda.Body;
parameterName = lambda.Parameters[0].Name;
}
public WhereCondition GetWhereCondition()
{
return GetWhereCondition(expression);
}
private WhereCondition GetWhereCondition(Expression expr)
{
if (expr is BinaryExpression bin)
{
var left = GetWhereCondition(bin.Left);
var right = GetWhereCondition(bin.Right);
if (bin.NodeType == ExpressionType.AndAlso) return new WhereCondition(left, WhereOperator.And, right);
if (bin.NodeType == ExpressionType.OrElse) return new WhereCondition(left, WhereOperator.Or, right);
if (bin.NodeType == ExpressionType.Equal) return new WhereCondition(left, WhereOperator.Equal, right);
if (bin.NodeType == ExpressionType.GreaterThan)
return new WhereCondition(left, WhereOperator.GreaterThan, right);
if (bin.NodeType == ExpressionType.GreaterThanOrEqual)
return new WhereCondition(left, WhereOperator.GreaterThanOrEqual, right);
if (bin.NodeType == ExpressionType.LessThan)
return new WhereCondition(left, WhereOperator.LessThan, right);
if (bin.NodeType == ExpressionType.LessThanOrEqual)
return new WhereCondition(left, WhereOperator.LessThanOrEqual, right);
throw new NotImplementedException(bin.NodeType.ToString());
}
if (expr is UnaryExpression un)
{
if (un.NodeType == ExpressionType.Not)
{
return new WhereCondition(GetWhereCondition(un.Operand), WhereOperator.Not, null);
}
if (un.NodeType == ExpressionType.Convert)
return GetWhereCondition(un.Operand);
throw new NotImplementedException(un.NodeType.ToString());
}
if (expr is MemberExpression ex)
{
var memberNames = GetMemberChain(ex);
if (memberNames.First() == parameterName)
{
memberNames.RemoveAt(0);
var par = new WhereCondition()
{
Operator = WhereOperator.Parameter,
Value = string.Join(".", memberNames)
};
var propInfo = ex.Member as PropertyInfo;
if (propInfo?.PropertyType == typeof(bool))
{
par = new WhereCondition
{
Left = par,
Right = new WhereCondition
{
Operator = WhereOperator.Value,
Value = "1"
},
Operator = WhereOperator.Equal
};
}
return par;
}
//https://stackoverflow.com/questions/2616638/access-the-value-of-a-member-expression
var objectMember = Expression.Convert(ex, typeof(object));
var getterLambda = Expression.Lambda<Func<object>>(objectMember);
var getter = getterLambda.Compile();
var value = getter();
return new WhereCondition
{
Operator = WhereOperator.Value,
Value = GetValue(value)
};
}
if (expr is ConstantExpression c)
{
return new WhereCondition
{
Operator = WhereOperator.Value,
Value = GetValue(c.Value)
};
}
if (expr is MethodCallExpression call)
{
if (call.Method.Name == "Contains")
{
WhereCondition left;
WhereCondition right;
if (call.Arguments.Count == 1)
{
var exArg = call.Object;
left = GetWhereCondition(exArg);
right = GetWhereCondition(call.Arguments[0]);
return new WhereCondition(left, WhereOperator.Contains, right);
}
if (call.Arguments.Count == 2)
{
left = GetWhereCondition(call.Arguments[0]);
right = GetWhereCondition(call.Arguments[1]);
return new WhereCondition(left, WhereOperator.Contains, right);
}
}
throw new NotImplementedException(expr.ToString());
}
throw new NotImplementedException(expr.ToString());
}
private List<string> GetMemberChain(MemberExpression ex)
{
var list = new List<string>();
if (ex.Expression is MemberExpression me) list = GetMemberChain(me);
if (ex.Expression is ParameterExpression pe)
{
list.Add(pe.Name);
}
list.Add(ex.Member.Name);
return list;
}
private string GetValue(object obj)
{
if (obj is string s) return "'" + s + "'";
if (obj is int i) return i.ToString();
if (obj is long l) return l.ToString();
if (obj is Enum) return ((int)obj).ToString();
if (obj is DateTime dt) return $"'{dt:G}'";
if (obj is IEnumerable<int> en) return string.Join(", ", en);
if (obj is decimal d) return d.ToString();
if (obj is null) return "null";
throw new NotImplementedException("Unsupported argument type " + obj.GetType());
}
}
Теперь у нас всё готово и мы перехватываем выполнение нашего тестогово запроса в LinqProvider, чтобы посмотреть, какая XML у нас получилась:
<?xml version="1.0" encoding="utf-16"?>
<RequestExpression xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" OrderByField="ChannelId">
<Limit Take="10" />
<Include>Promo</Include>
<Include>Prod</Include>
<Condition Operator="And">
<Left Operator="Equal">
<Left Operator="Parameter" Value="ProdId" />
<Right Operator="Value" Value="0" />
</Left>
<Right Operator="GreaterThan">
<Left Operator="Parameter" Value="UnitCost" />
<Right Operator="Value" Value="10,0" />
</Right>
</Condition>
</RequestExpression>
Вроде всё на месте. Переходим к серверу. Там предстоит сделать сущности, репозитории и обеспечить выполнение запроса.
План такой: мы принимаем этот XML, возвращаем ему обличие RequestExpression и трансформируем его в текстовый sql запрос. Потом учимся читать объекты из датаридера и только потом делаем linq на стороне сервера.
Делаем класс SqlRequest, который ни от кого пока не зависит, кроме нашего RequestExpression. Там будет только два интересных куска. Это рекурсивная обработка иерархии Where:
public string ParseWhere(WhereCondition condition)
{
if (condition == null) return "";
if (condition.Operator == WhereOperator.Value) return condition.Value;
if (condition.Operator == WhereOperator.Not) return "NOT (" + ParseWhere(condition.Left) + ")";
if (condition.Operator == WhereOperator.Or) return "(" + ParseWhere(condition.Left) + " OR " + ParseWhere(condition.Right) + ")";
if (condition.Operator == WhereOperator.And) return ParseWhere(condition.Left) + " AND " + ParseWhere(condition.Right);
if (condition.Operator == WhereOperator.Equal) return ParseWhere(condition.Left) + " = " + ParseWhere(condition.Right);
if (condition.Operator == WhereOperator.GreaterThan) return ParseWhere(condition.Left) + " > " + ParseWhere(condition.Right);
if (condition.Operator == WhereOperator.GreaterThanOrEqual) return ParseWhere(condition.Left) + " => " + ParseWhere(condition.Right);
if (condition.Operator == WhereOperator.LessThan) return ParseWhere(condition.Left) + " < " + ParseWhere(condition.Right);
if (condition.Operator == WhereOperator.LessThanOrEqual) return ParseWhere(condition.Left) + " =< " + ParseWhere(condition.Right);
if (condition.Operator == WhereOperator.Parameter) return condition.Value;
if (condition.Operator == WhereOperator.Contains)
{
return ParseWhere(condition.Right) + " IN(" + ParseWhere(condition.Left) + ")";
}
throw new NotImplementedException(condition.Operator.ToString());
}
И сборка запроса целиком:
public string GetTextRequest()
{
var requestParts = new List<string>();
requestParts.Add("SELECT " + GetFieldAliases());
requestParts.Add("FROM " + table.SchemaDbName + "." + table.TableDbName + " " + TableAsName);
if (WhereSql != "") requestParts.Add("WHERE " + WhereSql);
if (OrderBySql != "") requestParts.Add("ORDER BY " + OrderBySql);
if (Skip != 0) requestParts.Add("OFFSET " + Skip + " ROWS");
if (Take != 0) requestParts.Add("FETCH NEXT " + Take + " ROWS ONLY");
var request = string.Join("\n", requestParts);
Console.WriteLine(request);
return request;
}
Ничего сложного тут нет. Я сюда добавил зачатки трансляции .Contains(), возможно потом поддержим на клиенте. Наверное. Но это не точно.
Но прежде, чем мы получим текстовый запрос в СУБД, нам придётся сгенерировать сущности и репозитории. Потому что нам нужен будет список полей таблицы, которые будут выбираться из базы. Я не стану выбирать по *
, потому что двойной джоин к одной и той же таблице гарантировано даст нам коллизию имён, а значит нам сразу надо сделать альяс для каждого поля. У нас должна быть гарантия наоборот - что никаких коллизий не будет.
public abstract class TableBase<T> : ISqlTable, ITable<T>
{
public abstract string TableDbName { get; }
public abstract string SchemaDbName { get; }
public abstract Dictionary<string, string> PropertiesToFields { get; }
public abstract string[] PrimaryKeyProperties { get; }
protected SqlRequest CreateRequest() => new SqlRequest(this);
public IEnumerable<T> GetEntities()
{
var sqlRequest = CreateRequest();
return GetEntities(sqlRequest);
}
public IEnumerable<T> GetEntities(RequestExpression expression)
{
var request = GetRequest(expression);
return GetEntities(request);
}
internal IEnumerable<T> GetEntities(string xmlRequest)
{
var expression = ExpressionSerializer.Deserialize(xmlRequest);
var request = GetRequest(expression);
return GetEntities(request);
}
public abstract IEnumerable<T> GetEntities(SqlRequest req);
private SqlRequest GetRequest(RequestExpression expression)
{
var sqlRequest = CreateRequest();
sqlRequest.SetWhere(expression.Condition);
sqlRequest.SetLimit(expression.Limit);
sqlRequest.SetOrderBy(expression.OrderByField, expression.OrderBy);
return sqlRequest;
}
}
public abstract class EntityBase
{
public abstract void LoadFromReader(FieldReader fr, string tableName);
}
Подключаем Oracle.ManagedDataAccess.Core и создаём FieldReader. Эта такая портянка, которая имеет перегрузку Read под каждый тип поля. Нужна, чтобы мы могли в коде загрузки сущности писать просто fr.Read(ref field, "FIELD").
public class FieldReader
{
private readonly OracleDataReader dr;
public FieldReader(OracleDataReader dr)
{
this.dr = dr;
}
public bool Read()
{
return dr.Read();
}
public void Read(ref int o, string fieldName)
{
o = Convert.ToInt32(dr[fieldName]);
}
}
Дальше я добавляю в сущности клиента конвертацию в прото-классы и из прото-классов. Для конвертации прото в сущность я использовал конструктор класса сущности, а обратно - метод GetProto(). Надо ещё заметить, что нужно написать конверторы для некоторых типов, не имеющих прямого соответствия в protobuf. На данный момент это decimal и DateOnly.
На этот момент тот я, который из 25-го года, уже дико устал от написания этой системы. Но осталось немного. Нужно сгенерировать сущности на сервере, там же репозитории и как-то заставить это всё заработать вместе. Эндпоинт отправки данных я пока напишу ручками для одной таблицы.
И так, я всё это собрал. Делаем простой грид, простую кнопку и простую вьюмодель:
public class MasterViewModel: NotifyPropertyChangedBase
{
private string fetchedTime = "0";
public string FetchedTime
{
get => fetchedTime;
set => SetField(ref fetchedTime, value);
}
public ObservableCollection<Product> Products { get; set; }
public MasterViewModel()
{
Products = new ObservableCollection<Product>();
}
public void LoadProducts()
{
var sw = Stopwatch.StartNew();
foreach (var product in Tables.Products)
{
var elapsed = sw.ElapsedMilliseconds + "ms";
Dispatcher.UIThread.Post(() =>
{
FetchedTime = elapsed;
Products.Add(product);
});
}
sw.Stop();
}
}
По кнопке запускаем LoadProducts(). Запускаем сервер локально и нажимаем кнопку в клиенте:

Не знаю, что я там намудрил, я хотел, чтобы также рывками грузилось. Ну да ладно. Паблишим сервер под линукс, загоняем через ftp, далее chmod +x, далее, далее, пропустить. Запускаем сервер.
Переключаем клиента на канал с VPN. Ииииии

1,2 секунды. Нда. Но что есть, то есть. Правда второй запуск уже давал примерно 700 мс, что чуть лучше, но всё равно не дотягивает до 250.
Тем не менее, номинально задача выполнена. Работает быстрее. Всего лишь в 4 раза, но опять же, увеличивая объём данных можно добиться любой кратности прироста. Почти.
Общая Шина
Вадим грезил Общей Шиной. И терраформом. Я не очень хорошо понимал, что такое терраформ, и поэтому не очень сильно язвил по этому поводу.
Но что такое Общая Шина, я представлял прекрасно. Эта штука, по задумке Вадима, должна была полностью решить все проблемы с передачей данных между системами. Это такой message broker, только круче и универсальнее. Прямо как в том древнем лонгриде от Ашманова. А по моему опыту настолько абстрактные проекты ничего не решают, а только добавляют проблем. И чаще всего не воплощаются в жизнь. А если и воплощаются и даже как-то работают, то исключительно за счёт эффекта "каши из топора".
Впрочем, Вадим меня не напрягал разговорами об "Общей Шине". Всё равно он не рисковал давать мне её разработку, потому что хотел премию. Только рассказывал, что мне всё нужно будет в будущем пересылать через Общую Шину и надо к этому уже сейчас готовиться. Знать бы ещё, как к ней обращаться.
Он искал отдельного человека для её реализации. Точнее, бюджет на этого человека. Я же до конца совместной работы периодически спрашивал Вадима, нашёл ли он шиномонтажника. Не знаю, оценил ли он эту шутку или всерьёз думал, что мне эта шина чем-то бы помогла.
Для своего проекта я не хотел судьбу "Общей Шины", поэтому сосредотачивался на конкретных вещах, которые можно увидеть и пощупать. Чем конкретнее понимаешь, какой ты хочешь результат - тем вероятнее его добиться.
Чекпоинт
И так, 1 июня у нас был запланирован чекпоинт с демонстрацией достижений менеджменту. Была готова первая версия ORM. Пока без джойнов, я их прикручу чуть позже, но уже умеющая забирать данные. Отправка данных была сделана отдельно от ORM через gRPC.
Был сделан логин, система ролей пользователей. Роли были сразу сделаны иерархичными и включающими в себя другие роли. Пользователь мог иметь несколько ролей.
Был сделан набросок бизнес-логики для будущей задачи. Было несколько окошек: для внесения заказа, там работал импорт из экселя и отправка на сервер, для его просмотра и для редактирования параметров. Была сделана система переводов из статуса в статус (в черновике пока). Были пользователи, были вендоры, были окошки для внесения и редактирования того и другого. По-моему даже бренды были.
Была сделана клиент-серверная система трекинга данных. Это когда один человек вносит заказ в базу данных - а у всех остальных он появляется в списке. Понятно, что события отслеживались только те, которые проходили через сервер.
Да мы даже успели сделать редизайн!
Презентация выглядела великолепно. Файл экселя на 60к строк засасывался в клиента и обрабатывался за минуту и за считанные секунды отправлялся на сервер. Потом его можно было открыть и посмотреть. Аналитик с другой машины менял имена и добавлял новые проекты, менял им статус. Менеджмент смотрел, как у меня имена и статусы меняются сами и появляются сами новые проекты.
Переход по статусам, разграниченный по правам, запрет на редактирование в "отмороженных" статусах. Автоматическая фиксация юзера, создающего сущность. Даты создания и последнего изменения. Свободные комментарии в парадигме insert-only к каждой сущности. Это всё должно было стать стандартом новой системы.
Сам клиент запускался две секунды. И сразу со списком заказов в разных статусах. Цветастое. С иконочками. Кросивое. И любой документ открывался моментально. Здесь я на хитрость пошёл - я грузил данные асинхронно, поэтому первые данные появлялись действительно сразу, заполняя грид. И можно было чуть заболтать менеджмент, пока дока не прогрузится и задисейбленные кнопочки не заэнейблятся. Полная загрузка тяжёлого документа занимала секунд 15.
Красота! Булочка просто, а не демка!
Но менеджмент как будто это не интересовало. Он как-то сдержанно отнёсся к достижениям нашего велосипедостроения. И в конце выдал пару новых задач.
А мне оставалось работать шесть месяцев.
Две задачи
Первая задача - это автоматически определить, в какую нашу внутреннюю товарную категорию нужно определить каждую строчку товара. Напоминаю - файл не наш, а категории наши. И файл может содержать всё что угодно. Он может быть даже на английском языке с французскими вкраплениями. Ладно, про французский я преувеличил. Вот ЭТО, заполненное абы кем и абы как, надо каталогизировать. Народ думал в сторону ИИ. Но в нём ещё надо разобраться, найти хост и ещё обучить. И как-то интегрировать.
Вторая задача немного похожа на первую. Нужно было взять другой список из товаров, где абы как и абы кем написан состав. Опять на смеси русского и английского. Потом привести это безобразие в соответствии с законами о розничной продаже РФ. То есть стандартные материалы, отсортированные по убывающей, единый стандартный формат.
Вадим отбрыкивался как мог руками и ногами. Вадим хотел премию, а новые задачи ну вот совсем не повышали вероятность её получить. Я же устал ковырять в одно лицо большую утопичную систему и был рад причинить добро прямо вот здесь и сейчас, сэкономив время сотрудникам и финансы менеджменту. В премию я не верил.
Первая задача заняла недели три. Я решил обе делать на новой платформе, чтобы протестировать её в бою. Документ нужно было загрузить из экселя в систему, потом запустить автоопределение категорий. Алгоритм пытался найти подходящую, если была неоднозначность - предлагал варианты, а конечный вариант оставлял пустым, для ручного заполнения. Получалась некая гарантия, что предположение не будет воспринято, как определение.
В процессе разработки фичи был допилен интерфейс, кое-что приведено в порядок, костыли заменены на колёса. В гриде появился DragFill, как в экселе. Ну тащишь ячейку за нижний правый угол, она копируется по всему диапазону, куда тащишь. Плюс мультиселект с выбором значения для всего диапазона. Вместе с сортировкой, группировкой, фильтрацией и контекстными плюшками получился довольно мощный инструмент для быстрого проставления категорий. Я даж запилил хоткей: можно было набрать поверх ячейки 115 и система подставляла категорию 1.1.5. С названием, конечно.
Я тестировал до получения приемлемого результата на файлах, которые аналитик взял у бизнеса в качестве примера. В итоге сделал к каждой категории список ключевых слов, на которые и агрился распознаватор. Чтобы не было слишком распухшее, сделал механизм синонимичности. Потом заинтерфейсил редактирование этого всего, добавил столбец с найденными ключевыми словами и отдал аналитику. Он уже допиливал ключевые слова, повышая процент распознавания. Процент распознавания, к слову, был от 80 до 95 процентов.
Вторая задача оказалась неожиданно сложнее.
Форматы исходных данных были совершенно разные: где-то через запятую, где-то через пробел и так далее. Особая боль - композитные материалы. Всё вместе давало чудовищную комбинаторную сложность. Пришлось помимо двух вспомогательных таблиц прибегнуть к чёрной магии последовательной обработки. В итоге было сдано с интерфейсом для дополнения этих таблиц, инструкцией пользователя и очень хорошими показателями разпознавания. Дальше пользователи повышали показатели распознавания сами. Как и в первой задаче, среди разпознанных строк процент корректных был 100%. Ещё плюс три недели.
В середине разработки этих фич увольняют одного разраба. Оптимизация.
В конце июля уходит наш руководитель. Нет, не Вадим. Вадим руководитель отдела разработки. А уходит Максим. Он руководитель айти отдела, и Вадима тоже. Макс вместе с Вадимом проводил собеседование со мной и брал меня на работу.
Выяснилось следующее: руководство компании в лице руководителя компании дало задачу Максу выбрать двух айтишников для последующего увольнения и оптимизации расходов. Макс послал в жопу руководство компании в лице руководителя компании и написал по собственному.
Вадим сказал: ты следующий. Вас трое осталось, но без тех двоих сразу всё рухнет. Так что готовься.
Вадим оказался неправ, но я тогда этого не знал. И работать стало несколько неспокойненько. Оставалось четыре месяца.
Апдейтер
Был сделан механизм автообновления с версионированием клиента. При подключении к серверу клиент запрашивал последнюю версию клиентского ПО, тот выдавал клиенту xml-файл с тем, что нужно скачать. Клиент запускал апдейтер с этим файлом, апдейтер качал бинари и перезапускал уже нового клиента. Со своей стороны сделал кнопку релиза, которая выгружала актуальную версию на сервер. Всё через gRPC.
Задача его была простая. Дать мне возможность часто релизиться, не делая голову ни себе, ни юзерам. И общаться с пользователями я не очень люблю, так что не искал повода покрасоваться в общем чате. Да и при отсутствии QA однокнопочный релиз - это не прихоть, это необходимость.
Старая ИС просто выкладывалась в шаре и каждому юзеру на рабочий стол выносился ярлык на батник, который при каждом запуске перезаписывает клиента на "нового" из шары. После выпуска новой версии необходимо было оповестить весь персонал о смене версии, чтобы перезагрузились.
Клиента старая команда упаковала с помощью fody в один файл весом 80мб, чтобы не заморачиваться со списком файлов. Шара работала очень хреново, особенно в рабочее время, поэтому клиент обычно запускался от двух до пяти минут. Расположение клиента должно было быть стандартное, иначе система не работала.
В моей версии апдейтера выскакивало окно с предложением обновиться. При старте клиента, если он обнаруживал новую версию, обновлялся принудительно, не спрашивая пользователя. Это корпоративный сегмент, детка. Тут нефиг в старой версии сидеть. Тут даже в предложении обновления была настойчивость и отсутствовал ChangeLog. Но в последствии он будет в обновлённом клиенте, уже по факту обновления. Чтобы не спрашивали, а сделал ли я какую-либо задачу.
Третья задача.
Третья задача оказалась совершенно мерзкой. Неприятной. Честный Знак, который не честный и не знак, обещал отрубить апи v2 для регистрации куар-кодов индивидуальных товаров. Нужно было переходить на v3, но у нас 1с-ники были загружены переходом на 8.3. Полтора года уже, ага. Конечно, то, что отрубят - это писями по воде виляно. Не мы одни такие раздолбаи. Но к дедлайну совершенно точно не успевали починить 1ску. Мне дали две недели и сертификат для входа в апи. Нужно было сделать отдельный синхронизатор, берущий поток чеков из БД и отправляющий в апи. Это я сейчас упрощённо, на практике там много нюансов, вроде множества позиций чека, каких-то кодов ОКВДБЛИАД, групп товаров и разрегистрирования куар обратно. Потому что возврат.
Вадим почему-то не вспомнил про премию, а когда я ему напомнил, почему-то проникся корпоративным духом и преисполнился сочувствием к девочкам, которым придётся отправлять каждый вечер это вручную. Теперь уже я отбрыкивался всеми рогами и копытами. Конечно, в премию я не верил, но где-то в глубине души теплилась надежда. Ну вы же понимаете? Но силы были не равны.
Работать оставалось три месяца.
Пришлось поставить крипто-про и ещё какую-то госмалварь на комп, чтобы дотнет скушал сертификат. Кстати, крипто-про платный и у меня был месяц триала. Я подключился к апи v3, собрал джейсончики, но отправлять было стрёмно. Да хрен его знает, где я навертел и как отнесётся ЧЗ к повторной отправке данных. Кассы-то ещё работают. Да и уверенности в корректности данных оракла у меня не было. Там же синхронизация, помните?
Проблема была в том, что сертификат был от прода Честного Знака, а от теста ни у кого не было. И для регистрации в лк нужны были учередительные документы. О чём я радостно сообщил менеджменту. Предложил на выбор: или давайте готовый сертификат на тест или пусть кто-нибудь возьмёт на себя ответственность и даст мне добро на тестирование в проде. Менеджмент ушёл думать (спойлер: и не вернулся).
В середине октября сертификат протух, а я вздохнул с облегчением. Теперь я мог бессовестно разводить руками лапками и говорить, что без сертификата я не могу даже подключиться.
На следующий день уволили Вадима.
Закат без рассвета
Через неделю или две менеджмент вышел на связь. На еженедельном созвоне присутствовали новые лица. Саша и ещё один парнишка из его команды. Как мне объяснили, руководство обратилось к старой команде разработки для помощи в критичное время.
На созвоне Саша представился, повторил несколько раз, что он тут временно и ну ни в коем случае не собирается возглавлять отдел разработки.
Но почему-то много внимания было уделено моей системе. Я рассказывал про причины появления такой системы, показал наглядно фетч курильщика и фетч здорового человека (за пару секунд зафетчил табличку побольше из MySql). Отправил в чат ссылки на тикет по тормозам и статью в конфе на исследования по скорости. Это были железобетонные аргументы, документированные в общей системе. Кстати, как вы думаете, кто заставил меня в своё время их написать? Правильно - Вадим.
Саша, будто меня не замечая, называл меня в третьем лице и говорил, что я развожу руководство на деньги. Он оперировал суммой в 6 млн, уже потраченных на проект. Не знаю, откуда он взял такую сумму. Говорил, что старая система хорошая и нет смысла тратиться на новую. Мне было неприятно.
Но с проблемой тормозов Саша нехотя согласился, после наглядной демонстрации. А может мне показалось, что согласился.
Менеджмент сообщил, что будет принимать решение о судьбе системы 2.0. И ушёл думать.
Заниматься дальше проектом у меня не было никакой мотивации. Но сидеть без дела было как-то неудобно. А вдруг всё наладится и меня спросят, а что я делал? Вадим уже не работал, я был сам по себе и мне пришла в голову совершенно дикая мысль. Прикрутить MySql. С точки зрения Вадима это было бы кощунством, потому что все данные в оракле, а саппортить вторую БД ресурсов не было. Но теперь мне было примерно похер. Я развлекался как мог.
Я залогинился на свой тестовый сервер под рутом.
apt-get install mysql-server
Прикручивание MySql на удивление прошло быстро и просто. На всё ушло дня три. После этого скорость работы с данными выросла ещё в несколько раз. ORM на сервере мог обращаться одновременно к ораклу и мускулю, стримя данные в протобуф. Клиент вообще не мог понять, откуда данные, там все репозитории выглядели одинаково.
Я вздохнул с облегчением. Несмотря на то, что я ускорил работу с ораклом, это работало только в проде. На локальной машине по прежнему царствовали боль и страдания, потому что от локального сервера до оракла был большой пинг. Ковыряние ручками в базе также тормозило. Мускулю же было наплевать на пинг, у него довольно эффективный бинарный протокол, не ждущий ответа от клиента. Единственное, что тормозило при задранном пинге - это последовательный инсерт с последующим SELECT_LAST_ID(). Это я тоже поправил, добавив батч инсерт и стало совсем хорошо.
Человечьи инты! Автоинкремент вместо сиквенса! Вменяемые текстовые типы, которые не ограничены 12к символов и не путают пустую строку с нуллом! Реордер колонок на уровне БД! А ещё мускульный датаридер позволял dr.GetInt32() по имени поля, а оракловый - только по ordinal. И там приходилось анбоксить всё из обжекта и конвертировать в целевой тип. И зачем вообще мы столько мучались, а, Вадим?
На следующем раунде переговоров казалось, Саша забыл обо всех аргументах, которые я приводил. Он рассуждал о том, какая старая система хорошая, и как ей стало плохо, потому что создатели её оставили. Он отзывался о новой системе, как о фактически закрытой.
Я поинтересовался, смотрели ли ребята документы, ссылки на которые я им предоставил.
В этот момент я о себе много узнал. В третьем лице Саша обо мне рассказывал менеджменту неприятные вещи. Что я взял неправильную таблицу, не разобрался, что она для другого. Что я не умею в SQL. Что я развожу руководство на деньги и уже потрачено на меня 6 млн. Не надо выбирать из этой таблицы 8к записей. Она не для этого, он просто не разобрался. И вообще если ты выбираешь больше тридцати строчек из оракла, то ты делаешь что-то не так.
Саш, а это не твоя команда сделала выборку в 6к строк для комбобокса при каждом открытии окна и при каждом изменении параметров?
Я сделал последнюю попытку. Рассказал про объёмы документов поставки, которые нужны по ТЗ. Показал, что вот так тормозит, если запускаю сервер локально. И вот так всё летает, если подключу к серверу в облаке. Видите разницу? Это один и тот же код.
Второй раунд переговоров окончился ничем. Я забил на еженедельные совещания. Мотивировал тем, что это было поздно для меня (20-21 по МСК) и я на такое не подписывался. На начальственное "надо" я тоже забил. Настоящая причина была в Саше. Я не хотел с ним общаться, я не хотел выполнять его задачи.
На третьем раунде переговоров был Максим. Руководство попросило его рассудить нас. Если я мотивировал решение именно скоростью, то у Макса был совсем другой набор аргументов. Он опирался на свой опыт работы с разными системами и на примерах рассказывал, насколько трёхзвенка лучше двухзвенки. Он сказал, что решение строить трёхзвенку принял он. Конечно, он лукавит. Возможно он первый эту мысль озвучил, этого я не помню, но тогда мы вместе искали решение, а не Макс продавил своё.
Третий раунд закончился ничем. Менеджмент ушёл думать.
А я психанул. Попросил внеочередной двухнедельный отпуск. Первый за год. В октябре. Мне дали окно через неделю.
За день до моего ухода в отпуск на связь выходит менеджмент и сообщает, что мой проект закрывают. И меня переводят в команду к Саше для работы над старым проектом. Я искренне поблагодарил за наконец-то принятое решение по проекту, потому что уходить в отпуск в подвешенном состоянии - ну такое себе. Вежливо поблагодарил за предложение поработать в команде под руководством Саши. И отказался.
Договорились, что по выходу из отпуска, 6 декабря, я пишу по собственному.
Вот и всё. Всё кончено. Я проиграл. То, над чем я работал - больше никому не нужно. Всё было напрасно.
Вскоре одна птичка мне начирикала следующую историю:
Саша арендовал какой-то супермногоядерный многогигабайтный VPS в облаке яндекса. И воткнул туда Remote Application Streaming, чтобы запустить там клиента старой системы для каждого сотрудника. Оно даже иногда работало быстрее. На уровне эффекта плацебо. Но иногда сильно медленнее. И помните про эксель? Ремотный клиент требовал ремотного экселя для интеграции, что добавляло немало попоболи. И появились плюшки RDP вроде загрузки канала и тормозов стрима. Странно, конечно, что скорость не выросла. Возможно, виндовый зоопарк отгорожен от пингвиньего заборчиком и там тоже пинг.
Птичка потом тоже заявление напишет. Недели через две. Мотивирует невменяемостью нового руководства (Саши). Но пока что втихаря ходит по собесам.
Послесловие
Отпуск. Вечер рабочего дня. Официально я трудоустроен, а по факту уже уволен. Я сижу, пью пиво (алкоголь вредит вашему здоровью) и думаю. Как так получилось, что я не смог? Может быть я плохую систему построил? Может я плохие аргументы подобрал? Почему я не смог доказать, что я делал то, что нужно было делать? Почему мои убедительные аргументы оказались настолько ничтожными для Саши, как будто он рели...
Стоп.
Бооооже моооой. Как я раньше этого не замечал?
Воспоминание разблокировано:
Год и двумя месяцами ранее. Мы с (тогда ещё новым) аналитиком сидим в офисе компании и разговариваем с девочками-менеджерами. Нам показывают кусок функционала, где что-то отправляется посчитаться и оно там очень долго молотится. Может отвалиться. Может сломать соседние джобы. И вот вспомнилась одна фраза "Ну и когда мощная машина оракл нам доделает?". Сказано было с нескрываемым сарказмом. Тогда я не обратил на это внимание. Теперь я понял, откуда ноги растут.
Я понял, почему наш отдел называется "отдел разработка Oracle". Я понял, почему аналитический отдел бизнеса, не имеющий отношения к БД называется "Отдел аналитики Oracle". И группа в ватсапе у них "OraAnal". Я сейчас не шучу, если что.
Я понял, почему в документации конфы ехал оракл через оракл. Ора-то, ора-сё.
Как будто я в руках вертел крышечку и примерял к корпусу. А она "щёлк" и всеми защёлками одновременно встала в пазы и теперь плотно сидит. Без люфта. Вот такое странное чувство удовлетворения. Всё встало на свои места.
Я понял, о чём говорил Саша, когда говорил, что я не знаю Оракл. Я понял, что он был прав. Не может же быть проблем в Божественном Оракле! Если у тебя что-то не получается, то значит ты действительно что-то делаешь не так. Это надо понимать.
А я, дурак, допускал греховные помыслы. Скорости там какие-то, удобства, поддерживаемость, читаемость... Это всё мирское. Недалёкое. Безбожное.
Мне дали возможность исповедаться и принять Божественный Оракл в сердце своё. Предложили работать под присмотром братьев, Познавших Божественность Оракла. А я отказался. Да ещё и оскорбил своих братьев сомнениями. Искушал их грехом.
Вот в чём дело.
Получившийся в процессе написания статьи ORM я выложил в свой репозиторий. Это не готовый к продакшену продукт, это скорее проверка гипотезы. Концепт.
В репозитории пока что отсутсвует:
генерация эндпоинтов для селекта со стороны сервера
джоин таблиц на стороне сервера
отсутствуют поля для джоина один-ко-многим
поддержка LINQ на сервере
поддержка типов данных вроде bool, short, byte, char
интерцептор и авторизация по токену
структура для апдейта и инсерта объектов, вся система readonly
отслеживание изменённых полей сущности как для клиента, так и для сервера
батч инсерт
клиент-серверный трекинг на табличном уровне
отдельный трекинг, но по строкам документа, работающий по подписке (нужно было для совместного редактирования большого документа)
асинхронная загрузка потока
двухсторонний асинхронный стриминг данных
поддержка MySql в равной степени с ораклом прозрачно для клиента
Я решил пока это не делать, потому что восстановление всех фич займёт очень много сил и времени, а я даже не знаю, нужно ли это будет кому-то.
Если вам показалось, что я ругаю Вадима - то вам показалось. Вадим лапочка. Он всегда остывал и принимал разумное решение. А что касается микроменеджмента... Он исправится, обязательно исправится. Сейчас с ним всё хорошо. Он работает в другой компании.
Ссылки на телеграмм не будет. Я лучше в следующий раз пропиарю свой продукт для работы с БД в парадигме model-first. Когда допилю фичи, доделаю сценарии, допишу документацию и выловлю баги.
Спасибо, что дочитали до конца.
Удачи вам! Всем пше згыр!
UPD.
В комментариях есть вопросы про fetch_size. Я решил ещё раз проверить эту гипотезу и покрутить его. В интернетах советуют задрать его до 10к, но я точно помню, что мы это делали. И до 10к и до 30к.
У меня совершенно случайно есть исходники OrmFactory, я решил проверить там. Ставим 30к и брейкпоинт:

Ого, там уже 131к! Тогда попробуем побольше. Я поставил 300к и табличка загрузилась за 3 сек. Я поставил миллион и табличка загрузилась за 2 секунды. Целиком 288 рядов поместились только в три миллиона:

Признаю, в своё время не докрутили. Жаль, что сейчас уже это ничем не поможет.
Комментарии (79)
kmatveev
27.06.2025 17:47Офигительнейше, очень приятный язык.
Хотел спросить, для тех, кто не знает C# и Linq, что такое Include() ? Выглядит как join.
И ещё: я правильно понял, что причина тормозов в том, что драйвер оракловского протокола работает синхронно: заказывает fetchSize строк, и пока все не отдаст приложению, новые не запросит? И канал у вас был с большой latency, в которую, при таком протоколе, оно и упиралось, при том что пропускная способность позволяла, и вы это обошли тем, что gRPC хорошо умеет в стриминг? А fetchSize нельзя подкрутить?
vsting
27.06.2025 17:47Я полагаю что в данном случае include подгружает записи за один запрос, что бы избежать потом n+1
Kerman Автор
27.06.2025 17:47Спасибо. Include это не совсем join. Да, он транслируется в left join, но может работать только по внешнему ключу (в случае с OrmFactory ещё и по виртуальному внешнему ключу). То есть его условие вшито в модель.
Про причину тормозов я так и не узнал. Тогда я крутил всё, что крутилось на клиенте, а сейчас мне уже не актуально.
То, что смена протокола (добавление gRPC на участке с задержкой) увеличила скорость - я этот эксперимент буквально вчера повторил. Да, похоже на то, что протокол оракла ждёт подтверждения (или запроса) со стороны клиента. Но как действительно это работает - я не знаю.
just_vladimir
27.06.2025 17:47Полностью поддерживаю, что стоило провести эксперименты с fetch size. Это первое, что приходит в голову при чтении статьи.
Kerman Автор
27.06.2025 17:47Я тоже полностью поддерживаю! Стоило!
Именно поэтому я и эксперементировал с фетч сайзом. Я вообще крутил всё, до чего мог дотянуться. А дотянуться я не мог разве что до настроек самого инстанса оракла.
А сейчас мне это не интересно. Я не работаю в этой компании полтора года как. Ну узнаю я, как это можно было починить, а что дальше? Статья вообще не об этом.
ValeriyPus
27.06.2025 17:47В некоторых БД индексы просто отваливаются (не из-за фрагментации).
Всегда проверяйте Include (сколько их).
И сколько данных (в мегабайтах) возвращаете (с БД и с сервера через свой api).
include 10 элементов, связанных с 10 элементами это уже увеличение объёма данных в 100 раз. (вместо 100 кб за мс пришло 10 мегабайт за 2 секунды. Просто потому что у вас не RAID-6 на SSD, и еще 10 таких же запросов)
Поэтому обычно используют 2 маппинга - 1 облегченный на грид, второй - на полный обьект (в карточку обьекта)
Kerman Автор
27.06.2025 17:47Ха. В той системе инклюды 1:М упаковывались в объекты перед передачей. Если классический селект повторил бы левую таблицу N раз, то в протобуфе был типизированный объект, где левая сущность была в одном экземпляре, а правая (множество) в своём списке в поле левой сущности.
То есть от БД до сервера трафик был в сто раз больше, а при передаче в клиент как положено, без оверхеда.
Там была единственная проблема - нужен был OrderBy по полю для джойна для асинхронной передачи таких объектов. Просто потому что ORM нужно было понять, что вот этот левый объект уже заполнен и его можно отправлять на клиента.
ValeriyPus
27.06.2025 17:47Да, вы учтите, что у вас HDD читает дай бог 100 Мб\сек (если это не Raid-6 SSD).
Это - пиковая теоретическая скорость.
В реальности с БД может прилетать по 10-100 Мб.
Даже если эти 100 Мб и упаковываются в обьект - это уже секунда-другая только на работу EF.
Т.е. при 10 связанных объектах с еще 10 объектами вам просто придет в 100 раз больше строк (с полями основного и связанных объектов)
И есть подозрение что где-то что-то все-таки материализуется (при работе с IQueryable).
ValeriyPus
27.06.2025 17:47Не смотрели
https://github.com/esskar/Serialize.Linq
Делал то же самое, только в backend Web Api.
Сделал автогенерацию обьектов для поиска по сущностям (через И), и автогенерацию фильтров.
Для поиска обычно не нужны сложные условия (все через И).
Ну и конечные точки (без include) - CRUD, поиск.
Год - многовато.
Kerman Автор
27.06.2025 17:47Да, смотрел. Изначально как раз хотел взять его в паре с EF core. Клиент им сериализует запрос, а сервер нахлобучивает его на репозиторий EF. Не срослось, и я взял только идею оттуда.
ValeriyPus
27.06.2025 17:47А, ну и зачем аж целый протокол городить-то?
Лишняя безопасность?
Подключайтесь сразу к БД через VPN, выполняйте SQL запросы.
Kerman Автор
27.06.2025 17:47Так я же всю статью об этом говорил. Там даже отдельная глава есть "Тормоза", ещё есть "Почему ORM". В статье описано, почему трёхзвенка. Я описывал задачи. Ну прочитайте хотя бы заголовок, а дальше перейдём к более тонким материям.
ValeriyPus
27.06.2025 17:47Да, я вам уже написал что вам надо смотреть объём данных, планы запросов
https://habr.com/ru/articles/922672/#comment_28497376
Ниже пишут - еще и канал оказывает влияние. Да и в UI нигде нет больше 20-50 строк на странице, но мало ли.
https://habr.com/ru/articles/922672/#comment_28498602
За использование кодогенерации +.
Kerman Автор
27.06.2025 17:47Я специально выбирал табличку, чтобы получить объём данных. Зачем мне его смотреть?
Какой план запроса может быть у
select * from table where rownum <= 8000
?Каналы пробовали все доступные.
Да и в UI нигде нет больше 20-50 строк на странице, но мало ли.
А вы там работали? Таблицу на сто тысяч строк и 50 колонок не хотите? А заполнение комбобокса на 6000 подсказок?
ValeriyPus
27.06.2025 17:47А пагинацию зачем придумали?
(Да ограничивать объем данных, чтобы по 1-100 мегабайт не гонять в том же веб-е)
А зачем делают различные маппинги (Лайт и фулл)?
(Да та же причина - основных полей дай бог половина, по которым идет работа и которые смотрят в гриде. Остальное идет в карточку обьекта)
Это - базовые принципы, на которых работает всё.
Даже поиск\автозаполнение - нашел 1000 элементов, вывел первые 10
Kerman Автор
27.06.2025 17:47А пагинацию зачем придумали?
Я не буду здесь приводить ответ манагеров на предложение сделать пагинацию. Иначе меня за сквернословие забанят.
Работа у них такая. Такие массивы данных. Им вот НАДО. Понимаете?
К слову, мой ORM вполне себе умел в пагинацию. В статье есть реализация трансляции Take/Skip.
ValeriyPus
27.06.2025 17:47Если менеджеры не понимают что такое пагинация и почему она необходима - значит это некомпетентные менеджеры.
Итог вы даже в статье описали - сокращения.
Обычно тривиальные вещи с неграмотным менеджментом лечатся вопросом "И где ты такое видел?"
Вы не High-end делаете (и вам не нужен оффлайн-кэш БД и подгрузка Diff-ов).
Kerman Автор
27.06.2025 17:47Вы сейчас примеряете общей гребёнкой весь бизнес. Не надо так. Есть действительно необходимость работать с большими данными из-за специфики. Когда у тебя десятки магазинов по всей стране, в 25 строчек ты не влезешь тупо списком магазинов. А есть категории, поставки, остатки и ещё много чего другого.
Не надо называть людей некомпетентными, когда вы просто не понимаете, в чём их работа состоит.
ValeriyPus
27.06.2025 17:47Вообще, это элементарные (базовые) вещи.
Т.е. определять, что не нужно показывать все 100 тысяч сообщений на форуме в веб-е научились очень давно.
Как раз по причине задержек из-за объема данных.
Пагинация - Это используют даже ребята из Google(!).
Веб, как и WPF/Winforms - только лишь UI, который по API что-то получает. Т.е. принцип тот же.
Хранить (кэшировать) объект и писать команды (и доставать недостающие их по RowVersion, и применять их к объекту) - тоже не сложно. (Гугл-таблицы и прочие карты).
Kerman Автор
27.06.2025 17:47Вот сидит манагер с большой таблицей. Ему нужно разобраться с аномалией, почему не складывается прибыль. Он группирует по товарной категории, сортирует по продажам и глазами пробегает, зажимая PgDown. Иногда меняет колонки местами, иногда подкручивает параметры вспомогательные. Он ищет места, где происходят аномальные скачки. Потом сверяет с датами, потом с другими категориями. Потом с остатками. Когда ему кажется, что он вроде нашёл - меняет группировку, сортировку и начинает выискивать, уже зная, где искать.
Манагер находит проблему и описывает её в бизнес терминах. Он хороший манагер, он за три часа делает то, на что отделу разработки понадобится два месяца. С написанием ТЗ, согласованиями, уточнениями, тестированием и приёмкой.
Идите и скажите ему, что он некомпетентный и не знает элементарные базовые вещи.
northrop
27.06.2025 17:47Хотите сказать что он глазами пробегает сразу по 100000 записей, тратя меньше 100мс на посмотреть/оценить запись?
Kerman Автор
27.06.2025 17:47Гораздо быстрее. Я сам видел, как они работают.
В подозрительных местах останавливаются, рассматривают детальнее. В каких-то местах идут медленнее, где-то проматывают.
Это натренированная годами на продажах нейросеть. Там используются такие параметры, о которых вы даже не подозревали, как погода на прошлой неделе в Воронеже. Только не спрашивайте, как это влияет на продажу плащей и зонтиков. Я не знаю.
ValeriyPus
27.06.2025 17:47Вот прямо Infinite Scroll в руку.
И 2 маппинга
https://habr.com/ru/articles/922672/comments/#comment_28497376
Kerman Автор
27.06.2025 17:47Дружище. Ну вот зачем ты это говно советуешь?
Зачем ты вообще пытаешься починить систему, которая почти два года не актуальна?
Ты хочешь сказать, что я плохие варианты выбирал и есть лучше? Но прости, ты не в курсе тысячи нюансов. Ты не в курсе, что я не пишу веб. Потому что не читал статью, где я про это говорил. Ты не в курсе, что бесконечный скролл хорош для просмотра постов на пикабу, но он категорически не подходит для работы аналитика. Ты не в курсе, что бэкенд под это дело тоже надо писать на сервере, хотя должен же быть, правда?
Ну вот зачем?
ValeriyPus
27.06.2025 17:47Проектируйте как хотите.
Если пагинации нет,
или какого-то кэша к которому применяются команды с сервера (или события).
Думаю, объяснять про декартовый взрыв смысла нет.
https://learn.microsoft.com/en-us/ef/core/querying/single-split-queries
Равно и то, что в некоторых БД AsSplitQuery не работает, и там проблема решается через .AsTracking и .Load.
Кстати, самое смешное что AsSplitQuery\Load реально может ускорить запросы в сотню раз (1 мб вместо 100 из БД).
Но 9 из 10 команд продолжают использовать Dapper и не знают почему 2 инклуда норм а 3 уже не работают.
BadDancer
27.06.2025 17:47Краткое содержание.
Кривая структура БД может причинять такие тормоза, что даже Oracle не не тащит, и даже ORM будет быстрее.
Мысли по поводу.
Улучшить быстродействие при увеличении пинга можно было увеличив FetchSize (умолчание в 10 строк годится только для БД в локалке). Понятно, почему не сделал автор - он не знает Oracle, но очень странно, что секта божественного Oracle этого не сделала.
При удаленной БД трехзвенка выглядит необходимой, дабы сервер таки рядом с БД был, странно, что руководство не осознало.
Если проблема только в свидетелях секты Oracle, можно было не брать MySQL, а организовать в новой БД рядышком - и народ бы меньше сопротивлялся, и часть аргументов типа нет возможности саппортить разные типы БД выбило бы.
Oracle не хуже и не лучше MySQL. Просто у них немного разное применение. Условно, Oracle и MySQL , как MAN сорокатонник рядом с грузовой газелью. Если для задач бизнеса хватает газельки, то пользовать MAN выглядит избыточным, если не сказать более.Kerman Автор
27.06.2025 17:47Кривая структура БД может причинять такие тормоза, что даже Oracle не не тащит
В статье показаны тормоза на практически пустой базе после свежей установки. Исходники есть в гитхабе, репозиторий тоже, методику я расписал, можете сами проверить. Речь не идёт о кривой структуре.
Улучшить быстродействие при увеличении пинга можно было увеличив FetchSize
Крутили. Насколько я помню, там максимум в 36кб. Могу ошибаться. Но я точно помню, что не помогло.
не брать MySQL, а организовать в новой БД рядышком
А зачем? Я немного не понял смысл. MySql я расписал, чем был лучше. Он тупо быстрее работает во всех инструментах со своим стандартным протоколом из коробки. А зачем новый инстанс оракла ставить рядом? Чисто для тестов? Так прод всё равно будет в облаке, потому что начальство наотрез против переноса данных в офис.
BadDancer
27.06.2025 17:47Каюсь, читал статью несколько по диагонали, т.к. пишу на Java. Но, если дело не в БД, если правильно понимаю, то проблема тормозов на пустой базе = проблема пингозависимости фетча (медленный по сравнению с MySQL протокол при большом пинге).
Т.е. если разместить потребителя рядом с БД - тормозов не будет. Т.е. надо сразу пилить трехзвенку, разместить сервер рядом с БД и отдавать потребителю ровно столько, сколько надо, хоть через protobuf, хоть http.
Не новый инстанс оракла рядом, а новую схему данных в том же инстансе.
Вообще сложилось впечатление, что Oracle для вас был избыточен. На наших задачах и с сервером рядом с БД (пинг меньше 1мс) он вполне неплохо работает, в таблицах на сотни миллионов - миллиарды строк выборка по индексам единицы - десятки тысяч строк в запросах до десятка джойнов (но в основном к справочникам конечно) за время единицы - сотни миллисекунд.
Есть отдельные долго выполняющиеся запросы, там как правило в силу некоторых причин приходится перечитывать несколько сот гигабайтную таблицу полностью, тут приходится ждать, да.
Tzimie
27.06.2025 17:47Автор, я все понимаю. Но вот вы сдали машину в сервис, потому что мотор плохо тянет. Через шесть месяцев вам ремонтник, светясь от счастья, говорит, что с самим двигателем ничего не получилось, зато в свободном месте в багажнике он поставил дополнительно двухтактный мотор от инвалидки (MySQL) и переделанный двигатель на основе мопеда, правда подавать бензин и искру надо вручную (contains и джойн пока нет).
Kerman Автор
27.06.2025 17:47Ваша аналогия похожа на котёнка с дверцей. MySql появился уже потом, когда уволили людей, бравших меня на работу и принимавших решение про трёхзвенку. Я оставался один и проект мой хотели закрыть.
В этом проекте всё уже было. И джоины (1:M и M:1) и трекинг и всё, что перечислено в последнем списке. Это просто проект для этой статьи такой урезанный.
Да и чем MySql плох? За что его так сравнивать с мотором от инвалидки? Разве не умеет хранить данные? Умеет. А в чём дело?
Tzimie
27.06.2025 17:47Мне кажется основная проблема была в том, что (даже считая что вы все сделали полностью и идеально) вы добавили новую сущность, которая написана в одно рыло, сложная, соответственно без community support, с полным bus factor, и все это надо развивать и поддерживать в дополнение к существующей, при этом менеджеры уже знали, что из тех айтишников останется два ...
Kerman Автор
27.06.2025 17:47Согласен. Сложности добавилось. Это если говорить про трёхзвенку.
Но у нас была задача сделать одновременное редактирование большого документа целым отделом. Это без серверной части не делается ну никак. Можно, конечно, обмазать всё триггерами и ходить каждым клиентом проверять, что там изменилось, но вы же понимаете, что это так себе решение.
Трёхзвенка сложнее в разработке, но при сложной логике она проще. Вот такой парадокс. Мы знали, на что шли, это всё было учтено при принятии решения.
А ORM на этой трёхзвенке уже практически не привносил сложностей. Зато вот сильно упрощал написание, а главное чтение бизнес-логики. Он нужен был на начальном этапе, когда я один пилю. Если бы набрали команду, то потихоньку перенесли бы логику на сервер.
4youbluberry
27.06.2025 17:47Сюжетные повороты и накал напряжения прям как в книге. Для меня статья скорее "какие проблемы решают большие дяди", нежели что-то действительно познавательное.
Kahelman
27.06.2025 17:47Что-то задается мне нас тут за лохов держат…
БД Оракл- в головном офисе , который может быть за Х000 км это нормальная практика.
Таблица на 40 млн. Записей выгляди. Большой но тоже не про Лема, не думаю что народ в Excel 40 млн. Записей за раз тащит.
Логика в БД - радуйтесь у вас есть view, stored procedures + в оракуле persistent view. Берем нормальную книжку по оракл и вперед.
Сравнивать оракл и MYSQL - это вообще за гранью.
Автору можно было выкинуть формы и использовать Excel как фронт-енд. Я в свое время так делал. Генерал оезультыта на сервере в виде csv и выдавал на клиента а там просто парсил и отображал на колонки/строчки. Все нормально работало- пользователи были счастливы.
В общем курите маны они рулез:)
Kerman Автор
27.06.2025 17:47Что-то задается мне нас тут за лохов держат…
БД Оракл- в головном офисе , который может быть за Х000 км это нормальная практика.
Я выложил исходники, описал методику. Почему бы вам самим не проверить?
Логика в БД - радуйтесь у вас есть view, stored procedures
У меня в языке программирования общего назначения есть классы, наследование, полиморфизм, интерфейсы, свойства, события, отладка, тесты, рефлекшен, куча библиотек, ORM, гит и ещё много чего другого.
Что-то меня не радует наличие хранимых процедур в БД. Что там ещё есть для управления сложностью больших проектов?
Сравнивать оракл и MYSQL - это вообще за гранью.
Почему?
Автору можно было выкинуть формы и использовать Excel как фронт-енд
Спасибо, не хочу.
Kahelman
27.06.2025 17:47Если вы сам себе злобный Буратино то это ваши проблемы. Как вам ООП, события, ORM помогут не тащить 40 млн записей с сервера на клиента? Могу поспорить на 100500 мильонов что вы не круче людей которые писали движок Оракла и оптимизировали его. Поэтому подготовить данные на сервере и передать на клиента только нужный результат это единственно правильная стратегия. Делать это на клиенте имеет смысл если у вас клиентов миллионы и сервер просто физически столько соединений не потянет. Но с вашими двумя калеками и 10-ю запросами в минуту - сервер это не проблема.
Почему-то Мерседес может все свои производственные данные за кучу лет держать в оракловской БД, а вы не можете. Вы круче Мерседеса?
Kerman Автор
27.06.2025 17:47Как вам ООП, события, ORM помогут не тащить 40 млн записей с сервера на клиента?
Я спрашивал про управление сложностью. Не надо подменять тезисы. Пожалуйста.
вы не круче людей которые писали движок Оракла
Почему не круче? Там какие-то особенные супер-люди, до которых нам не дотянуться? Так какие-то волшебные знания, отличающиеся от привычной компьютер саенс?
Почему-то Мерседес может все свои производственные данные за кучу лет держать в оракловской БД, а вы не можете.
И пусть держат. В чём вообще проблема? Или в том, что я усомнился в Божественности Его Величия Оракла?
Kahelman
27.06.2025 17:47Для начала почитайте как тестировался SQLight, это далеко не постргоесс и не оракл. Потом рассказывайте какой вы крутень . Если вы хотя бы в половину того что делают разработки Оракла и Постгреса можете, то что же вы в «мухосранских контрах» сидите? Вас бы уже гуглы с фейсбуками с руками оторвали …
Ext_Art
27.06.2025 17:47Вы серьезно? Хранимые процедуры (логика в БД) были анахронизмом еще 20 лет назад.
Kahelman
27.06.2025 17:47Этакий кто вам такое сказал? Ссылку можно? „а мужики-то из Оракла и Постргреса» и не знают…. Если вы пилите прилож уху которой будут два человека пользоваться то возможное вам и не надо. А когда у вас предприятие Котору лет до фига и данных тоже, то ту другие разговоры. Куча страховых компаний в той же Европе ещё на мейнфреймах и Коболе сидит а вам тут хранимые процедуры устарели. Ну-ну, предложите страховщикам переписать все на вашем новомодном фреймворке … Потом поделитесь ответом
Kahelman
27.06.2025 17:47“4,4 секунды! О как! Похоже, я перестарался с замедлением канала. Что стало видно сейчас - так это то, что данные поступают рывками. Это не тормоза OrmFactory, она в норме показывает плавный скролл на 60fps. А тут видно, что прямо слайд-шоу, как крайзис на встройке. Что любопытно, даже на глаз видно, что интервал "подёргиваний" примерно равен четверти секунды, то есть те самые 230мс пинга за небольшим плюсом на передачу очередного пакета.”
Есть мнение что автор немножко балабол и не в тебе БД и оптимизации вообще.
Так получилось что я сейчас под Шанхаем, и есть корпоративный сервак с Oracle. Причём сервак не сильно шустрый.
Считаем:
Пинг от Шанхая до Германии 600-800 мсек. сижу на немецкой сим карте через корпоративный ВПН.Берем старый добрый SQL Tools 2 -GUI для работы с Ораклом, build 19, которому сто лет в обед - копирайт указан до 2021 гота
Запускаем select all, из таблицы в которой 3000 записей.
Время выполнения 2.96 sec.
Если запустить count (*) то результат будет около 500-800 мсек - т.е фактически время пинга.Так что все у Оракла с работой по сути хорошо.
Если имеетс Нормальный канал, а не черт знает что, то время ответа будет равно времени пинга.Тут по прямой под 9000 километров, а по проводам гораздо больше будет.
Не думаю что у автора сервер за 10000 км находится чтобы расстояние стало критическим фактором.
Kerman Автор
27.06.2025 17:47Не думаю что у автора сервер за 10000 км
Сервер у меня находится на локальной машине в виртуалке
автор немножко балабол
Ещё раз перейдёте на личности - получите минус в карму. Я предупредил.
Я вам только что сказал - есть исходники, есть методика. Проверяйте. Если есть сомнения - оформите мысль так, чтобы это можно было протестировать.
Kahelman
27.06.2025 17:47Как говорили запорожские казаки. « какой же ты рыцарь, коль голым задом ежа не придавишь». Напугал минусом в карму. Я такой фигней не страдаю…
Kerman Автор
27.06.2025 17:47Запускаем select all, из таблицы в которой 3000 записей. Время выполнения 2.96 sec. Если запустить count (*) то результат будет около 500-800 мсек - т.е фактически время пинга.
Три секунды для 3к записей - это прямо много. Очень много.
51к записей из MySql за секунду У вас разница между пингом и фетчем всего получается больше двух секунд. Неужели сами не видите?
Давайте я сделаю тот же фетч через впн.
те же 51к записей То есть у меня время меньше, чем у вас, но записей на порядок больше. Вопросы?
Kahelman
27.06.2025 17:47Я вам написал что я в Китае за китайский фаерволлом, по мобильному телефону. А вы поди по широченному каналу. Читайте что вам пишут. Далее - на вашем скриншоте не видно 3000 записей, так что вы там реально выбрали -вопрос большой.
Kerman Автор
27.06.2025 17:47Не-не, дорогой мой. Так в серьёзных дисскусиях не делается. Я никого не в чём не обвинял. Это Вы меня обвинили в балабольстве. Теперь обозначайте, что в моей статье неправда, а потом ставьте эксперимент, который я описал, публикуйте результат и потом уже говорите, что я балабол.
Я понимаю, что сложно поверить в то, что мускуль может 51к строк за секунду выбрать. Но что вам мешает самостоятельно проверить?
Kahelman
27.06.2025 17:47Вы коллега, таки балабол, поскольку «выбрать из БД» и передать по сети это таки не одно и то же.
Далее, если вы слегка понимаете в устройстве БД то должны знать, что выбрать данные один раз, и послать повторный запрос на выборку тех же данных -это тоже две большие разницы. Поскольку «нормальные» БД во первых сохраняют план выполнения запроса - т.е. время выполнения повторного запроса будет в любом случае быстрее, а во вторых будут использовать «горячий кеш» Так что замерить скорость выборки данных Не так то просто.
Kerman Автор
27.06.2025 17:47Я обещал минус поставить - я поставил. За неконструктивное общение. Я за вежливое и конструктивное.
Если вы не видите второй скрин, где подписано, что это через ВПН, то дальше нам общеться не о чем.
SoVaLoL
27.06.2025 17:47А мне обидно стало за автора :(
Ни черта не понял толком из статьи - не моя область, но ощутил и понял что ТС фактически сделал из говна конфетку
С согласованиями, убеждениями и палками в колесах в процессе
Затем пришли менее компетентные чуваки, которых уволили, ущемились и слили более скиллового разраба
Чтож, автор, проблема не в вас а в не адекватном менеджменте. Очень жалко что работа не реализована осталась, но если менеджмент идиоты - то тут ничего не попишешь
P.s. даже не вкуривая в смысл технички - было очень интересно читать) прям кайфанул :) респект ТСу!
Ext_Art
27.06.2025 17:47Автор взялся за дело о котором ничего не знал в начале работы (оракл и его протокол) и попытался построить трехзвенный велосипед (космолет), так и не разобравшись в сути проблемы. Менеджмент почуял что все идет не по резьбе и в отчаянии пытался решить проблему начиная с верхов и последовательно увольняя руководителей автора от старшего к младшему. Пока не дошел до самого автора. Выглядит на самом деле так. Если прочитать статью внимательно, мы увидим, что менеджмент вообще никак не мешал автору. Но вот признаки сомнения присутствовали после первой же презентации самописной трехзвенной системы ORM с блекджеком и женщинами низкой социальной ответственности, которая вот-вот будет написана в одно лицо в кратчайшие сроки.
Kerman Автор
27.06.2025 17:47Менеджмент сам хотел вменяемую и быструю систему. Он сам подписался под графиком работ, о чём я в статье писал. Более того, менеджмент сам пообещал премию, если успеем к НГ. Никто их за язык не тянул. Но проект закрыли в октябре, хотя я шёл по графику, несмотря на три дополнительные задачи. Это стоило мне отпуска.
endpoints
Статья показывает, что как не надо было делать и почему нельзя соглашаться исправлять косяки других)
bossalex
Вся проблема в том что, изначально всё неправильно было создано и главный гемор это excel. Тут даже и web технологии мало помогут. Проще всё взять переделать и упростить. Но на это никто не пойдёт придётся делать рефакторинг бизнеса. Но при глобальной схеме бизнес процессов их можно и нужно упрощать, создовать свой универсальный стандарт, который позволит масштабировать систему и скорее всего менять БД sql на объектную. А так сплошные заплатки, эксперементы и прочая лабуда, показывающая ковыряние супер пупер синьора. А нужен был обычный кризис программер, который бы не ковырял старые развалины кое-как работающего кода и кое как работающего БД. Изначально надо было строить схему и это должен был сделать бизнес аналитик, а тот просто пошёл на поводу и собирал всё хотелки пользователей. Проблема в том что работа в команде, а команда как рак, лебедь и щука. Поэтому глобально с задаче никто не работал и думаю вся эта работа нацелена на то как сделать работу куче специалистов и не дай бог никого не уволить и сохранить бюджет. И это тупо для тех кто работает в больших конторах и это нравится никого не уволили и при этом денег потрачено желательно чем больше тем лучьше. На какой нибудь no-code вся эта система была бы переписана и запилена с встроенно no sql СУБД за пару месяцев типа VisualData или таже банальная delphi на проостом фреймворке даже с sql СУБД. Зная тот же dev express эта такая неоптимизированная grid система что и не удивительно. Проще ehlib хоть и не панацея.