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. Вот как это реализовано:

  1. 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); 
  1. Join — Объединяет две модели по заданным ролям. Например:

Join join(&sourceModel, Qt::UserRole, &joinModel, Qt::UserRole, {{Qt::DisplayRole, Qt::DisplayRole}}); 
  1. AggregateByRow — Вычисляет агрегированные значения:

AggregateByRow agg(&source, Qt::UserRole, Qt::DisplayRole, Sum, Qt::UserRole + 1);
  1. Case — Реализует условную логику, аналогичную SQL CASE WHEN. Проверяет значение в sourceRole и возвращает результат из списка условий или значение по умолчанию.

QList<QPair<QVariant, QVariant>> conditions = {{"A", "Alpha"}, {"B", "Beta"}}; Case caseObj(&amp;source, Qt::UserRole, conditions, Qt::UserRole + 1, "Unknown"); qDebug() << caseObj.data(0, Qt::UserRole + 1).toString(); // "Alpha" для "A" 
  1. 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *