Наконец-то настало время заняться тем, что я уже давно планировал — создать подробный гайд по работе с асинхронной версией SQLAlchemy 2.0 в стиле ORM. Эта серия статей охватит абсолютно все аспекты: от моделей и связей между ними до миграций и методов взаимодействия с данными в базе.

Что планируется?

Я собираюсь написать несколько статей, которые будут сбалансированы между необходимой «скучной» теорией и практическими примерами, чтобы помочь каждому, кто уже знаком с Python, освоить этот «магический» фреймворк. И поверьте, вы скоро поймёте, что алхимия в названии фреймворка выбрана не случайно.

Тема на сегодня

Для начала, давайте разберёмся, что такое SQLAlchemy и почему каждый разработчик, работающий с реляционными базами данных (такими как SQLite, PostgreSQL, MySQL и т. д.), должен знать о ней. После этого — настройка. Мы будем работать с PostgreSQL, но не переживайте: код, который мы напишем, универсален для всех реляционных баз данных. Мы начнем с базовой настройки SQLAlchemy для асинхронного взаимодействия, а затем перейдём к созданию таблиц в современном декларативном стиле.

Модели таблиц

Сегодня мы научимся описывать модели таблиц и обсудим разные типы колонок — от распространенных (таких как Integer, String, Text) до специфических (Array, JSON).

Модели таблиц в SQLAlchemy — это своеобразные Python-классы, которые напрямую отображают структуру таблиц в базе данных. Каждая модель описывает определённую таблицу, её колонки и их типы данных. Модели позволяют вам работать с записями базы данных как с обычными объектами, что сильно упрощает взаимодействие с базой.

Сегодня мы разберём, как установить связи на уровне базы данных с помощью внешних ключей (ForeignKey), а также как задействовать более продвинутые связи в SQLAlchemy, которые значительно упрощают работу с данными благодаря использованию relationship. Начнем с базовых типов связей: "Один к одному" (1:1), "Один ко многим" (1:N) и "Многие к одному" (N:1).

Единственное, что пока оставим в стороне — это связи "Многие ко многим" (N:M) и индексы. Об этих темах мы поговорим позже, в следующих статьях.

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

Немного теории

Для тех, кто только знакомится с SQLAlchemy, важно понимать, что это не просто фреймворк, а полноценный инструмент для работы с реляционными (табличными) базами данных. Он поддерживает два стиля: core и ORM.

Core и ORM

  • Core — это низкоуровневый подход, который позволяет выполнять запросы с использованием SQL-выражений, обеспечивая полный контроль над процессом. Этот стиль подходит тем, кто хочет максимально приблизиться к стандартному SQL или имеет особые требования к производительности.

  • ORM (Object-Relational Mapping) — это стиль, в котором фреймворк отображает таблицы базы данных на Python-классы. С ORM вы работаете с объектами, а не со строками SQL. Именно на этот стиль мы будем ориентироваться, так как он более удобен, универсален и популярен среди разработчиков.

Основные компоненты ORM

Работа с SQLAlchemy в стиле ORM включает в себя несколько ключевых понятий:

  1. Модели таблиц — это Python-классы, представляющие таблицы базы данных. Эти классы содержат информацию о структуре таблиц, таких как колонки, типы данных и связи между таблицами.

  2. Сессии — объекты, через которые осуществляется взаимодействие с базой данных. Они позволяют выполнять запросы и фиксировать изменения. Сессия открывается в начале работы с базой и закрывается в конце, обеспечивая связь с базой данных на протяжении одного «сеанса».

  3. Фабрика сессий — это шаблон для создания сессий. Он используется для управления подключением к базе данных и создания новых сессий по мере необходимости.

Почему каждый Python-разработчик должен знать SQLAlchemy?

SQLAlchemy упрощает работу с базами данных, превращая их в интуитивно понятные объекты Python. Это делает ваш код более чистым и читаемым, поскольку вы пишете на Python, а не на SQL. Кроме того, он позволяет вам работать с разными базами данных практически без изменения кода — отличная возможность для тех, кто работает в командах, где требуются разные типы БД. SQLAlchemy также предлагает мощные инструменты для управления связями и миграциями, что делает его универсальным выбором для разработки крупных проектов.

Связи между таблицами

Как упоминалось ранее, SQLAlchemy позволяет устанавливать связи между таблицами через внешние ключи. Рассмотрим основные типы связей:

  • "Один к одному" (1:1) — используется, когда каждая запись в одной таблице должна соответствовать только одной записи в другой таблице. Например, профиль пользователя может быть связан с аккаунтом пользователя в соотношении один к одному.

  • "Один ко многим" (1:N) — при такой связи одна запись в одной таблице может соответствовать нескольким записям в другой таблице. Например, один пользователь может иметь несколько постов в блоге.

  • "Многие к одному" (N:1) — обратная связь "один ко многим". В этом случае несколько записей из одной таблицы могут ссылаться на одну запись в другой таблице, например, несколько комментариев могут быть привязаны к одному посту.

Такие связи позволяют вам строить сложные структуры данных и упрощают доступ к связанным данным.

Подготовим базу данных для работы

Для подготовки базы данных к работе на примере PostgreSQL у нас есть несколько вариантов. Давай разберем их:

Установка PGAdmin на компьютер

  1. Зайдите на официальный сайт PGAdmin и скачайте последнюю версию.

  2. Установите программу, не забудьте запомнить пароль, который придумаете при установке.

  3. После установки база данных PostgreSQL будет работать локально на вашем компьютере.

Используйте следующие параметры для подключения:

  • Хост: localhost

  • Порт: 5432

  • База данных: postgres

  • Пользователь: postgres

Этот метод предоставляет доступ к базе данных только на вашем компьютере.

Развертывание PostgreSQL через Docker-контейнер

Убедитесь, что на вашем компьютере установлен Docker и Docker Compose.

Следуйте инструкциям из этой статьи, чтобы поднять базу данных через Docker-compose.

При локальном запуске доступ будет только с вашего компьютера, а при запуске на VPS сервере можно настроить глобальный доступ.

