Программы
Три примера работы с SQL базой данных в Python — Pony ORM (бонус)

Три примера работы с SQL базой данных в Python — Pony ORM (бонус)

К заметкам про работу с базой данных из sqlite3, sqlalchemy.Table и sqlalchemy.orm решил добавить и заметку про Pony ORM — крутую, но несколько эзотерическую ORM для Python.

В предыдущих заметках рассмотрены 3 примера работы с БД из Python:

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

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

В этой заметке я также добавил пользователей - владельцев фотографий для демонстрации работы со связями типа один-ко-многим: у одного пользователя может быть много фотографий, а у фотографии только один владелец.

Работа с SQL базой данных в Python через Pony ORM

Опять же, sqlite3 и sqlalchemy.Table заставляют программиста писать логику запросов исходя из синтаксиса SQL, при этом создание запросов выносится в какой-то дополнительный уровень абстракции. То есть, чтобы изолировать логику работы с базой данных от основного кода с бизнес-логикой, нам потребуется ещё одна логическая единица (модуль, класс, или же набор функций).

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

Что же мы имеем при работе с PonyORM? Здесь нечто "посередине". Можем делать ORM как в sqlalchemy.orm, а можем делать питоняшную магию, которая будет преобразована в запросы. И раз уж это "пони" - "дружбо-магию"...

И для начала определим модели данных:

    from pony import orm


    db = orm.Database()


    class User(db.Entity):
        id = orm.PrimaryKey(int, auto=True)
        name = orm.Required(str)
        photos = orm.Set('Photo')
        age = orm.Required(int)


    class Photo(db.Entity):
        id = orm.PrimaryKey(int, auto=True)
        url = orm.Required(str)
        owner = orm.Required(User)
        tags = orm.Set('Tag')


    class Tag(db.Entity):
        id = orm.PrimaryKey(int, auto=True)
        name = orm.Required(str)
        photos = orm.Set(Photo)


    db.bind(provider='sqlite', filename='example.db', create_db=True)
    db.generate_mapping(create_tables=True)
    orm.set_sql_debug(True)

Как видите, код по структуре похож на sqlalchemy.orm, однако, менее многословен.

Также при описании типов данных мы во многом полагаемся на Python типы данных. Даже связь фотографий и пользователей мы делаем через orm.Set, который во многом повторяет интерфейс типа множества, встроенного в Python. Однако, это лишь начало, ведь в запросах раскроется ещё больше питоняшности и дружбо-магии:

    @orm.db_session
    def fill_data():
        u1 = User(name='Alice', age=17)
        u2 = User(name='Bob', age=18)
        p1 = Photo(url='https://900913.ru/static/img/logo-32x32.png', owner=u1)
        p2 = Photo(url='https://900913.ru/static/img/logo-192x192.png', owner=u2)
        p3 = Photo(url='https://900913.ru/static/img/9cover.jpg', owner=u2)
        orm.commit()

        print(orm.select(p.url for p in Photo if p.owner == u1)[:10])
        print([p.url for p in u2.photos])
        print(orm.select(u.name for u in User if u.age >= 18)[:10])

И если до этого мы создавали сессию как контекстный менеджер, в этом случае мы используем для этого декоратор. Мы также могли это сделать в виде контекстного менеджера:

    with orm.db_session:
    def fill_data():
        u1 = User(name='Alice', age=17)
        u2 = User(name='Bob', age=18)
        ...

но в этот раз давайте исходить из того, что функция - это отдельное атомарное действие, поэтому используем декоратор. Запросы к базе данных, как можно заметить, мы делаем не через session.query, а в более привычном виде: вот создали пользователей, вот фотографии...

Единственное, всё также стоит не забывать про orm.commit() / orm.rollback().

Пользователей мы привязали к фотографиям при создании. И сделали запрос на получение фотографий пользователя u1.

    print(orm.select(p.url for p in Photo if p.owner == u1)[:10])

