Беглый опрос коллег на моем текущем проекте показал, что при словах "ORM и работа с БД" в подавляющем большинстве случаев звучат слова "Алхимия" и "Django ORM". Знания этих двух слов, в общем, достаточно, чтобы писать чистый, аккуратный и рабочий код. Но расширение инженерного кругозора пока еще никому не вредило, поэтому сегодня мы добавим в нашу картину мира несколько (возможно, до этого дня незнакомых) классных штук для работы с БД.



Yoyo


Сегодня любая ORM идет со внутренней системой миграции БД. Простой и классный подход, когда ORM следит за структурой таблиц, в общем, всех устраивает. Но у этого решения случая, когда пользоваться им неудобно:


  • Есть парни, которые фигачат запросы в БД, минуя ORM. При этом используется что-то типа asyncpg и небольшая самописная нашлепка для упрощения составления запросов.


    Почему эти парни отказываются от удобств ORM? Да потому что любая обертка для БД сжирает некоторое количество системных ресурсов, а этим парням нужно писать высокопроизводительный код. ORM они выкинули, а базу мигрировать им как-то надо.


  • Встроенные миграторы могут иметь свои специфические взгляды на построение связей и индексирование записей. Порой победить эти воззрения ORM на структуру таблиц не удается, приходится самому нашлепывать исправления запросов.

В обоих этих случаях удобно примененять миграции, составленных вручную — мы не опираемся на модели ORM, а втупую руками набиваем необходимые SQL инструкции и кормим их в простенький мигратор, который последовательно применяет изменения структуры к БД.


На выходе получается чистая, понятная и полностью управляемая структура таблиц, составленная с умом.


Для такого вот ручного подхода существует мигратор схем БД yoyo.


pip install yoyo-migrations

Дальше вся работа по управлению миграциями идет с помощью исполняемого файла yoyo


yoyo new ./migrations -m "Add column to foo"

Команда создает файлик, в который можно вписать одну или несколько инструкций для миграции схемы


from yoyo import step
steps = [
   step("CREATE TABLE foo (id INT, bar VARCHAR(20), PRIMARY KEY (id))",
        "DROP TABLE foo"),
]

После этого миграции можно катить на базу


yoyo apply --database postgresql://scott:tiger@localhost/db ./migrations

Все просто, как полено. И при этом у вас абсолютно полный контроль над тем, как выглядит БД.


Минусов у такого подхода два


  • За набором полей в таблицах и их параметрами придется следить ручками. Каждое изменение приводит к необходимости писать ALTER TABLE самому, теряется возможность мигрировать все в один клик.
  • Мерджить такие миграции тоже придется всегда руками и головой. Это, конечно, лишний труд. Но на практике конфликты и сложные мерджи миграций встречаются редко.

Peewee


image


Маленькая и не самая популярная ORM (хотя про нее писали здесь уже не один раз), которая, тем не менее, имеет свою аудиторию.
Peewee создана быть максимально тупой и простой оберткой для БД, с максимально понятным механизмом выполнения запросов и легко читаемым кодом.


Users.select().where(
   Users.user_id == user_id
).get()

Несмотря на простоту, минималистичность и малое количество кода, в peewee есть все, что нужно для вменяемой работы.


  • Адекватная производительность (хотя и не самая большая скорость выполнения запросов)
  • Все необходимые плюшки — различные наборы полей, связи между сущностями, пулы коннектов, наборы плагинов и расширений.
  • Есть даже более-менее вменяемая асинхронщина, добавляемая сторонним модулем peewee_async.

Pony ORM


Pony — легендарная своей скоростью обертка. Слой работы с БД, написанный с помощью этой ORM, может по скорости в разы надрать зад другим решениям. Магии в ее скорости нет, есть очень грамотная политика кеширования запросов в базы, кучи оптимизаций и хитрости с кодом. В сумме это приводит к тому что Пони жарит с конской скоростью.


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


query = select(c for c in Customer
               if sum(o.total_price for o in c.orders) > 1000)

Product.select().order_by(lambda p: desc(sum(p.order_items.quantity))).first()

Такой подход требует определенного слома мозга.


Tortoise ORM



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


Tortoise — относительно молодой проект, который пока что находится в стадии активной разработки. Хорошая производительность этой библиотеки объясняется тем, что ORM не содержит ниего лишнего и из коробки заточена под асинхронщину. А еще в ней предполагается использование uvloop, который работает быстрее, чем родные питонячие циклы эвентов.


ORM эта еще слишком сыра для применения в бою (например, пока даже не реализованы пулы коннектов), но посматривать за развитием этой либы стоит. Если у разработчиков все пойдет нормально — в ближайший год мы получим действительно быструю обертку для БД с хорошей скоростью и без странного синтаксиса в стиле Pony.

Комментарии (9)


  1. DSolodukhin
    25.07.2019 14:52

    Когда я последний раз смотрел Peewee, там не было встроенной поддержки One-to-Many, Many-to-Many. Т.е. в pony, например, можно сделать так:

    categories = Set('Category', table='item_category', column='category_id')
    

    И у вас есть готовое поле для связанной коллекции категорий. В peewee есть такое?

    В yoyo можно выполнить миграцию вне транзакции, столкнулся с такой необходимостью, но не могу найти в документации ничего на этот счет?


  1. evocatus
    25.07.2019 18:02

    А мне кажется, что синтаксис PonyORM как раз очень удобный. Как можно не любить генераторы и лямбы?


    1. evocatus
      25.07.2019 18:33

      P.S. Подскажите, кто знает — PonyORM продолжает развиваться? Потому что уже больше года как назад видел шикарную презентацию его создателей, где они грозились выпустить версию 0.8 с поддержкой миграций, но пока нет её.


      1. andreymal
        26.07.2019 13:42

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


  1. dikkini
    25.07.2019 23:55

    ORM — зло!


    1. EvokSinister
      26.07.2019 04:11

      ORM — это инструмент. А каждому инструменту нужно своё применение.


  1. MikiRobot
    26.07.2019 00:13

    Альтернатива yoyo: alembic.sqlalchemy.org использовать алхимию не обязательно.


  1. workbohdan
    26.07.2019 00:36

    Ребята! Подскажите, пожалуйста, проверенную в бою асинхронную ORM питоновскую для postgres. Хочу что-то такое попробовать, но не знаю на чем остановится: GINO, asyncpg, aiopg


  1. andreymal
    26.07.2019 13:34

    Минусом [Pony] можно назвать синтаксис запросов в этой ORM — он весьма специфический, с применением генераторов, лямбд и прочих языковых плюшек Питона.

    По-моему это наоборот большой плюс, которым я активно пользуюсь, так как позволяет писать по сути обычный питоновый код, который в большинстве случаев просто работает. Правда, pylint выпадает в осадок от таких лямбд