Параметры для подключения будут те, которые вы указали при настройке Docker Compose-файла.

Развертывание PostgreSQL с помощью сервиса

Этот подход удобен тем, что все вопросы по настройке, оптимизации и запуску базы данных берет на себя хостинг-сервис. Одним из таких сервисов является Amvera Cloud.

Вам нужно просто зарегистрироваться, получить бонус в 111 рублей за регистрацию и за пару минут запустить PostgreSQL с внешним доступом. Далее я расскажу, как это сделать, так как сам выбираю этот способ.

Поднимаем PostgreSQL с внешним доступом на Amvera Cloud

  • Выполняем регистрацию в Amvera Cloud, если ее ещё не было

  • Переходим в раздел проектов

  • Жмем на «Создать проект». Далее даем имя проекту, выбираем тип сервиса «База данных» и выбираем тариф. Для учебных целей подойдет «Начальный».

Название не обязательно должно совпадать с именем базы данных.
Название не обязательно должно совпадать с именем базы данных.
  • На новом экране выбираем тип базы данных, даем имя базы данных, придумываем логин и пароль пользователя, подключаем суперпользователя и жмем на «завершить». Размер кластера для СУБД тоже ставим 1. Для учебных целей этого достаточно.

  • Открываем внешний доступ к базе данных. Для этого нужно перейти в созданный проект. Там переместиться на вкладку «Настройки». На открывшемся экране кликаем на «Добавить доменное имя». Тип подключения выбираем «POSTGRES». После кликаем на «Подтвердить и привязать».

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

Вот данные для подключения к базе данных:

ХОСТ — это адрес, который вы получили на последнем этапе.
ПОЛЬЗОВАТЕЛЬ — это либо пользователь, которого вы создали, либо супер-пользователь postgres.
ПАРОЛЬ — это пароль, который вы сами придумали.
БАЗА ДАННЫХ — это база данных, которую вы создали.

Если вы вдруг забудете какие-то параметры, то всегда можете открыть вкладку «Конфигурация» в вашем проекте и посмотреть нужную информацию.

Тестируем подключение к базе данных

Независимо от того какой тип подъема базы данных вы выбрали, перед тем как мы приступим к работе с SQLAlchemy, давайте выполним тестовое подключение к базе данных.

Для этого предлагаю установить бесплатную и удобную программу DBeaver Community. Она универсально работает со всеми табличными базами данных, в том числе и с PostgreSQL.

  • Скачиваем DBeaver Community под свою операционную систему

  • Запускаем

  • Кликаем на «Новое соединение» и в списке выбираем PostgreSQL

  • Указываем данные для подключения. Я беру данные полученные в Amvera Cloud

Соединение установлено успешно!
Соединение установлено успешно!
  • Затем нажмите на кнопку «Тест соединения». Если вы видите результат, как на скриншоте выше, то можете нажать «Готово». Соединение установлено успешно.

Подготавливаем проект

Теперь, когда PostgreSQL готов к работе, приступим к настройке проекта в IDE. Я выбираю PyCharm, но это может быть Atom, VSCode и другие редакторы.

  1. Создаем проект в PyCharm и активируем виртуальное окружение.

  2. Создаем файл .env для хранения переменных окружения для подключения к базе данных:

Пример:

DB_HOST=alchemsy-yakvenalex.db-msk0.amvera.tech
DB_PORT=5232
DB_NAME=adlchemydb
DB_USER=admin
DB_PASSWORD=dsakjjASSDkk

Создаем файл requirements.txt и добавляем необходимые библиотеки:

alembic==1.13.3
sqlalchemy==2.0.35
pydantic==2.9.2
pydantic-settings==2.5.2
asyncpg==0.29.0

Устанавливаем зависимости:

pip install -r requirements.txt

Краткое описание модулей

  • Alembic: Инструмент для управления и миграции схемы базы данных. Используется для внесения изменений в структуру базы данных (добавление, удаление, изменение таблиц и столбцов).

  • SQLAlchemy: Основная библиотека для работы с базами данных в Python, обеспечивающая ORM (объектно-реляционное отображение) и средства для выполнения SQL-запросов.

  • Pydantic: Библиотека для валидации данных и управления типами данных в Python. Pydantic позволяет удобно определять схемы данных и проверять их на соответствие типам. В будущем мы будем использовать эту библиотеку для трансформации данных из SQLAlchemy в привычные нам объекты, такие как словари.

  • Pydantic-settings: Дополнение к Pydantic, позволяющее легко управлять конфигурационными настройками приложений, используя Pydantic для валидации и типизации. Будем использовать для работы с переменными окружения, которые позволят нам подключаться к базе данных PostgreSQL.

  • Asyncpg: Асинхронный движок для работы с PostgreSQL через SQLAlchemy.

Создание базы данных

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

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

  • Таблица профилей — связь 1:1. У каждого пользователя может быть только один профиль.

  • Таблица постов — связь 1:N. Один пользователь может иметь множество постов, но каждый пост может быть написан только одним пользователем.

  • Таблица комментариев — связь 1:N. Один пользователь может иметь множество комментариев, и каждый комментарий может быть написан только одним пользователем.

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

Это должно дать вам хорошее понимание принципов работы SQLAlchemy и того, как строить модели для различных типов данных и связей. Время воплощать это в код!

Подготовим конфигурационный файл

В корне проекта я создам файл config.py. Его смысл в том, чтобы вывести в проект переменные окружения и сгенерировать ссылку на подключение к базе данных PostgreSQL. Для этих задач я буду использовать pydantic-settings.

Вот полный пример кода:

import os
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    DB_USER: str
    DB_PASSWORD: str
    DB_HOST: str
    DB_PORT: int
    DB_NAME: str
    
    # DATABASE_SQLITE = 'sqlite+aiosqlite:///data/db.sqlite3'
    model_config = SettingsConfigDict(
        env_file=os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env")
    )

    def get_db_url(self):
        return (f"postgresql+asyncpg://{self.DB_USER}:{self.DB_PASSWORD}@"
                f"{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}")

        
settings = Settings()

