Валидация данных в SQLAlchemy с Pydantic: пример и оптимизация
При разработке backend-приложений на Python часто возникает задача валидации данных перед записью в базу данных. SQLAlchemy - мощный ORM, но он не предоставляет встроенных средств для кастомной проверки значений полей. Решение - использовать библиотеку Pydantic, которая позволяет задавать строгие правила для каждой модели данных. В этой статье разберём пример интеграции SQLAlchemy и Pydantic для модели студента, оценим корректность кода и предложим способы его оптимизации.
Проблема: отсутствие валидации в SQLAlchemy
Стандартные колонки SQLAlchemy (например, String(30)) ограничивают только длину хранимого значения, но не проверяют содержание. Например, имя студента не должно начинаться с цифры, а специальность должна входить в утверждённый список. Для таких сценариев нужна внешняя валидация.
Решение: модель Pydantic для проверки данных
Создадим класс StudentValidation на основе BaseModel из Pydantic. В нём определим поля и добавим кастомные валидаторы через декоратор @field_validator.
Пример модели Pydantic
class StudentValidation(BaseModel):
student_first_name: str
student_last_name: str | None
specialty: str
enrolment: str
@field_validator('student_first_name')
def validate_student_first_name(cls, value):
if len(value) > 50:
raise ValueError('Длина имени не должна превышать 50 символов.')
if value[0].isdigit():
raise ValueError(f'Имя не должно начинаться с цифры: {value}')
return value
@field_validator('student_last_name')
def validate_student_last_name(cls, value):
if len(value) > 50:
raise ValueError('Длина фамилии не должна превышать 50 символов.')
if value[0].isdigit():
raise ValueError(f'Фамилия не должна начинаться с цифры: {value}')
return value
@field_validator('specialty')
def validate_specialty(cls, value):
if value not in specialties:
raise ValueError(f'Приведенная специальность -> {value}, в то время как список допустимых -> {specialties}')
return valueМодель SQLAlchemy для таблицы students
Определим класс Student с колонками, соответствующими полям Pydantic-модели. Обратите внимание: типы в SQLAlchemy (например, String(30)) могут отличаться от ограничений в Pydantic (50 символов). Рекомендуется синхронизировать эти значения.
class Student(Base):
__tablename__ = 'students'
student_id: Mapped[int] = mapped_column(primary_key=True)
student_first_name: Mapped[str] = mapped_column(String(30))
student_last_name: Mapped[str | None] = mapped_column(String(30))
specialty: Mapped[str]
enrolment: Mapped[str]Функция добавления студента в БД
Создадим функцию insert_student, которая принимает экземпляр StudentValidation, преобразует его в объект SQLAlchemy и сохраняет в базе данных.
def insert_student(student: StudentValidation):
student_db = Student(
student_first_name=student.student_first_name,
student_last_name=student.student_last_name,
specialty=student.specialty,
enrolment=student.enrolment
)
with Session(engine) as connection:
connection.add(student_db)
connection.commit()
insert_student(StudentValidation(**data))Как улучшить код: советы по оптимизации
Представленный код корректен, но его можно сделать компактнее и надёжнее. Вот несколько рекомендаций:
- Синхронизируйте лимиты: если Pydantic проверяет длину до 50 символов, а колонка SQLAlchemy - до 30, данные пройдут валидацию, но не запишутся. Унифицируйте ограничения.
- Используйте конфигурацию модели Pydantic: вместо повторяющихся валидаторов для имени и фамилии создайте один общий метод или используйте
Annotatedтип. - Добавьте обработку ошибок: оберните вызов
insert_studentв try-except, чтобы корректно реагировать на исключения валидации или базы данных. - Разделите логику: вынесите преобразование модели в отдельный метод
to_orm()внутриStudentValidation.
Заключение
Интеграция SQLAlchemy и Pydantic - правильный и гибкий подход для валидации данных в Python-проектах. Ваш код работает, но его можно улучшить, следуя принципам DRY и синхронизируя ограничения. Используйте Pydantic для проверки бизнес-правил, а SQLAlchemy - для работы с базой данных. Это обеспечит чистоту и надёжность вашего backend-приложения.