Tortoise ORM - Простая асинхронная альтернатива SQLAlchemy

Tortoise ORM - Простая асинхронная альтернатива SQLAlchemy

Иван Ашихмин

Пост на сайте

Поддержать проект на Boosty

Поддержать проект в Telegram

Когда речь заходит об использовании ORM в Python, многие сразу вспоминают SQLAlchemy. Это мощный инструмент для работы с базами данных, но для новичков его порог входа может быть довольно высоким. Особенно когда дело доходит до асинхронных приложений или сложных моделей данных. Если вы ищете более простой, интуитивно понятный и лёгкий в освоении инструмент, обратите внимание на Tortoise ORM.

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

Установка

Tortoise ORM поддерживает различные базы данных, такие как PostgreSQL и MySQL. Для работы с PostgreSQL используйте версию tortoise-orm[asyncpg], а для MySQL — tortoise-orm[asyncmy].

# Для PostgreSQL 
pip install tortoise-orm[asyncpg]

# Для MySQL
pip install tortoise-orm[asyncmy]

Сравнение Tortoise ORM и SQLAlchemy

Для начала посмотрим, как отличаются подходы к созданию моделей и их использованию в Tortoise ORM и SQLAlchemy.

Модель в SQLAlchemy

Пример модели User, написанной с использованием SQLAlchemy:

import datetime 
from sqlalchemy import BigInteger, String, Integer, func
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

class Base(DeclarativeBase):
__abstract__ = True

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

class User(Base):
__tablename__ = "user"

telegram_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
first_name: Mapped[str] = mapped_column(String, nullable=False)
last_name: Mapped[str] = mapped_column(String, nullable=True)
username: Mapped[str] = mapped_column(String, nullable=True)
language_code: Mapped[str] = mapped_column(String, nullable=False)
created_at: Mapped[datetime.datetime] = mapped_column(server_default=func.now(), default=datetime.datetime.now)

Модель в Tortoise ORM

Теперь посмотрим, как будет выглядеть аналогичная модель в Tortoise ORM:

from tortoise.models import Model 
from tortoise import fields
import datetime

class User(Model):
id = fields.IntField(pk=True)
telegram_id = fields.BigIntField(null=False)
first_name = fields.CharField(max_length=255)
last_name = fields.CharField(max_length=255, null=True)
username = fields.CharField(max_length=255, null=True)
language_code = fields.CharField(max_length=10, null=False)
created_at = fields.DatetimeField(auto_now_add=True)

class Meta:
table = "user"

Обратите внимание, насколько синтаксис в Tortoise ORM проще. Модель создаётся как класс, наследуемый от Model, а поля данных задаются через типы из tortoise.fields. В "Мета-классе" указываем, как будет называться таблица модели. На этом этапе главное не начать думать, что работаешь в Django 😉.

Пример получения или создания записи

Теперь давайте сравним, как в SQLAlchemy и Tortoise ORM можно получить или создать запись пользователя.

Пример в SQLAlchemy

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

async def get_or_create_user(user: UserSchemaBaseInput) -> UserSchemaOutput: 
async with session_maker() as session:
query = select(User).where(user.telegram_id User.telegram_id)
result = await session.execute(query)
exist_user = result.scalar_one_or_none()
if exist_user:
return UserSchemaOutput.model_validate(obj=exist_user, from_attributes=True)

user_query = (
insert(User)
.values(**user.dict())
.returning(
User.id,
User.telegram_id,
User.first_name,
User.last_name,
User.username,
User.created_at,
User.language_code,
)
)
result = await session.execute(user_query)
await session.commit()
new_user = result.mappings().first()
return UserSchemaOutput.model_validate(obj=new_user, from_attributes=True)

Как видим, получается достаточно много действий.

Пример в Tortoise ORM

Теперь аналогичный пример в Tortoise ORM:

async def get_or_create_user(telegram_id: int, first_name: str, last_name: str, username: str, language_code: str): 
user, created = await User.get_or_create(
telegram_id=telegram_id,
defaults={
"first_name": first_name,
"last_name": last_name,
"username": username,
"language_code": language_code,
"created_at": datetime.datetime.now(),
}
)
return user, created

В Tortoise ORM этот процесс значительно проще: мы используем метод get_or_create, который выполняет поиск записи по указанным полям и создаёт новую, если такая запись не найдена. Здесь нет необходимости явно работать с сессиями или писать SQL-запросы — всё выполняется через ORM.

Использование Pydantic с Tortoise ORM

SQLAlchemy и Pydantic часто используются вместе для валидации и сериализации данных, но в Tortoise ORM нет строгой необходимости использовать Pydantic. Tortoise имеет встроенные методы для сериализации моделей, например, метод .to_dict().

Тем не менее, если вы уже используете Pydantic в вашем проекте, его можно легко интегрировать с Tortoise ORM. Важно отметить, что Tortoise ORM также имеет встроенную поддержку Pydantic через методы .model_validate() для создания и валидации схем данных.

Пример использования Pydantic с Tortoise ORM:

from pydantic import BaseModel 

class UserSchema(BaseModel):
telegram_id: int
first_name: str
last_name: str | None
username: str | None
language_code: str

async def get_user_schema(user_id: int) -> UserSchema:
user = await User.get(id=user_id)
return UserSchema.model_validate(user, from_attributes=True)

Таким образом, Tortoise ORM прекрасно сочетается с Pydantic, но также может обходиться и без него.

Заключение

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

Напишите в комментариях, стоит ли его применить в одном из будущих гайдов, для более конкретного примера использования?

Пост на сайте

Поддержать проект на Boosty

Поддержать проект в Telegram

Report Page