Pull to refresh

Comments 13

Плохо это тем, что, во-первых, сгенерировать запрос сам по себе для ORM — весьма тяжелая операция, а затем он передается в СУБД, где происходит трата ресурсов еще раз: его необходимо распарсить, провалидировать, создать план исполнения и только потом исполнить

Не понял. Почему генерация, парсинг, валидирование и создание плана выполнения осуществляются каждый раз?

Берём, к примеру, sqlite. Создаём (один раз) код запроса с параметрами. Делаем вызов sqlite3_prepare_v2(). А дальше каждый раз лишь вызываем sqlite3_bind_..., чтобы задать значения параметров, и выполняем запрос. Вроде очевидная оптимизация, идея генерить текстовое представление каждого запроса изначально порочна.

Я в своё время был очень неприятно удивлён тем, что Python DB API не декларирует PREPARE STATEMENT. execute() занимается сразу и подготовкой, и исполнением. И потому подготовленными запросами не балуются в ORM. Уж в SQLAlchemy точно. Это не смотря на то, что в драйверах, конечно же, оно всё есть. Продраться через кишки engine|connection к реальному драйверу (например, psycopg2) можно, после чего можно вызвать его функцию prepare (кажется, так называется). Но сама алхимия этим не пользуется, вызывая запрос на исполнение каждый раз, даже если он же был в этом же самом подключении использован в предыдущий раз.

Более того, например, в PostgreSQL подготовка запроса не избавляет от этапа планирования. И это, в общем, очевидно, поскольку подготовить запрос можно за пределами транзакции, и этоп план будет валиден и в пределах транзакции, за пределами которой он подготовлен.

PostgreSQL – по вашей ссылке написано "Use of a generic plan avoids planning overhead", и вроде ORM – это как раз то использование, когда это оправдано.

А насчёт питона удивили, да. Не готов внимательно читать документацию (не пишу на питоне). Правда, copilot выдаёт примеры с примерно таким же использованием, как на C (и однотипно для sqlite/mysql/ibmdb), но тут вникать надо.

В том и дело, что на чистом драйвере отдельно взятой СУБД всё это доступно, но алхимия — это слой абстракции над всеми возможными драйверами, в том числе теми, что написаны на чистом Python. В этом случае расчитывать на большее, чем стандарт DB API не приходится. Все выходящие за рамки него интерфейсы по определению не стандартизованы: где имя метода другое, где порядок аргументов другой, где тип возврата своеобразный. Поэтому: раз алхимия, то никаких prepared statements. Впрочем, отважному джедаю и это не помеха. Я в алхимии дергал из Connection нижележащий драйвер psycopg2 и у него уже дергал copy_from / copy_to, когда сильно приперло, оставляя основную часть приложения на Query Builder и engine::Connection.

Жуть какая.

Интересно, почему не сделали определение конкретного движка и вызов его методов для оптимизации. Для массово используемых библиотек вроде обычная практика.

Нет такого понятия, как «методы конкретного движка», если речь о драйверах конкретной СУБД. Есть DB API, кривой стандарт. Алхимия наружу предоставляет ровно то, что определено в нем, поскольку больше не может гарантировать. При этом, если очень надо, можно всё же добраться до драйвера в рантайме и вызвать специфичный метод (конечно же, то, что принимает штатный execute() в качестве параметров, придётся конвертировать, иначе не примет). Возврат, само собой, тоже вручную обрабатывать придётся. Это Python, можно почти всё, что угодно, но у всего есть цена.

Вот смотрю я на финальные примеры python-кода и итогового sql-запроса, и не понимаю, зачем вообще эта возня с ORM. Для того, чтобы написать такой python-код все равно потребуется хорошее знание sql, ты все равно будешь вынужден потестировать запрос на реальных данных, покрутить-посмотреть, что ничего не забыл, зачем все это, мистер Андерсон? Все, что перечислено в итоговом пункте "Итак", на мой взгляд, никак не перевешивают необходимость в целом разбираться во всей ORM-библиотеке, как она что под капотом делает, не лучше ли пустить это время на изучение sql и кишков употребляемой субд для написания более эффективных запросов?