Тут мы создали класс Settings, поместив в него все переменные из файла .env. Кроме того, мы описали метод, который позволит генерировать ссылку для асинхронного подключения к базе данных PostgreSQL через SQLAlchemy.

Далее я назначил переменную settings, как объект класса Settings. Теперь у нас появилась возможность обращаться через точку к нужным нам методам и переменным. Пример ниже:

print("DB URL =>", settings.get_db_url())
print("DB HOST =>", settings.DB_HOST)

В классе я оставил пример ссылки для подключения к SQLite. Там для асинхронной работы используется движок aiosqlite.

Конфигурационный файл базы данных

Для работы с базой данных в SQLAlchemy обычно создается отдельный файл, где прописываются основные настройки. Мы назовем его database.py. Вот минимальная конфигурация:

from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.ext.asyncio import AsyncAttrs, async_sessionmaker, create_async_engine
from config import settings


DATABASE_URL = settings.get_db_url()

# Создаем асинхронный движок для работы с базой данных
engine = create_async_engine(url=DATABASE_URL)
# Создаем фабрику сессий для взаимодействия с базой данных
async_session_maker = async_sessionmaker(engine, expire_on_commit=False)


# Базовый класс для всех моделей
class Base(AsyncAttrs, DeclarativeBase):
    __abstract__ = True  # Класс абстрактный, чтобы не создавать отдельную таблицу для него

Описание конфигурации

  1. DeclarativeBase: Основной класс для всех моделей, от которого будут наследоваться все таблицы (модели таблиц). Эту особенность класса мы будем использовать неоднократно.

  2. AsyncAttrs: Позволяет создавать асинхронные модели, что улучшает производительность при работе с асинхронными операциями.

  3. create_async_engine: Функция, создающая асинхронный движок для соединения с базой данных по предоставленному URL.

  4. async_sessionmaker: Фабрика сессий для асинхронного взаимодействия с базой данных. Сессии используются для выполнения запросов и транзакций.

Как это работает

  • DATABASE_URL: Формируется с помощью метода get_db_url из файла конфигурации config.py. Содержит всю необходимую информацию для подключения к базе данных.

  • engine: Асинхронный движок, необходимый для выполнения операций с базой данных.

  • async_session_maker: Фабрика сессий, которая позволяет создавать сессии для взаимодействия с базой данных, управлять транзакциями и выполнять запросы.

  • Base: Абстрактный базовый класс для всех моделей, от которого будут наследоваться все таблицы. Он не создаст отдельную таблицу в базе данных, но предоставит базовую функциональность для всех других моделей.

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

Расширим код настроек

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

Мы опишем эти колонки только один раз, а создаваться они будут в каждой таблице. Кроме того, давайте создадим универсальный метод, который будет давать имена нашим таблицам по такой схеме: «имя модели таблицы» + «s» с переводом в нижний регистр.

Например, если у нас было имя модели User, то имя таблицы будет users.

Вот полный измененный код:

from datetime import datetime
from sqlalchemy import Integer, func
from sqlalchemy.orm import DeclarativeBase, declared_attr, Mapped, mapped_column
from sqlalchemy.ext.asyncio import AsyncAttrs, async_sessionmaker, create_async_engine

from config import settings

DATABASE_URL = settings.get_db_url()

# Создаем асинхронный движок для работы с базой данных
engine = create_async_engine(url=DATABASE_URL)
# Создаем фабрику сессий для взаимодействия с базой данных
async_session_maker = async_sessionmaker(engine, expire_on_commit=False)

# Базовый класс для всех моделей
class Base(AsyncAttrs, DeclarativeBase):
    __abstract__ = True  # Класс абстрактный, чтобы не создавать отдельную таблицу для него

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    created_at: Mapped[datetime] = mapped_column(server_default=func.now())
    updated_at: Mapped[datetime] = mapped_column(server_default=func.now(), onupdate=func.now())

    @declared_attr.directive
    def __tablename__(cls) -> str:
        return cls.__name__.lower() + 's'

В базовом классе Base определены три колонки (id, created_at, updated_at), которые будут добавляться ко всем моделям, унаследованным от Base:

  • id: Первичный ключ, который автоматически инкрементируется.

  • created_at: Дата и время создания записи, задаются автоматически.

  • updated_at: Дата и время последнего обновления записи, автоматически обновляется при каждом изменении.

Пока оставим этот файл как есть, но скоро к нему вернемся.

Наверняка вы заметили незнакомый синтаксис описания колонок таблицы, а именно некий Mapped и mapped_column. Давайте разберемся, что это и как нам с этим работать.

Mapped

Mapped — это современный способ аннотировать типы данных для колонок в моделях SQLAlchemy. Он позволяет более четко указать, что переменная представляет собой колонку таблицы в базе данных, делая код более читаемым и понятным.

Пример:

id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)

Здесь мы указываем, что id — это колонка типа int, которая будет использоваться как первичный ключ и автоматически инкрементироваться.

mapped_column

mapped_column — это функция, которая используется для создания колонок в моделях SQLAlchemy. Она принимает в качестве аргументов тип данных колонки и дополнительные параметры, такие как primary_key, nullable, default и так далее.

Пример:

created_at: Mapped[datetime] = mapped_column(server_default=func.now())

Здесь мы создаем колонку created_at, которая будет автоматически заполняться текущей датой и временем при создании записи.

Как это использовать

Используя Mapped и mapped_column, мы можем более лаконично и понятно описывать модели.

Пример для модели пользователя:

from sqlalchemy import Integer, String
from sqlalchemy.orm import Mapped, mapped_column


class User(Base):
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    name: Mapped[str] = mapped_column(String, nullable=False)
    email: Mapped[str] = mapped_column(String, unique=True, nullable=False)

В этом примере:

  • id: Целочисленный первичный ключ, который автоматически инкрементируется.

  • name: Строка, которая не может быть пустой (nullable=False).

  • email: Уникальная строка, которая также не может быть пустой.

Максимум гибкости и минимум кода

С Mapped и типовыми аннотациями мы можем значительно упростить описание моделей.

