FastAPI 4. Модель пользователя и Alembic
В этом посте начнём работу по системе пользователей в проекте. Опишем модели базы данных и инициализируем Alembic для создания миграций.
Реклама
Практически каждому проекту требуется модель пользователя — для регистрации, авторизации и работы с профилем. Наш проект не станет исключением.
Изначально я планировал использовать готовую библиотеку FastAPI Users
. Такие решения действительно упрощают разработку, но они не всегда помогают глубже понять, как работает система. Кроме того, у готовых библиотек могут возникать проблемы с совместимостью, задержки в поддержке новых версий фреймворков, а также ограниченные возможности для кастомизации.
Поэтому в рамках текущей серии статей мы создадим собственную систему пользователей. В нескольких следующих постах мы разберём процесс создания модели пользователя, регистрации, авторизации и другие важные аспекты.
Базовая модель
Начнём не с модели пользователя, а с создания базовой модели, от которой будут наследоваться все остальные. Это позволит нам избежать дублирования кода и организовать работу с базой данных более эффективно.
Если вы работали с моделями в Django, то знаете, что при создании модели не нужно явно указывать первичный ключ, если его не требуется настраивать — это поле автоматически добавляет базовая модель. Мы реализуем аналогичный подход, добавив свою базовую модель с полем первичного ключа.
В пакете core
создадим подкаталог models
, а в нём файл base.py
.
Теперь создадим класс Base
, который будет унаследован от DeclarativeBase
из SQLAlchemy. Этот класс предназначен для декларативного стиля работы с таблицами базы данных, что означает, что мы описываем таблицы в виде Python-классов.
Поля класса
В классе Base
будет два ключевых элемента:
- Абстрактный класс:
Мы добавим атрибут__abstract__ = True
, чтобы указать SQLAlchemy, что этот класс не должен использоваться для создания таблицы в базе данных. Абстрактные классы используются как шаблоны для других моделей, которые наследуются от них. - Поле id:
Полеid
будет служить уникальным идентификатором для каждой записи в таблице. Оно будет иметь тип данныхMapped[UUID]
. Вот что это значит:
Mapped
— это специальный тип SQLAlchemy, который связывает поле класса с соответствующим столбцом в таблице базы данных.UUID
— тип данных, представляющий собой универсальный уникальный идентификатор.
Как работает UUID
?
UUID (Universally Unique Identifier) — это стандарт для создания уникальных идентификаторов. Одной из самых популярных версий UUID является версия 4 (UUIDv4), которая генерируется случайным образом, используя криптографически безопасные методы. Это гарантирует уникальность каждого идентификатора даже при создании на разных устройствах или в разное время.
Использование mapped_column
Поле id
будет объявлено через функцию mapped_column
. Эта функция связывает атрибут класса с конкретным столбцом в базе данных. Мы укажем следующие аргументы:
- Тип данных
UUID(as_uuid=True)
, который будет хранить значение UUID в виде строки. primary_key=True
укажет, что это поле будет основным ключом таблицы, т.е. уникальным идентификатором.- Значение по умолчанию —
uuid.uuid4
, которое автоматически сгенерирует уникальный UUID для каждой новой записи.
Метод as_dict
Для удобства мы добавим метод as_dict
, который преобразует объект модели в словарь. Это полезно, когда нужно передавать данные в JSON или другие форматы.
Код класса:
import uuid
from sqlalchemy import UUID
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
__abstract__ = True
id: Mapped[UUID] = mapped_column(
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
)
def as_dict(self):
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
Модель пользователя
Теперь создадим саму модель пользователя. В пакете models
, где мы уже разместили базовую модель, создадим новый файл user.py
. В этом файле опишем класс User
, который будет унаследован от базовой модели Base
.
Поля модели
Модель пользователя будет включать семь ключевых полей, каждое из которых отвечает за различные аспекты работы с пользователем:
__tablename__
:- Это специальный
dunder-атрибут
, который указывает SQLAlchemy, как будет называться таблица в базе данных. В данном случае имя таблицы —user
.
- Это специальный
email
:- Поле для хранения электронной почты пользователя. Оно имеет строковый тип данных с максимальной длиной 100 символов.
- Это поле должно быть уникальным (
unique=True
), так как каждому пользователю присваивается свой уникальный email, и обязательным (nullable=False
), чтобы предотвратить создание записи без email.
password
:- Поле для хранения пароля пользователя в хешированном виде. Используем текстовый тип данных (
Text
), так как хеш пароля может быть достаточно длинным. - Это поле не уникально (
unique=False
), поскольку два пользователя могут иметь одинаковые пароли (но с разными email), и обязательно (nullable=False
).
- Поле для хранения пароля пользователя в хешированном виде. Используем текстовый тип данных (
is_active
:- Булево поле, указывающее, активен ли пользователь или его аккаунт заблокирован. Это полезно для управления доступом.
- По умолчанию значение —
False
, так как новый пользователь считается неактивным, пока, не подтвердит регистрацию.
is_superuser
:- Ещё одно булево поле, указывающее, является ли пользователь администратором или обладает ли он расширенными правами доступа.
- По умолчанию значение —
False
.
is_verified
:- Булево поле, которое определяет, подтвердил ли пользователь свою электронную почту после регистрации. Это полезно для предотвращения фейковых регистраций и спама.
- По умолчанию значение —
False
, что означает, что пользователь считается неподтверждённым, пока не выполнит верификацию.
created_at
:- Поле, которое хранит дату и время регистрации пользователя. Оно автоматически заполняется текущей датой и временем при создании записи.
- Для этого используется комбинация
server_default=func.now()
(для работы на уровне базы данных) иdefault=datetime.datetime.now
(для работы на уровне приложения).
Код модели:
import datetime
from sqlalchemy import String, Boolean, func, Text
from sqlalchemy.orm import Mapped, mapped_column
from lkeep.core.models.base import Base
class User(Base):
__tablename__ = "user"
email: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
password: Mapped[str] = mapped_column(Text, unique=False, nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
is_superuser: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
is_verified: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
created_at: Mapped[datetime.datetime] = mapped_column(
server_default=func.now(), default=datetime.datetime.now
)
Видимость моделей для Alembic
Чтобы система миграций Alembic могла "увидеть" наши модели и корректно работать с ними при создании миграций, необходимо выполнить определённые действия. Это важный шаг, так как без него Alembic не сможет автоматически отслеживать изменения в моделях базы данных.
Добавление __all__
и импортов
- Открываем
__init__.py
в пакетеmodels
:- Этот файл нужен для того, чтобы Python воспринимал директорию как пакет. В данном случае мы будем использовать его для экспорта наших моделей, чтобы они стали доступными для Alembic.
- Добавляем
dunder-атрибут
__all__
:- Атрибут
__all__
— это специальная конструкция, которая указывает, какие имена (модели) должны экспортироваться, когда кто-то выполняет импорт всех объектов из пакета с помощью конструкцииfrom models import *
. - Мы перечислим все модели, которые должны быть доступны для Alembic.
- Атрибут
- Импорты моделей:
- После
__all__
нужно явно импортировать все модели, которые мы перечислили, чтобы Alembic мог работать с ними при генерации миграций.
- После
Пример кода:
from lkeep.core.models.base import Base
from lkeep.core.models.user import User
__all__ = ("Base", "User")
Теперь при создании миграций Alembic сможет видеть наши модели и корректно отслеживать изменения в базе данных.
Установка и инициализация Alembic
Для работы с базой данных и управления её миграциями мы будем использовать Alembic — инструмент для создания и применения изменений в структуре базы данных. Это удобно, так как не требует вручную изменять существующие таблицы или записывать их в отдельных скриптах. Процесс работы Alembic схож с миграциями в Django, что делает его понятным для тех, кто уже знаком с этим фреймворком.
Установка Alembic
Чтобы установить Alembic в проект, выполним следующую команду:
poetry add alembic black
alembic
— сам инструмент для создания и применения миграций.black
— дополнительный инструмент для автоматического форматирования кода.
Инициализация Alembic
После установки нужно инициализировать Alembic в проекте. Для этого выполняем следующую команду:
alembic init --template async alembic
Здесь мы указываем:
--template async
— указывает на использование асинхронного шаблона для работы с базой данных, что важно для проектов на FastAPI.alembic
— имя директории, где будут храниться настройки и файлы миграций.
Результат
После выполнения команды в корне вашего проекта появятся:
- Директория
alembic
— в ней будут храниться все миграции и файлы конфигурации. - Файл
alembic.ini
— основной конфигурационный файл для Alembic, в котором можно настроить параметры миграций.
Настройка Alembic
После инициализации Alembic необходимо внести изменения в конфигурацию, чтобы корректно подключиться к базе данных и настроить форматирование миграций.
Шаг 1: Изменение alembic.ini
Откроем файл alembic.ini
. В нем потребуется раскомментировать следующие строки:
- 9-я строка — задаёт шаблон имени файлов для миграций:
# было
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# стало
file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
- Строки 70-73 — настройки для инструмента форматирования кода
black
. Они обеспечат автоматическое форматирование всех файлов миграций:
# было
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# стало
hooks = black
black.type = console_scripts
black.entrypoint = black
black.options = -l 79 REVISION_SCRIPT_FILENAME
Важно: будьте внимательны с пробелами! В начале строк не должно быть лишних пробелов.
Также закомментируем 61-ю строку, где указывается URL для подключения к базе данных:
# было
sqlalchemy.url = driver://user:pass@localhost/dbname
# стало
# sqlalchemy.url = driver://user:pass@localhost/dbname
В дальнейшем мы зададим свои параметры подключения.
Шаг 2: Изменение env.py
Теперь откроем файл env.py
, который находится в директории alembic
.
- Найдите строку
config = context.config
и после неё добавьте следующее:
from lkeep.core.settings import settings
config.set_main_option("sqlalchemy.url", settings.db_settings.db_url.unicode_string())
Этот код указывает Alembic, где взять URL для подключения к базе данных.
- Далее найдите строку
target_metadata = None
и измените её на:
from lkeep.core.models.base import Base
target_metadata = Base.metadata
Это позволит Alembic собирать все модели, унаследованные от базовой модели Base
, для создания таблиц в базе данных.
Первые миграции
Теперь давайте проверим, корректно ли настроено подключение к базе данных и создание миграций.
Шаг 1: Создание миграции
Для этого откроем терминал и выполним команду:
alembic revision --autogenerate -m "Create User Table"
- Флаг
--autogenerate
указывает Alembic автоматически сгенерировать файл миграции на основе изменений в моделях. - Флаг
-m
и строка после него используются для добавления описания миграции (по аналогии с описаниями коммитов в Git).
После выполнения команды, в директории versions
должен появиться новый файл миграции. Откройте его и проверьте, все ли поля созданы корректно. Это важный шаг! Всегда проверяйте сгенерированные файлы миграций, чтобы убедиться, что автоматическая генерация не пропустила что-либо важное.
Шаг 2: Применение миграции
После того как вы убедились, что файл миграции создан корректно, применим его командой:
alembic upgrade head
Эта команда обновит базу данных до самой последней версии миграции, то есть выполнит все необходимые изменения.
Шаг 3: Проверка
Теперь можно подключиться к базе данных и проверить, создались ли новые таблицы и поля. Если всё прошло успешно, вы увидите таблицы, которые были описаны в моделях.
Заключение
В этом посте мы создали базовую модель, модель пользователя, настроили и применили миграции с использованием Alembic. Это лишь начало работы с системой пользователей и аутентификации, и впереди нас ждёт ещё множество интересных задач.
P.S. Мы также установили библиотеку black
для автоматического форматирования файлов миграций. Вы можете запускать его вручную, выполнив в терминале команду:
black .
Эта команда проверит все .py
-файлы в текущей директории на соответствие стандартам PEP-8 и при необходимости отформатирует их. Если хотите, вы можете указать конкретную директорию или файл вместо точки, чтобы сузить область проверки. Это хороший способ поддерживать код в порядке и избегать проблем с форматированием.
Все статьи