Btw, работа по рецепту готовки Алхимии проделана мое почтение, сам пост хорош!

зачем вообще эта возня с ORM

Затем, чтобы сохранить контроль над структурой и именами при возможных рефакторингах, например, схемы данных. В сотнях портянок чистого SQL запросто может найтись несколько штук одинаковых именований разных сущностей: customers.name as name vs. users.name as name vs. cities.type_name || ‘ ‘ || cities.name as name. Переименовать что-то одно массовым Rename all, не сломав весь проект, бывает трудно, а то и невозможно. ORM от этого всего абстрагирует.

Плюс, как уже говорил, сбрасывать со счетов возможную помощь alembic для простейших миграций не стоит.

Для того, чтобы написать такой python-код все равно потребуется хорошее знание sql

Так оно, звание, нужно в любом случае, если разработчик допущен к участку кода, который обращается к SQL-серверу.

ты все равно будешь вынужден потестировать запрос на реальных данных

Да кто ж к реальным данным разработчика-то пустит? Там десять огородов оцепления из эксплуатации, SecOps и всевозможных compliance. Генерируй синтетику на девбоксе, обмахивайся тестами на QA, лови ошибки правильно и скорее выпускай хотфиксы на стейджинге. А прод — он для серьезных пацанов. Там совсем другие соображения превалируют.

Все, что перечислено в итоговом пункте "Итак", на мой взгляд, никак не перевешивают необходимость в целом разбираться во всей ORM-библиотеке, как она что под капотом делает, не лучше ли пустить это время на изучение sql и кишков употребляемой субд для написания более эффективных запросов?

Я исхожу из обратного: ORM сейчас почти в каждом «пет проекте» (они неправильно это называют) выпускника курсов вайти есть. Знать и понимать хотя бы этот инструмент — обязанность разрабочиков, которые его выбирают. Отсюда уже можно начать плясать дальше: как использовать этот инструмент на всю катушку, чтобы получить максимум выгоды. Да, если не знаешь SQL — беда-печаль, но научиться можно. В качестве подспорья: выше почти готовое решение для тех, кто отважится. В конце-концов, то, что я предложил в виде готовой реализации, допускает постепенную миграцию. Один запрос переписали, второй, третий. Стальные ждут своей очереди в виде уже имеющихся ORM-монстров и при этом ничуть не ломаются.

В сотнях портянок чистого SQL запросто может найтись несколько штук одинаковых именований разных сущностей

А label("string") сильно спасает?). Во-вторых, если это хорошая портянка, как правило с какой-то аналитикой, то кроме хороших тестов вам ничего и никто не даст уверенности, что ваш запрос делает то, что от него требовалось. Именования это там ещё пол беды, не менее важна логика.

По поводу логики: как запрос не крути, он всё равно декларативен (SQL под капотом же). И вам всё равно надо понимать запрос целиком, не важно на чем он написан. И дебажить по кускам и поставить брейкпоинт посреди запроса вы всё равно не можете.

Потратив время на SQL, вам проще будет освоиться завтра на другом стеке или как минимум прочитать запросы, которые прилетают от соседней команды, которая пишет не на питоне. И один из самых важных плюсов для меня - на SQL я почти всегда смогу найти решение на условном stackoverflow. Найти его на ORM что-то сложнее крудов уже не оч просто.

Alembic. Если проект несложный, то в целом и не столь обязательно отказываться от него. В другом случае - никто не мешает писать для миграций код самому. К тому же, алембик всё равно надо всегда перепроверять, он любит в автогенерациях свинью подкинуть, например, вместо переименования колонки просто взять и удалить её и создать новую с новым именем.

SQL - боль, но ORM за пределами крудов может стать ещё бОльшей болью.

А label("string") сильно спасает?

Он очень помогает тем, что хотя бы не мешает, потому что локальный. Алхимии и базе они не нужны, на самом деле, они нужны мне в качестве опорных точек, чтобы сверять до коммита то, что получилось, с эталонным запросом, который я только что отладил в сыром виде.