Пример:

from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.ext.asyncio import AsyncAttrs, DeclarativeBase


class Base(AsyncAttrs, DeclarativeBase):
    __abstract__ = True

    
class User(Base):
    __tablename__ = 'users'

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    name: Mapped[str]
    surname: Mapped[str | None]
    email: Mapped[str] = mapped_column(unique=True)

Что это значит:

  • name: Mapped[str]: Поле name обязательно (nullable=False по умолчанию), строкового типа. Нет необходимости явно указывать mapped_column(String, nullable=False).

  • surname: Mapped[str | None]: Поле surname необязательно (nullable=True), так как тип данных указывает на то, что оно может быть None. Нет необходимости явно указывать mapped_column(String, nullable=True).

Комбинирование:

Поле email показывает, как мы можем добавлять дополнительные параметры к mapped_column, если это необходимо.

Преимущества

  • Чистота кода: Упрощает и делает код более читаемым.

  • Явность типов: Обозначает тип данных сразу, что полезно для понимания структуры данных.

  • Гибкость: Позволяет легко управлять обязательностью полей.

Полный пример

Вот как можно описать полную модель пользователя с использованием этих принципов:

from sqlalchemy import String, Integer, DateTime, func
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.ext.asyncio import AsyncAttrs, DeclarativeBase


class Base(AsyncAttrs, DeclarativeBase):
    __abstract__ = True

    
class User(Base):
    __tablename__ = 'users'

    id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
    name: Mapped[str]
    surname: Mapped[str | None]
    email: Mapped[str] = mapped_column(unique=True)
    created_at: Mapped[DateTime] = mapped_column(server_default=func.now())
    updated_at: Mapped[DateTime] = mapped_column(server_default=func.now(), onupdate=func.now())
    

В дальнейшем покажу, как ещё можно упростить и сократить код с помощью Mapped.

Другими словами, если нужно что-то «стандартное», используем чистый Mapped, а если требуется более детальное описание — добавляем mapped_column.

Модель пользователей

Приступим к описанию моделей. Первой моделью мы опишем модель пользователей. Пока не будем подключать связи SQLAlchemy (relationship), чтобы не усложнять процесс написания кода.

Из «необычного» мы используем внешние ключи (ForeignKey), тем самым закладывая основу для будущих связей.

Что такое ForeignKey простыми словами

Просто говоря, ForeignKey (внешний ключ) – это способ связать одну таблицу с другой в базе данных. Представьте, что у вас есть две таблицы: пользователи и посты. Каждому посту нужен автор, так ведь?

Вот тут и вступает в игру ForeignKey. В таблице постов мы добавляем колонку, которая будет хранить ID пользователя из таблицы пользователей. Это позволяет SQLAlchemy (и базе данных) понять, кто автор каждого поста.

Пример кода:

from sqlalchemy import Integer, String, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.ext.asyncio import AsyncAttrs, DeclarativeBase


class Base(AsyncAttrs, DeclarativeBase):
    __abstract__ = True

    
class User(Base):
    __tablename__ = 'users'

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    name: Mapped[str]

    
class Post(Base):
    __tablename__ = 'posts'

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    title: Mapped[str]
    content: Mapped[Text]
    user_id: Mapped[int] = mapped_column(ForeignKey('users.id'))  # Внешний ключ

Это позволяет связать каждый пост с конкретным пользователем, что делает работу с данными гораздо удобнее и структурированнее. Далее, в этой связке, будет использоваться relationship — зависимость на стороне SQLAlchemy, которую мы тоже рассмотрим в дальнейшем.

Создадим файл models.py

Создадим в корне проекта файл models.py и опишем там первую модель:

from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column
from database import Base

class User(Base):
    username: Mapped[str] = mapped_column(unique=True)
    email: Mapped[str] = mapped_column(unique=True)
    password: Mapped[str]
    profile_id: Mapped[int | None] = mapped_column(ForeignKey('profiles.id'))

Обратите внимание, что мы не задали имя таблицы и не назначили первичный ключ (ID пользователя). Это нам и не требуется, так как класс Base автоматически добавит эти колонки при создании таблицы и назначит ей имя users.

Оптимизация кода с аннотациями

Мы также заметили, что строка Mapped[str] = mapped_column(unique=True) повторяется несколько раз. Чтобы оптимизировать этот процесс, мы можем воспользоваться аннотациями.

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

Для использования аннотаций необходимо из модуля typing импортировать объект Annotated. Аннотации я обычно описываю в файле database.py. Вот пример:

from typing import Annotated
from sqlalchemy import String
from sqlalchemy.orm import mapped_column


uniq_str_an = Annotated[str, mapped_column(unique=True)]

Описание аннотации

  • Annotated — это инструмент из модуля typing, который позволяет добавлять метаданные к типам данных. В данном случае мы используем его для описания колонки в SQLAlchemy.

  • str — это тип данных из Python, который указывает, что колонка будет строкового типа.

  • mapped_column(unique=True) — эта функция указывает, что колонка будет уникальной, то есть два значения в этой колонке не могут повторяться.

Пример использования аннотации:

from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column
from database import Base, uniq_str_an


class User(Base):
    username: Mapped[uniq_str_an]
    email: Mapped[uniq_str_an]
    password: Mapped[str]
    profile_id: Mapped[int | None] = mapped_column(ForeignKey('profiles.id'))

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

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

Таблица с профилем пользователя

Теперь опишем модель (таблицу) профиля пользователей. Тут мы включим несколько дополнительных типов данных для закрепления.

Предлагаю добавить следующие поля:

  • Имя: строка, обязательное поле

  • Фамилия: строка, не обязательное поле

  • Возраст: целое число

  • Пол: мужской / женский — сделаем чтоб срабатывало одно из двух значений (ENUM)

  • Профессия: одно из представленных значений (ENUM)

  • Интересы: список (Array) из строк

  • Контакты: JSON (питоновский словрь) с произвольным списком контактов.

Перед описанием самой модели, давайте подготовим набор значений для пола и для профессий. Для этого, предварительно, давайте импортируем enum (обычный enum, не из SQLAlchemy).

