The Jerusalem PostNorthern Israel’s children need protected classrooms, not more promises - opinionESPN Deportes¿Cuánto ganaría Isaac del Toro por el título en Mundial de Ciclismo?ESPNTexas-Tennessee: The battle for UT, SEC orange and Manning loyaltyRTP DesportoRenato Veiga sobre Haaland. "É um excelente jogador mas não me tira o sono"PunchNigeria Open golf tournament returns in 2027 after 28 yearsInquirerPalace to Sara Duterte: Respond with evidence, stop using fallaciesBollywood HungamaEXCLUSIVE: Yash Raj Films bags overseas distribution rights of Sooraj Barjatya’s Yeh Prem Mol LiyaCollider‘SNL UK’ Cast Teases a Spicier Season 2 Inspired by the American OriginalAnime News NetworkMY HERO ACADEMIA: All's Justice Switch 2 Port Video Game ReviewVarietyHow To Watch Neil Young, Willie Nelson, Dave Matthews Band and More Perform Live at CNN’s ‘Farm Aid Music Festival’Observador DesportoIrlandês de 64 anos morre em praia não vigiada em AlvorDigital SpyEastEnders fans think they've worked out the next owners of The Vic
The Daily Newsstand · Free, Always
Saturday, September 26, 2026

Pony ORM: толстая пачка фич и улучшений

Translate

DISCLAIMER: Это неофициальный форк. Но, по моим сведениям, и в официальный pony эти фичи скоро подъедут в каком-то виде.

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

В pony добавилось:
- поддержка асинхронности
- поддержка миграций
- разнообразные улучшения из серии quality-of-life

Так что, фреймворк pony теперь чрезвычайно важен и сердит (что можно понять по этой картинке). Всё благодаря DeepSeek 4.1 Flash и Kimi K3 (последняя - умная и дорогая, она делала ревью).

Я думаю, проще всего проиллюстрировать новые фичи на каком-нибудь примере, который чуть сложнее, чем hello world.

Сначала нужно установить pony. Я сделал свой собственный форк poney, поэтому, чтобы его установить, нужно выполнить

uv add poney

Кстати, приятный побочный эффект моей деятельности - в том, что авторы pony, наконец, активизировали свои усилия по реализации давно назревших фич и планируют их зарелизить уже в скором будущем. Реализация, думаю, будет отличаться - потому что мы работали независимо. Тем не менее, очень вероятно, что скоро можно будет использовать pony из апстрима с примерно тем же результатом.

Итак, мы установили пони. Дальше, нужно создать Database:

from pony.orm import Database

db = Database(provider=‘postgres’, dsn=“dbname=poney user=postgres”)

Кстати, опции dsn раньше не было - мне её предложил добавить DeepSeek, когда я его попросил расширить возможности декларации сущностей (таблиц), сделав его по возможности поближе к чистому SQL (точнее, DDL). В числе прочего, он добавил и CHECK constraints:

from pony.orm import constraint


class Person(db.Entity):
    name = Required(str)
    age = Required(int)
    cars = Set('Car')

    @constraint.check
    def adults_only(self):
        return self.age > 18


class Car(db.Entity):
    make = Required(str)
    model = Required(str)
    owner = Required(Person)

В апстримном pony, чтобы пользоваться сущностями, нужно выполнить

db.generate_mapping(create_tables=True)

create_tables можно не передавать, если таблицы в базе уже созданы. Кстати, теперь у нас есть для этого миграции. Попробуем воспользоваться ими:

db.migrations.make()

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

pony migrations make --db models:db

Опцию --db нужно указать всего один раз: она запишется в файл config.ini в папке migrations. Итак, запускаем:

MigrationError:
  No applications are registered:
    migrations are per-application (db.application(...), see pony-apps);
    entities without an application are not covered by migrations

Ошибка: нам говорят, что миграции поддерживаются только для сущностей, которые принадлежат какому-то "приложению".

Идея "database applications" взята из django. Только я пошёл ещё дальше и сделал однозначное соответствие схема <-> приложение. Правда, не все базы данных поддерживают разные схемы внутри одной базы. Для тех баз, которые не поддерживают схемы, приложения - это логическая сущность, позволяющая группировать таблицы. Никакого префикса в названия таблиц, в этом случае, не добавляется - в отличие от django.

Но, если вы используете postgres, то ваши таблицы уже принадлежат каким-то схемам (по дефолту - public). Остаётся только указать этот факт явным образом:

database = Database(provider=‘postgres’,
                    dsn=“dbname=poney user=postgres”)
db = database.application('public')

Сгенерируем миграции:

Сгенерировался 0001_initial.sql. Выглядит он так:

CREATE TABLE "public"."person" (
  "id" SERIAL PRIMARY KEY,
  "name" TEXT NOT NULL,
  "age" INTEGER NOT NULL,
  CONSTRAINT "chk_person__adults_only" CHECK ("age" > 18)
);