Вспомни, как ты работаешь с SQL: хотя бы две таблицы в запросе есть — и ты уже навешиваешь алиасы на имена таблиц, на автомате уже. И уж тем более, если имеется неопределенность (в двух таблицах есть колонки, которые называются одинаково и хотя бы одна участвует в запросе). Переименуй мне по всем сырым запросам поле name в таблице customers. Вот здесь “c.name” автозаменой можно, или это cities.name?

Я открою маленький секрет (полишинеля): я сначала пишу запрос в чистом виде, отлаживаю его, насколько возможно, в локальной базе со всякой синтетикой (плюс справочники, которые не являются данными кастомеров, а потому есть у меня под рукой), и только потом переношу его в алхимию. Так не только проще, но у меня вышло единственным рабочим вариантом.

Что до алембика: его миграция же так и так — файл в PR. Его так и так другие будут смотреть, почему я перед коммитом его не обязан посмотреть? И да, именно то, что алембик — тот же питонячий скрипт, дает ему бесконечное количество форы перед движками миграций, основанными на чистом сиквеле. Вот, к примеру, надо немного пошаманить во время миграции, условий всяких отработать. Можно, конечно, анонимную процедуру для какого-нибудь Flyway попытаться впихнуть. Но проще на питоне. У нас как-то девопсы засетапили постгрес не той версии (11-я, кажется, а то и десятая). Там enum так просто не обновить. Мы накалякали на питоне простенькую функцию, которая разом делала нужное со всеми таблицами, где использовался enum, и обновляло так, как в этой версии это было возможно. С процедурой там возни много было бы. Ещё в проекте менеджмент очень не хотел заниматься этими справочниками, предпочитали швырнуть эксель, мол, обновляйте, на разработку штатного средства управления справочниками бюджет выделять категорически отказывались. Вот Flyway сдох бы всасывать эксель или хотя бы csv, валидировать, размечать уже имеющиеся в таблицах данные пользователей, обновлять таблицы, да так, чтобы ключи отношений не взбрыкнули, плюс там некоторая генерация связей от справочника делалась (комбинации вносились в базу для работы триггеров, внешних ключей и CHECK CONSTRAINTS). Миграции на питоне в алембике это вывозят, не поморщившись.

И, как это написано в статье, ORM здесь нужен для автодополнения, очень быстрого рефакторинга и сахарочка вида string.contains(). Круды-не круды, да хоть аналитика всякая.

Я открою маленький секрет (полишинеля): я сначала пишу запрос в чистом виде, отлаживаю его, насколько возможно, в локальной базе со всякой синтетикой (плюс справочники, которые не являются данными кастомеров, а потому есть у меня под рукой), и только потом переношу его в алхимию. Так не только проще, но у меня вышло единственным рабочим вариантом.

Вы даже сами признали, что это у вас единственный рабочий вариант. А у вас команда из 7 бэкендеров. Двое пришли с ноды, а трое писали на джанге всю жизнь и не видели алхимию. Может проще уж всем на SQL ?)

Алембик - хороший поинт про дата миграции, это действительно хорошее решение, никто не заставляет отказываться, можно комбинировать инструменты и брать лучшее (или не брать худшее) от разных миров.

Пересаживать на комбинированный SQLAlchemy ORM + SQLAlchemy SQL, однозначно, стоит. Здесь моя вкусовщина, но джанговский query builder имеет отвратительнейший синтаксис на аннотациях, полностью ломающий понимание того, что здесь происходит, для человека, который с SQL знаком, а джангой нет. А вот sa.sql будет интуитивно понятен и тем, и другим.

О том, что переход к изложенному в статье можно выполнять постепенно, вместо того, чтобы выкинуть всё старое разом и весь проект за раз переписать на то, что выше, я тоже уже упоминал.

Годная статья, спасибо! Вернусь к ней когда возьму в следующий раз в руки, алхимию😊

Sign up to leave a comment.

Articles