import enum

Enum из модуля enum в Python используется для создания перечислений, которые представляют собой набор именованных значений. Это позволяет определять типы данных с ограниченным набором возможных значений.

Перечисления для пола:

class GenderEnum(str, enum.Enum):
    MALE = "мужчина"
    FEMALE = "женщина"

Перечисления для профессий:

class ProfessionEnum(str, enum.Enum):
    DEVELOPER = "разработчик"
    DESIGNER = "дизайнер"
    MANAGER = "менеджер"
    TEACHER = "учитель"
    DOCTOR = "врач"
    ENGINEER = "инженер"
    MARKETER = "маркетолог"
    WRITER = "писатель"
    ARTIST = "художник"
    LAWYER = "юрист"
    SCIENTIST = "ученый"
    NURSE = "медсестра"
    UNEMPLOYED = "безработный"

Теперь опишем саму модель.

class Profile(Base):
    first_name: Mapped[str]
    last_name: Mapped[str | None]
    age: Mapped[int | None]
    gender: Mapped[GenderEnum]
    profession: Mapped[ProfessionEnum] = mapped_column(
        default=ProfessionEnum.DEVELOPER,
        server_default=text("'UNEMPLOYED'")
    )
    interests: Mapped[List[str] | None] = mapped_column(ARRAY(String))
    contacts: Mapped[dict | None] = mapped_column(JSON)

К сожалению, тип данных ARRAY и JSON пока не получается описать через чистый Mapped, поэтому тут использовал mapped_column. По остальным все будет корректно работать.

Использование параметров default и server_default

Вы, наверное, заметили, что для профессии мы использовали параметры default и server_default. Давайте детально разберем, чем они отличаются и в каких случаях их использовать.

  1. Параметр default:

    • Этот параметр задает значение по умолчанию на уровне приложения (SQLAlchemy).

    • Это означает, что если при создании объекта в коде значение для данного поля не указано, будет использовано значение, указанное в default. Например, при создании объекта класса User, если значение для поля profession не передано, SQLAlchemy автоматически подставит значение по умолчанию, указанное в default.

    • Пример: Если у нас есть перечисление (ENUM) профессий, то значение по умолчанию может быть выбрано через точку, например: ProfessionEnum.DEVELOPER.

  2. Параметр server_default:

    • Этот параметр задает значение по умолчанию на уровне базы данных.

    • Это значит, что если при вставке записи в таблицу значение для данного поля не указано, сама база данных подставит значение, указанное в server_default. В отличие от default, это значение применяется, если запись добавляется в таблицу напрямую, например, через SQL-запросы, минуя приложение.

    • Важно: Для использования этого параметра с ENUM, нужно передавать значение в виде текстового выражения с помощью метода text, который импортируется из SQLAlchemy. Значение ENUM указывается в кавычках как текст, например: "WRITER", а не само значение, такое как ProfessionEnum.WRITER. Это необходимо для корректного выполнения запроса на стороне базы данных.

Модель для постов

Теперь опишем простую модель для постов. Предварительно подготовлю модель со значениями для статуса публикации поста.

class StatusPost(str, enum.Enum):
    PUBLISHED = "опубликован"
    DELETED = "удален"
    UNDER_MODERATION = "на модерации"
    DRAFT = "черновик"
    SCHEDULED = "отложенная публикация"

Теперь опишем саму модель

class Post(Base):
    title: Mapped[str]
    content: Mapped[Text]
    main_photo_url: Mapped[str]
    photos_url: Mapped[List[str] | None] = mapped_column(ARRAY(String))
    status: Mapped[StatusPost] = mapped_column(default=StatusPost.PUBLISHED, server_default=text("'DRAFT'"))
    user_id: Mapped[int] = mapped_column(ForeignKey('users.id'))

Достаточно простая модель, которая будет содержать все обязательные поля: название, текстовый контент (предположим HTML-отформатированный текст), ссылка на главное фото, ссылки на дополнительные фото, статус публикации из набора и user_id для связки.

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

array_or_none_an = Annotated[List[str] | None, mapped_column(ARRAY(String))]

В нашем текущем коде перечисления (ENUMS) занимают много места. Давайте вынесем их в отдельный файл с названием sql_enums.py.

import enum


class GenderEnum(str, enum.Enum):
    MALE = "мужчина"
    FEMALE = "женщина"


class StatusPost(str, enum.Enum):
    PUBLISHED = "опубликован"
    DELETED = "удален"
    UNDER_MODERATION = "на модерации"
    DRAFT = "черновик"
    SCHEDULED = "отложенная публикация"


class ProfessionEnum(str, enum.Enum):
    DEVELOPER = "разработчик"
    DESIGNER = "дизайнер"
    MANAGER = "менеджер"
    TEACHER = "учитель"
    DOCTOR = "врач"
    ENGINEER = "инженер"
    MARKETER = "маркетолог"
    WRITER = "писатель"
    ARTIST = "художник"
    LAWYER = "юрист"
    SCIENTIST = "ученый"
    NURSE = "медсестра"
    UNEMPLOYED = "безработный"

Модель с комментариями

В этой модели нам необходимо будет связать комментарий, как с автором комментария, так и с постом, к которому этот комментарий был оставлен.

Кроме того, в качестве демонстрации давайте тут добавим колонку is_publish – булевое значение в формате опубликован комментарий или нет. Просто чтоб посмотреть как такие поля описываются.

И добавим рейтинг от 1 до 10, чтоб закрепить тему с ENUM.

Опишем ENUM

class RatingEnum(int, enum.Enum):
    ONE = 1
    TWO = 2
    THREE = 3
    FOUR = 4
    FIVE = 5
    SIX = 6
    SEVEN = 7
    EIGHT = 8
    NINE = 9
    TEN = 10

Теперь опишем саму модель:

class Comment(Base):
    content: Mapped[Text]
    user_id: Mapped[int] = mapped_column(ForeignKey('users.id'))
    post_id: Mapped[int] = mapped_column(ForeignKey('posts.id'))
    is_published: Mapped[bool] = mapped_column(default=True, server_default=text("'false'"))
    rating: Mapped[RatingEnum] = mapped_column(default=RatingEnum.FIVE, server_default=text("'SEVEN'"))