CREATE TABLE "public"."car" (
  "id" SERIAL PRIMARY KEY,
  "make" TEXT NOT NULL,
  "model" TEXT NOT NULL,
  "owner" INTEGER NOT NULL
);

CREATE INDEX "idx_car__owner" ON "public"."car" ("owner");

ALTER TABLE "public"."car" ADD CONSTRAINT "fk_car__owner" FOREIGN KEY ("owner") REFERENCES "public"."person" ("id") ON DELETE CASCADE

Его можно применить командой apply. Если что, миграции схемы выполняются внутри DDL-транзакции.

MigrationError:
  No applications are registered:
    migrations are per-application (db.application(...), see pony-apps);
    entities without an application are not covered by migrations

Все миграции схемы - это обычный SQL. В целом, идея в том, чтобы было удобно использовать pony c уже имеющейся базой данных.

Кроме миграций схемы, ещё поддерживаются так называемые миграции данных. Это - питоновские скрипты. Миграцию данных можно создать так:

Вот что создалось:

# depends: 0001_initial.sql

from pony.orm import Database


db = Database.instance().new()
db.introspect()


if __name__ == '__main__':
    pass

Это обычный питоновский скрипт почти без какой-либо магии (вся магия - в том, что он выполняется внутри db_session). Разберём, что означает каждая строчка.

Первая строчка с комментарием определяет зависимости миграции. Зависимостей у миграции может быть сколько угодно. Воркфлоу с миграциями будет хорошо знаком тем, кто работал с django. Например, при наличии "нескольких голов" вас попросят сделать мердж-миграцию:

Да, это файл с расширением txt. Выглядит он примерно так:

-- depends: 0002_add_data.py, 0002_other_migration.py

      0003_merge.txt
             │
┌────────────┴─────────────┐
│                          │
0002_add_data.py           0002_other_migration.py
│                          │
└────────────┬─────────────┘
             │
     0001_initial.sql

Но вернёмся к миграции 0002_add_data.py. Следующей строчкой идёт

# depends: 0001_initial.sql

from pony.orm import Database


db = Database.instance().new()


if __name__ == '__main__':
    pass

Ну, почему там Database.instance() - ещё можно понять: миграции знают, где взять database. Но почему .new(), и потом .introspect()? Дело в том, что тот объект Database, который мы импортировали из нашего проекта, нам не подходит. Он слишком свежий, и миграции нужен не он, а его "снимок" из прошлого. В django, например, написано много кода для того, чтобы уметь восстанавливать этот "снимок" по файлам миграций. Я выбрал более простой путь - режим интроспекции.

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

# depends: 0001_initial.sql

from pony.orm import *

db = Database.instance().new()
public = db.application('public')

class Person(public.Entity):
    cars = Set('Car')

db.introspect()

me = public.Person(name='Vitalik', age=37)
car = public.Car(make='Toyota', model='Camry', owner=me)

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

Миграции данных удобно писать и отлаживать, например, в Jupyter ноутбуках:

Для того, чтобы увидеть, что мы получаем из базы интроспекцией, есть опция dump :

db = database.new()
db.introspect('public', dump='introspected.py')

В нашем случае, файл introspected.py выгляди так:

from pony.orm import *


class Car(db.Entity):
    id = PrimaryKey(int, auto=True, name='car_pkey')
    make = Required(str)
    model = Required(str)
    owner = Required('Person', index='idx_car__owner', reverse='car_set')

class Person(db.Entity):
    id = PrimaryKey(int, auto=True, name='person_pkey')
    name = Required(str)
    age = Required(int)
    car_set = Set('Car', reverse='owner')

car_set - это дефолтный атрибут для коллекции. В нашей миграции мы его переопредели на cars.

Это всё, что я хотел сказать о миграциях - и так получилось длинно. Теперь скажу пару слов об асинхронности. Поддерживаются postgres и mariadb/mysql. Оба драйвера позволяют работать с пулом соединений, чем мы и пользуемся. После использования для какой-либо сессии, соединение возвращается в пул.

Принцип очень простой: мы пишем async with db_session, и после этого обычные методы становятся асинхронными:

async with db_session:
    me = await Person[1]

То же самое касается методов get и select, а также .load() - подгрузки частично загруженного объекта:

await obj.load()

Методы flush и commit/rollback тоже становятся асинхронными. Операции записи остаются синхронными, потому что мы всё равно записываем в базу отложенным образом - в конце сессии.

Такой подход стал возможен, потому что API pony построен вокруг работы с сессиями. Поэтому, мы точно можем сказать, находимся мы внутри блокирующей или асинхронной сессии. В django так бы сделать не получилось.

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

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

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.