Привет, Хабр! В этой статье я хочу рассказать о своём проекте — библиотеке 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<QPair<QVariant, QVariant>> 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. Буду рад вашим отзывам и предложениям! Пишите в комментариях, что вы думаете, и делитесь своими идеями.
ссылка на оригинал статьи https://habr.com/ru/articles/885954/
Добавить комментарий