Если вы внимательно читали эту статью, то к настоящему моменту у вас должно быть полное представление о том, как описывать колонки с помощью ORM SQLAlchemy в современном стиле. Теперь мы перейдем к более сложной теме — описанию зависимостей между таблицами.

Описание зависимостей (relationship)

Здесь я постараюсь максимально подробно объяснить тему зависимостей, так как знаю, что новички часто испытывают сложности на этом этапе. Моя цель — помочь вам полностью понять, как описывать связи в моделях SQLAlchemy.

Один-к-одному (One-to-One)

1. Пример связи

У нас есть две модели: User и Profile. Связь между ними организована по принципу один-к-одному (One-to-One). Это значит, что каждый пользователь имеет только один профиль, и каждый профиль принадлежит только одному пользователю.

2. Почему используется One-to-One?

Обычно связь "один-к-одному" используется для разделения основной и дополнительной информации. Например, информация о пользователе (имя, email) может находиться в таблице User, а данные профиля (возраст, профессия, интересы) хранятся в отдельной таблице Profile. Это позволяет избежать излишней ширины основной таблицы, что упрощает её обслуживание и управление данными.

3. Техническая реализация

В SQLAlchemy технология relationship позволяет настраивать и управлять связями между таблицами. В случае связи один-к-одному между таблицами User и Profile настройка выглядит следующим образом:

Связь в модели User:

profile: Mapped["Profile"] = relationship(
    "Profile",
    back_populates="user",
    uselist=False,  # Ключевой параметр для связи один-к-одному
    lazy="joined"  # Автоматически подгружает profile при запросе user
)
  • back_populates="user" — указывает на атрибут обратной связи в модели Profile. Это значит, что при доступе к профилю можно также получить связанного пользователя.

  • uselist=False — определяет, что связь не является списком (по умолчанию relationship предполагает связь "один-ко-многим" или "многие-к-одному"). Здесь один профиль на одного пользователя, поэтому uselist=False задает связь один-к-одному.

  • lazy="joined" — задает стратегию подгрузки данных. SQLAlchemy выполнит JOIN запрос и подгрузит профиль сразу при запросе пользователя.

Обратная связь в модели Profile:

user: Mapped["User"] = relationship(
    "User",
    back_populates="profile",
    uselist=False
)
  • back_populates="profile" — связывает эту связь с атрибутом profile в модели User, обеспечивая двустороннее управление. Если связать пользователя и профиль, то оба атрибута (user и profile) будут обновляться одновременно.

  • uselist=False — так как у каждого профиля может быть только один пользователь, значение False указывает, что это отношение не поддерживает списки, а подразумевает один объект.

Как это работает?

Когда создается объект User с прикрепленным Profile, SQLAlchemy автоматически связывает их через определение связи. Например, если мы получаем пользователя через ORM-запрос select, SQLAlchemy выполнит JOIN-запрос и загрузит профиль, если используется joinedload для опции lazy='joined'. Это подробнее рассмотрим в следующей статье.

Связь Один-ко-Многим и Многие-к-Одному

1. Пример связи

В нашей модели User связан с моделью Post по принципу один-ко-многим: один пользователь может создавать много постов, но каждый пост принадлежит только одному пользователю. Эта связь также называется многие-к-одному со стороны Post, так как несколько постов могут ссылаться на одного пользователя.

Аналогично, модель Post связана с Comment по принципу один-ко-многим: каждый пост может иметь несколько комментариев, но каждый комментарий относится к конкретному посту.

2. Почему используется One-to-Many и Many-to-One?

Связь один-ко-многим или многие-к-одному используется, когда один объект (например, пользователь) связан с множеством других объектов (например, постов). Это позволяет логически сгруппировать данные и избежать избыточности. В нашем случае, каждый пользователь может иметь несколько постов, и каждый пост — несколько комментариев.

3. Техническая реализация

Связь один-ко-многим и многие-к-одному можно настроить с помощью relationship и ForeignKey.

Связь между User и Post:

class User(Base):
    # Поля пользователя...
    
    posts: Mapped[list["Post"]] = relationship(
        "Post",
        back_populates="user",
        cascade="all, delete-orphan"  # Удаляет посты при удалении пользователя
    )

class Post(Base):
    # Поля поста...
    
    user_id: Mapped[int] = mapped_column(ForeignKey('users.id'))
    
    user: Mapped["User"] = relationship(
        "User",
        back_populates="posts"
    )
  • user_id: В Post это внешний ключ, который связывает каждый пост с конкретным пользователем.

  • back_populates:

    1. back_populates="user" в Post указывает на связь с полем posts в User.

    2. back_populates="posts" в User связывает с полем user в Post. Эти настройки обеспечивают двустороннюю связь.

  • cascade="all, delete-orphan": Эта настройка в модели User указывает, что все посты, связанные с пользователем, должны быть удалены, если удаляется сам пользователь.

Связь между Post и Comment:

class Post(Base):
    # Поля поста...
    
    comments: Mapped[list["Comment"]] = relationship(
        "Comment",
        back_populates="post",
        cascade="all, delete-orphan"
    )

Здесь, благодаря relationship, можно связать объекты друг с другом при создании, и SQLAlchemy автоматически определит, к каким записям они относятся.

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

Написали? Отлично, значит можно открывать спойлере с полным кодом.

Скрытый текст
from sqlalchemy import ForeignKey, JSON, text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from database import Base, uniq_str_an, array_or_none_an, content_an
from sql_enums import GenderEnum, ProfessionEnum, StatusPost, RatingEnum