При чём, заметьте, на сколько питоняшно это сделано - мы передали orm.select Python-генератор, по которому Pony построил запрос. И при получении среза (слайса) [:10] PonyORM выполнит этот запрос со здвигом 0, лимитом 10.

А фотографии по пользователю u2 мы вообще получаем из кеша PonyORM. Если бы мы не только что его создали, то есть кеш был бы непрогрет, то это был бы ещё один SQL-запрос:

    print([p.url for p in u2.photos])

Теперь пример со связью многие-ко-многим, аналогичный предыдущим из примеров sqlite3, sqlalchemy:

    @orm.db_session
    def create_select_m2m():
        t1 = Tag(name='sql')
        t2 = Tag(name='linux')
        t3 = Tag(name='python')

        for p in orm.select(x for x in Photo):
            if p.id % 2:
                p.tags.add(t1)
                p.tags.add(t2)
            else:
                p.tags.add(t2)

        print(orm.select(p.url for p in Photo if t1 in p.tags)[:])

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

    for p in orm.select(x for x in Photo):
        ...

то есть по запросу можно и итерироваться как по обычному генератору (что также очень в стиле Python).

И для каждого нечётного тега добавим 2 тега, а для чётного - только 1. Никакой логики, лишь для простоты наполнения базы данных связями тегов и фотографий.

Также заметьте, что добавление происходит как и с обычным питоновским множеством - через .add.

Далее - запрос на получение всех фотографий, отмеченных указанным тегом:

    print(orm.select(p.url for p in Photo if t1 in p.tags)[:])

"Под капотом" всё также произойдёт LEFT JOIN, но посмотрите - на сколько об этом нет ни намёка. Лишь Python-код...

Или даже так:

    print(orm.select((p.url, len(p.tags.id)) for p in Photo if len(p.tags) > 1)[:])

Опять же, на чистом Python мы делаем запрос на фотографии, у которых более 1 метки. В итоге - HAVING COUNT, о котором нет ни слова.

В общем, очень любопытная ORM с точки зрения Python дзен!

Также может быть вам интересно:

Три примера работы с SQL базой данных в Python (sqlalchemy.orm)

Многие приложения на Python используют базу данных для хранения и эффективного использования их в своей работе. В этот раз на примере sqlalchemy.orm

Читать »

Примеры работы с NoSQL базой данных Redis из Python

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

Читать »
Фото Как сделать свою middleware в Django (с примерами)

Как сделать свою middleware в Django (с примерами)

Middleware или "промежуточное программное обеспечение" - элегантный способ установить общие правила обработки запросов и ответов приложения. Давайте напишем парочку middleware, чтобы понять, как они работают.

Фото Как настроить отправку почты из Django

Как настроить отправку почты из Django

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

Фото Добавляем поддержку медиа-файлов в Django проект

Добавляем поддержку медиа-файлов в Django проект

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

Фото Настройка журналирования (логирования) в Python с примерами

Настройка журналирования (логирования) в Python с примерами

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

Фото Нет слов, одни... однострочники

Нет слов, одни... однострочники

На днях вышел пост со списком полезных однострочников для JavaScript программистов. Памятуя Perl-овую молодость, заглянул туда.

Фото Добавляем переменные в контекст Django шаблонов (свой контекст-процессор)

Добавляем переменные в контекст Django шаблонов (свой контекст-процессор)

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

Фото Пример своей консольной команды в Django проекте

Пример своей консольной команды в Django проекте

Если вы работали с Django проектом, то, скорее всего, запускали команды из консоли (manage.py). В Django есть простой способ писать свои команды для управления проектом.

Фото Разграничение прав доступа на Django сайте

Разграничение прав доступа на Django сайте

Почти на любом веб-сайте необходимо разделять пользователей на группы и предоставлять им разные возможности. В Django есть довольно серьёзная система прав доступа для пользователей - давайте её рассмотрим!