ProxyOrmModel — ORM-подход к работе с данными в Qt
Привет, Хабр! В этой статье я хочу рассказать о своём проекте — библиотеке ProxyOrmModel для Qt, которая упрощает работу с данными в моделях. Если вы когда-нибудь сталкивались с необходимостью фильтровать, сортировать, группировать или агрегировать данные в QAbstractItemModel, то, вероятно, знаете, как это может быть утомительно. Я решил создать инструмент, который делает это проще и удобнее, вдохновившись идеями ORM (Object-Relational Mapping) из мира баз данных. Здесь я поделюсь архитектурой, ключевыми классами и уроками, которые я вынес из разработки.

Идея и мотивация
Всё началось с того, что в одном из проектов я решил отделить модели для обработки логикой и модели для представлений (qml). Для представлений мне необходимо объединять данные из нескольких моделей, вычислять суммы и средние значения, а также реализовывать сложные условия вроде SQL CASE WHEN. Стандартные прокси-модели Qt (QSortFilterProxyModel) не покрывали всех моих задач, а добавление join в саму модель делает ее громоздкой, трудно поддерживаемой и перегружает сущности лишними полями. Я решил создать универсальную прокси-модель, которая:
Поддерживает фильтрацию (WHERE) — надоело писать filterAcceptRow каждый раз
Позволяет сортировать и группировать данные
Выполняет агрегацию (например, SUM, AVG)
Реализует условную логику (CASE WHEN)
Оставляет основные модели чистыми, перенося всю логику представления в QML
Так появился ProxyOrmModel, ProxyOrmValue и набор классов на основе интерфейса ISource.
Обзор проекта
Мой проект состоит из нескольких ключевых компонентов:
ProxyOrmModel: Основная модель, наследуемая от QAbstractListModel (в будущем хочу перейти на QAbstractItemModel). Она объединяет данные из разных источников (ISource), поддерживает фильтрацию (Where), сортировку и группировку.
ISource: Интерфейс для источников данных с методом data (row, role) и встроенным кэшированием.
FromSource: Базовый источник, который напрямую берет данные из исходной модели.
Join: Класс для объединения двух моделей по заданным ролям (аналог SQL JOIN).
AggregateByRow: Выполняет агрегацию (Count, Sum, Avg, Min, Max, First, Last) для строк с одинаковыми значениями.
Case: Реализует условную логику, возвращая значения по заданным условиям.
Каждый класс был протестирован с использованием QTest, чтобы гарантировать надежность.
Технические детали
Ключевая идея проекта — разделение ответственности: основные модели (те, что получают данные из базы) остаются «чистыми» и содержат только данные из исходных таблиц, а вся логика отображения (фильтрация, агрегация, преобразование) переносится в прокси-модели для использования в QML. Вот как это реализовано:
ProxyOrmModel
Хранит карту источников (QMap sourceMap), где роли сопоставлены с реализациями ISource. Это позволяет добавлять новые источники данных без изменения основной модели.
Поддерживает фильтрацию через Where и OrWhere, сортировку с помощью быстрой сортировки (quickSortRecursive) и группировку по роли.
Основная модель остается нетронутой, а вся логика отображения обрабатывается в QML через свойства и роли.
QStandardItemModel source(3, 1);
source.setData(source.index(0, 0), "A", Qt::UserRole);
source.setData(source.index(1, 0), "B", Qt::UserRole);
source.setData(source.index(2, 0), "A", Qt::UserRole);
QMap roles = {{Qt::UserRole, Qt::UserRole}};
ProxyOrmModel model(&source, roles);
model.andWhere(Qt::UserRole, Where(Where::Equals, "A"));
model.sort(Qt::UserRole, Qt::AscendingOrder);
Join — Объединяет две модели по заданным ролям. Например:
Join join(&sourceModel, Qt::UserRole, &joinModel, Qt::UserRole, {{Qt::DisplayRole, Qt::DisplayRole}});
AggregateByRow — Вычисляет агрегированные значения:
AggregateByRow agg(&source, Qt::UserRole, Qt::DisplayRole, Sum, Qt::UserRole + 1);
Case — Реализует условную логику, аналогичную SQL CASE WHEN. Проверяет значение в sourceRole и возвращает результат из списка условий или значение по умолчанию.
QList> conditions = {{"A", "Alpha"}, {"B", "Beta"}};
Case caseObj(&source, Qt::UserRole, conditions, Qt::UserRole + 1, "Unknown");
qDebug() << caseObj.data(0, Qt::UserRole + 1).toString(); // "Alpha" для "A"
ProxyOrmValue — Класс для агрегации данных по всей модели (Count, Sum, Avg, Min, Max) с поддержкой фильтрации через Where. Автоматически пересчитывает значения при изменении модели. Полезен для вывода сводной информации, например, общей суммы или среднего значения.
ProxyOrmValue value(&source, ProxyOrm::ProxyOrmValue::Sum, Qt::DisplayRole);
value.where(Qt::UserRole, Where::Equals, "A");
qDebug() << value.value().toInt(); // Сумма значений для строк с "A"
Чтобы ускорить работу, я добавил кэширование в ISource и оптимизировал агрегацию через QHash в некоторых классах.
Пример использования
Вот как можно использовать ProxyOrmModel для фильтрации и агрегации:
OrderViewModel::OrderViewModel(QAbstractListModel *sourceModel,
QAbstractListModel *userModel,
QAbstractListModel *staffModel,
QObject *parent)
: ProxyOrm::ProxyOrmModel{sourceModel,
{{OrderViewModel::IdRole, OrderModel::IdRole},
{OrderViewModel::NumberRole, OrderModel::NumberRole},
{OrderViewModel::UuidRole, OrderModel::UuidRole},
{OrderViewModel::CreatedByRole, OrderModel::CreatedByRole},
{OrderViewModel::TotalRole, OrderModel::TotalRole},
{OrderViewModel::CreatedAtRole, OrderModel::CreatedAtRole}},
parent}
{
ProxyOrm::Join *userJoin
= new ProxyOrm::Join(this,
OrderModel::CreatedByRole,
userModel,
UserModel::IdRole,
{{OrderViewModel::FirstnameRole, UserModel::FirstnameRole},
{OrderViewModel::LastnameRole, UserModel::LastnameRole},
{OrderViewModel::StaffIdRole, UserModel::StaffIdRole}});
addSource(userJoin);
ProxyOrm::Join *staffJoin
= new ProxyOrm::Join(this,
OrderViewModel::StaffIdRole,
staffModel,
StaffModel::IdRole,
{{OrderViewModel::StaffNameRole, StaffModel::NameRole},
{OrderViewModel::StaffRateRole, StaffModel::RateRole}});
addSource(staffJoin);
auto userOrderTotal = new ProxyOrm::AggregateByRow(this,
OrderViewModel::CreatedByRole,
OrderViewModel::TotalRole,
ProxyOrm::TypeAggregate::Avg,
OrderViewModel::UserOrderTotalRole);
addSource(userOrderTotal);
auto userCountOrder = new ProxyOrm::AggregateByRow(this,
OrderViewModel::CreatedByRole,
OrderViewModel::IdRole,
ProxyOrm::TypeAggregate::Count,
OrderViewModel::CountOrderRole);
addSource(userCountOrder);
auto caseOrder = new ProxyOrm::Case(this,
OrderViewModel::CountOrderRole,
{{1, "один заказ"}, {2, "два заказа"}},
OrderViewModel::CaseRole,
"какое-то количество заказов");
addSource(caseOrder);
auto orWhere = ProxyOrm::OrWhere{ProxyOrm::Where{ProxyOrm::Where::Equals, 11},
ProxyOrm::Where{ProxyOrm::Where::Equals, 1}};
andWhere(OrderViewModel::CreatedByRole, orWhere);
sort(OrderViewModel::StaffRateRole, Qt::SortOrder::AscendingOrder);
groupBy(OrderViewModel::CreatedByRole);
}
QHash OrderViewModel::roleNames() const
{
QHash roles;
roles[IdRole] = "idRole";
roles[NumberRole] = "numberRole";
roles[UuidRole] = "uuidRole";
roles[CreatedByRole] = "createdByRole";
roles[TotalRole] = "totalRole";
roles[CreatedAtRole] = "createdAtRole";
roles[FirstnameRole] = "firstnameRole";
roles[LastnameRole] = "lastnameRole";
roles[StaffIdRole] = "staffIdRole";
roles[StaffNameRole] = "staffNameRole";
roles[StaffRateRole] = "staffRateRole";
roles[UserOrderTotalRole] = "userOrderTotalRole";
roles[CountOrderRole] = "countOrderRole";
roles[CaseRole] = "caseRole";
return roles;
}
ListView {
model: orderViewModel
delegate: Label {
text: qsTr("fullname: %1 %2 ").arg(firstnameRole).arg(lastnameRole)
}
}
Label {
text: "Total: " + totalValue.value
}
Проблемы и решения
В процессе разработки я столкнулся с несколькими трудностями:
Производительность: Линейный поиск в Join и AggregateByRow был медленным. Решил это с помощью кэширования и хэш-таблиц.
Сигналы: Было сложно отследить изменение join моделей.
Тестирование: Сначала писал код без тестов, но потом добавил модульные тесты с QTest, что сильно упростило отладку.
Результаты и выводы
В итоге я получил гибкую модель, которая упрощает работу с данными в Qt. Она позволяет заменить сложные цепочки прокси-моделей на одну универсальную сущность с поддержкой сложных операций. Среди преимуществ:
Удобство настройки фильтров и агрегации.
Быстрая работа благодаря кэшированию.
Хорошая тестируемость.
Сейчас основная задача добавить поддержку многопоточности через QtConcurrent, для того чтобы не нагружать основной поток приложения.
Заключение
Надеюсь, мой проект будет полезен сообществу Qt-разработчиков. Если вам интересно попробовать ProxyOrmModel, то вот ссылка на GitHub. Буду рад вашим отзывам и предложениям! Пишите в комментариях, что вы думаете, и делитесь своими идеями.