class User(Base):
    username: Mapped[uniq_str_an]
    email: Mapped[uniq_str_an]
    password: Mapped[str]
    profile_id: Mapped[int | None] = mapped_column(ForeignKey('profiles.id'))

    # Связь один-к-одному с Profile
    profile: Mapped["Profile"] = relationship(
        "Profile",
        back_populates="user",
        uselist=False,  # Обеспечивает связь один-к-одному
        lazy="joined"  # Автоматически загружает связанные данные из Profile при запросе User
    )

    # Связь один-ко-многим с Post
    posts: Mapped[list["Post"]] = relationship(
        "Post",
        back_populates="user",
        cascade="all, delete-orphan"  # При удалении User удаляются и связанные Post
    )

    # Связь один-ко-многим с Comment
    comments: Mapped[list["Comment"]] = relationship(
        "Comment",
        back_populates="user",
        cascade="all, delete-orphan"  # При удалении User удаляются и связанные Comment
    )


class Profile(Base):
    first_name: Mapped[str]
    last_name: Mapped[str | None]
    age: Mapped[int | None]
    gender: Mapped[GenderEnum]
    profession: Mapped[ProfessionEnum] = mapped_column(default=ProfessionEnum.DEVELOPER,
                                                       server_default=text("'UNEMPLOYED'"))
    interests: Mapped[array_or_none_an]
    contacts: Mapped[dict | None] = mapped_column(JSON)

    # Обратная связь один-к-одному с User
    user: Mapped["User"] = relationship(
        "User",
        back_populates="profile",
        uselist=False
    )


class Post(Base):
    title: Mapped[str]
    content: Mapped[content_an]
    main_photo_url: Mapped[str]
    photos_url: Mapped[array_or_none_an]
    status: Mapped[StatusPost] = mapped_column(
        default=StatusPost.PUBLISHED,
        server_default=text("'DRAFT'"),
        nullable=False
    )

    user_id: Mapped[int] = mapped_column(ForeignKey('users.id'))

    # Связь многие-к-одному с User
    user: Mapped["User"] = relationship(
        "User",
        back_populates="posts"
    )

    # Связь один-ко-многим с Comment
    comments: Mapped[list["Comment"]] = relationship(
        "Comment",
        back_populates="post",
        cascade="all, delete-orphan"
    )


class Comment(Base):
    content: Mapped[content_an]
    user_id: Mapped[int] = mapped_column(ForeignKey('users.id'))
    post_id: Mapped[int] = mapped_column(ForeignKey('posts.id'))
    is_published: Mapped[bool] = mapped_column(default=True, server_default=text("'false'"))
    rating: Mapped[RatingEnum] = mapped_column(default=RatingEnum.FIVE, server_default=text("'SEVEN'"))

    # Связь многие-к-одному с User
    user: Mapped["User"] = relationship(
        "User",
        back_populates="comments"
    )

    # Связь многие-к-одному с Post
    post: Mapped["Post"] = relationship(
        "Post",
        back_populates="comments"
    )

Реальное создание таблиц

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

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

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

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

alembic init -t async migration

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

Настройка migration/env.py для работы с базой данных

Для того чтобы Alembic мог корректно взаимодействовать с нашей базой данных, необходимо внести изменения в файл migration/env.py. Этот файл отвечает за конфигурацию миграций, и в нем нужно указать информацию для подключения к базе данных, а также импортировать все модели.

Шаги настройки

1. Импорт подключения и моделей

В первую очередь, импортируем ссылку для подключения к базе данных и базовый класс Base из файла database.py, а также все наши модели из файла models.py:

from database import Base, DATABASE_URL
from models import User, Comment, Post, Profile

2. Конфигурация подключения

Далее указываем Alembic, какой URL использовать для подключения к базе данных:

config = context.config
config.set_main_option("sqlalchemy.url", DATABASE_URL)

Здесь мы используем переменную DATABASE_URL, которая содержит строку подключения к нашей базе данных PostgreSQL.

3. Определение метаданных

Указываем Alembic, где искать информацию о моделях. Для этого присваиваем переменной target_metadata метаданные из Base, которые включают все модели:

target_metadata = Base.metadata

Эти шаги подготавливают файл env.py к работе с вашей базой данных. Когда мы задаем target_metadata, Alembic получает доступ к структуре всех наших моделей и использует их для создания или обновления схемы базы данных, добавляя новые таблицы и столбцы при изменении моделей. Таким образом, файл env.py становится связующим звеном между моделями SQLAlchemy и миграциями в Alembic, позволяя легко вносить и отслеживать изменения в базе данных.

Измененный код migration/env.py

import asyncio
from logging.config import fileConfig
from sqlalchemy import pool
from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config
from alembic import context
from database import Base, DATABASE_URL
from models import User, Comment, Post, Profile

config = context.config
config.set_main_option("sqlalchemy.url", DATABASE_URL)

if config.config_file_name is not None:
    fileConfig(config.config_file_name)

target_metadata = Base.metadata
# остальной код оставляем без изменений 

Подготовка файла миграций

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

alembic revision --autogenerate -m "Initial revision"

Эта команда создаст миграционный файл, который Alembic будет использовать для создания наших таблиц на основе моделей.

Если все было введено корректно, то вы увидите такое сообщение. Тут Alembic сообщает, что он обнаружил таблицы и затем говорит о том, что он сформировал файл с инструкциями для миграций по указанному адресу.

Если мы заглянем в этот файл, то увидим, что каждая колонка описана в более привычном для SQL CORE стиле. Это свидетельствует о том, что нам удалось четко объяснить Alembic, какие поля мы хотим получить на выходе.

Важные нюансы работы с Alembic и ENUM типами данных

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

1. Проблемы при создании колонки с ENUM

После того как тип данных ENUM создан в PostgreSQL, его имя резервируется и считается уникальным. При работе с Alembic эта особенность не всегда учитывается автоматически, что может привести к ошибкам в будущем.

Пример проблемы

Допустим, у нас есть такая колонка:

sa.Column('gender', sa.Enum('MALE', 'FEMALE', name='genderenum'), nullable=False)

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

Решение

Чтобы избежать этой проблемы, нужно явно указать параметр create_type=False, чтобы Alembic не пытался повторно создать ENUM-тип:

sa.Column('gender', sa.Enum('MALE', 'FEMALE', name='genderenum', create_type=False), nullable=False)

