EAV-каталог для доски объявлений на FastAPI и SQLModel: опыт разработки
При создании доски объявлений или каталога агрегатора рано или поздно возникает задача хранения характеристик, которые сильно различаются от категории к категории. У автомобиля это пробег, коробка передач и тип привода, у кроссовок — размер и сезон, а у сплит-системы — мощность и площадь охлаждения.

При создании доски объявлений или каталога агрегатора рано или поздно возникает задача хранения характеристик, которые сильно различаются от категории к категории. У автомобиля это пробег, коробка передач и тип привода, у кроссовок — размер и сезон, а у сплит-системы — мощность и площадь охлаждения. Традиционный подход с отдельными таблицами или колонками для каждого атрибута приводит к лавине миграций и сложной схеме. Команда сервиса «О’Полка» поделилась опытом внедрения паттерна Entity-Attribute-Value (EAV) на стеке FastAPI, SQLModel и PostgreSQL 16. Этот подход позволяет гибко управлять атрибутами без изменения схемы базы данных, что особенно важно для стартапов с быстро меняющимися требованиями.
Как устроен EAV-каталог в «О’Полке»
Автор проекта Дмитрий Бабчук описывает архитектуру, в которой каждая категория объявлений имеет свой набор атрибутов. Вместо того чтобы создавать отдельную таблицу для каждой категории, используются три основные сущности: категория, атрибут и значение атрибута. Категории хранятся в виде дерева с поддержкой вложенности через parentid. Атрибуты определяются для каждой категории и могут иметь различные типы: текст, число, список, булево значение. Значения атрибутов для конкретного объявления связываются с ним через внешние ключи.
В качестве ORM используется SQLModel, который объединяет возможности SQLAlchemy и Pydantic. Это позволяет описывать модели данных и одновременно получать валидацию схем. Для хранения дерева категорий применяется материализованный путь (materialized path), что упрощает запросы подкатегорий. Атрибуты категории хранятся в отдельной таблице с указанием типа и единицы измерения. Значения — в таблице с колонками attributeid, listingid и value, где value — текстовое поле, а тип преобразуется на уровне приложения.
Предыстория и контекст
До внедрения EAV команда рассматривала альтернативы: JSONB-поля в PostgreSQL и классическую нормализацию с отдельными таблицами. JSONB давал гибкость, но усложнял индексацию и валидацию на уровне базы. Отдельные таблицы для каждой категории быстро привели бы к десяткам и сотням таблиц, что усложнило бы поддержку и миграции. EAV был выбран как компромисс между гибкостью и производительностью, особенно с учётом того, что количество категорий в «О’Полке» превышает несколько сотен и постоянно растёт.
Какие проблемы решает EAV и где он «выстрелил»?
Главное преимущество EAV — возможность добавлять новые атрибуты без изменения схемы базы данных. Если в категории «Электроника» появляется новый тип разъёма, достаточно вставить запись в таблицу атрибутов, и все объявления этой категории смогут его использовать. Это особенно важно для стартапа, где требования к характеристикам меняются еженедельно. Кроме того, EAV упрощает фильтрацию: можно построить универсальный API, который принимает пары «атрибут-значение» и формирует SQL-запрос с JOIN.
Однако есть и обратная сторона. Производительность выборок страдает при большом количестве атрибутов: чтобы получить все характеристики объявления, требуется несколько JOIN. Команда решила эту проблему кэшированием на уровне приложения и использованием материализованных представлений для частых запросов. Также EAV усложняет обеспечение целостности данных: например, нельзя на уровне БД гарантировать, что для автомобиля обязательно указан пробег. Валидация перенесена на уровень Pydantic-схем.
Технические детали реализации
В боевом репозитории «О’Полки» модели выглядят следующим образом. Категория имеет поля id, name, parentid (nullable) и path — строка с материализованным путём (например, "1.3.7"). Атрибут содержит id, categoryid, name, type (enum: text, number, select, boolean), unit и options — список возможных значений для типа select. Значение атрибута (AttributeValue) хранит listingid, attributeid и value. Для ускорения поиска по значениям созданы индексы на attributeid и value.
Взаимодействие с EAV через FastAPI реализовано с помощью dependency injection. При создании объявления эндпоинт принимает JSON с categoryid и списком attributes: [{attributeid, value}]. Валидация проверяет, что все атрибуты принадлежат указанной категории и соответствуют типам. Затем в транзакции создаётся запись объявления и пакетно вставляются значения. Для чтения используется отдельный эндпоинт, который подгружает все атрибуты категории и их значения для объявления, формируя плоский словарь.
Как реализовать EAV на FastAPI и SQLModel?
Для реализации EAV на FastAPI и SQLModel необходимо определить модели Category, Attribute и AttributeValue. Категория использует materialized path для иерархии. Атрибут содержит тип и единицу измерения. Значение хранится в текстовом поле, а приведение типов выполняется на уровне приложения с помощью Pydantic. FastAPI эндпоинты принимают и возвращают данные через Pydantic схемы, что обеспечивает валидацию и документацию Swagger.
Кого затронет и как
Описанный подход будет полезен разработчикам, создающим каталоги с динамическими характеристиками: доски объявлений, интернет-магазины, агрегаторы товаров и услуг. Особенно актуален для стартапов и небольших команд, где скорость изменений высока, а ресурсы на поддержку сложных схем ограничены. Российские и СНГ-проекты, работающие с множеством категорий (например, «Юла», «Авито»), могут почерпнуть идеи для оптимизации своих систем, хотя в крупных проектах часто используют комбинацию EAV с JSONB и поисковыми индексами.
Для пользователей «О’Полки» внедрение EAV означает более точные фильтры и возможность указывать специфические характеристики, которые раньше не поддерживались. Например, для объявления о продаже квадрокоптера можно будет указать «максимальная скорость» и «время полёта», даже если этих полей не было при запуске категории.
Что будет дальше
Команда «О’Полки» планирует добавить поддержку единиц измерения с автоматической конвертацией (например, километры в мили) и расширить систему валидации для числовых диапазонов. Также в разработке — полнотекстовый поиск по значениям атрибутов с использованием GIN-индексов на JSONB-представлении. В перспективе EAV-слой будет дополнен кэширующим слоем на Redis для снижения нагрузки на базу данных.
Итог
EAV — мощный, но компромиссный паттерн, который оправдан в сценариях с большим количеством разнородных и часто меняющихся атрибутов. Опыт «О’Полки» показывает, что при грамотной реализации на FastAPI и SQLModel можно добиться приемлемой производительности и гибкости, необходимой для современного каталога. Разработчикам, стоящим перед аналогичным выбором, стоит внимательно оценить частоту изменений атрибутов и требования к скорости выборки — возможно, EAV станет оптимальным решением.