Совет: Всегда указывайте create_type=False для колонок с ENUM, чтобы избежать конфликтов при повторных миграциях.

2. Проблемы при откате миграций (downgrade)

При удалении таблиц с помощью Alembic таблицы удаляются, но связанные с ними ENUM-ы остаются в базе данных. Это может привести к конфликтам, если в будущем вы захотите использовать те же имена для новых ENUM-типов.

Решение

Чтобы Alembic корректно удалял типы ENUM при откате миграций, нужно расширить метод downgrade следующим образом:

def downgrade() -> None:
    # Удаление таблиц
    op.drop_table('comments')
    op.drop_table('posts')
    op.drop_table('users')
    op.drop_table('profiles')

    # Удаление типов ENUM
    op.execute('DROP TYPE IF EXISTS ratingenum')
    op.execute('DROP TYPE IF EXISTS genderenum')
    op.execute('DROP TYPE IF EXISTS professionenum')
    op.execute('DROP TYPE IF EXISTS statuspost')

В этом случае, при откате миграции, будут удалены не только таблицы, но и все соответствующие ENUM-типов.

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

Выполнение миграций

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

Обновление базы данных до последней версии миграции

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

alembic upgrade head

Эта команда применит все миграции, которые находятся в директории migrations/versions, и приведет структуру базы данных к актуальному состоянию. upgrade head обновит схему базы данных до самой последней версии, которая была создана и зарегистрирована в файлах миграций.

Выполнение миграции до конкретного ID

Если вам нужно обновить базу данных не до самой последней версии, а до конкретной миграции, можно указать идентификатор (ID) нужной миграции. Например:

alembic upgrade d97a9824423b

В данном случае d97a9824423b — это уникальный идентификатор нужной миграции. Alembic выполнит все миграции до указанного ID, обновив базу данных до состояния, соответствующего этой версии.

Выполняем миграцию

Я вижу, что все прошло успешно. Зайду в базу данных и выполню проверку.

Все таблицы на месте.
Все таблицы на месте.

Откат миграций: Downgrade

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

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

Откат на одну версию назад

Чтобы откатить миграцию на одну версию назад, используйте следующую команду:

alembic downgrade -1

Откат до конкретной миграции

Если вам нужно откатить базу данных до определенной миграции, укажите ID этой миграции:

alembic downgrade d97a9824423b

Эта команда вернет базу данных к состоянию, соответствующему миграции с ID d97a9824423b, и удалит все изменения, примененные после нее.

Пример downgrade() с учетом удаления типов ENUM

Вот как должен выглядеть метод downgrade после внесения всех необходимых правок:

def downgrade() -> None:
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_table('comments')
    op.drop_table('posts')
    op.drop_table('users')
    op.drop_table('profiles')
    # ### end Alembic commands ###

    # Удаление типов ENUM
    op.execute('DROP TYPE IF EXISTS ratingenum')
    op.execute('DROP TYPE IF EXISTS genderenum')
    op.execute('DROP TYPE IF EXISTS professionenum')
    op.execute('DROP TYPE IF EXISTS statuspost')

Разбор:

  1. Удаление таблиц: Команды op.drop_table() удаляют все указанные таблицы из базы данных. Это автоматически генерируется Alembic и удаляет таблицы на основе текущего состояния миграций.

  2. Удаление ENUM-типов: Мы добавляем команды для удаления типов ENUM, которые могут оставаться в базе данных даже после удаления связанных таблиц. Команда op.execute('DROP TYPE IF EXISTS ...') удаляет типы ENUM, если они существуют.

Проверка отката

После того как вы внесли правки и убедились, что все готово, можно выполнить откат:

  • Откат на одну версию назад:

    alembic downgrade -1
  • Откат до определенной миграции:

    alembic downgrade d97a9824423b

Этот процесс удалит как таблицы, так и связанные ENUM-типы, корректно возвращая базу данных в предыдущие состояния.

Мы не получили никаких ошибок и это значит, что все сработало как мы и планировали.
Мы не получили никаких ошибок и это значит, что все сработало как мы и планировали.

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

Заключение

Друзья, сегодня мы с вами глубоко погрузились в основы работы с SQLAlchemy. Мы обсудили настройку базы данных, создание таблиц, установление связей между ними, а также рассмотрели процесс миграций с помощью Alembic. Я понимаю, что объем материала получился довольно внушительным, и может быть сложно усвоить все с первого раза. Но не переживайте — практика поможет вам закрепить знания!

Я старался объяснить все максимально доступно, но лучший способ освоить эту тему — это продолжать работать с кодом. Не спешите, внимательно изучите все, о чем мы говорили, и не стесняйтесь обращаться к дополнительной информации. Особенно это касается работы со связями между таблицами — без их понимания будет сложно двигаться дальше.

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

Мой план состоит в том, чтобы сначала подробно разобрать все тонкости работы с асинхронной SQLAlchemy. Завершим этот мини-курс созданием полноценного проекта на FastAPI, где на практике применим все полученные знания.

Готовый проект, как и базу данных PostgreSQL, которую мы размещали в этой статье, вы сможете легко развернуть на Amvera Cloud всего за несколько минут — но об этом мы поговорим отдельно.

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

Полные исходники кода, использованного в этой статье, а также эксклюзивный контент, который я не публикую на Хабре, вы сможете найти в моем Telegram-канале «Легкий путь в Python». В канале уже почти 1000 участников, которые вместе изучают и обсуждают Python!

До скорого!

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


  1. IvanZaycev0717
    12.10.2024 07:20

    class GenderEnum(str, enum.Enum):

    Здесь можно не использовать множественное наследование. В стандартной библиотеке Enum есть класс StrEnum, также есть и IntEnum


    1. yakvenalex Автор
      12.10.2024 07:20

      Спасибо за замечание. Учту. А str, Enum и int, Enum не будет так-же работать?


      1. FelixMikson
        12.10.2024 07:20

        будет, но мы же о красоте и современном синтаксисе (до 3.11 нужно как в статье)


        1. yakvenalex Автор
          12.10.2024 07:20

          Принял)