Введение
В заключительной статье цикла подведём итог: мы построили достаточно полноценный самодельный слой абстракции — repository, маппинг через рефлексию, Unit of Work, кеширование — но QSqlTableModel, разобранный в основном цикле статей, тоже решает похожие задачи, причём «из коробки», без необходимости писать весь этот код самостоятельно. Разберём, когда оправдан каждый из двух подходов.
Концепция
QSqlTableModel спроектирован прежде всего для прямой связки с виджетами отображения (QTableView) — он реализует интерфейс QAbstractTableModel, автоматически генерирует SQL для CRUD-операций и хорошо подходит, когда основная задача — отобразить и отредактировать таблицу базы данных напрямую в UI с минимумом кода. Самодельный repository-слой даёт значительно больше контроля и гибкости: произвольные, не привязанные к структуре одной таблицы запросы, явное кеширование, типизированные C++ объекты вместо обращения по индексам строк/столбцов, и возможность полноценного unit-тестирования бизнес-логики в отрыве от UI и даже от конкретной СУБД (через mock-репозитории, статья 238).
Пример кода: Сравнение самодельного ORM-слоя
// Сценарий 1: простое отображение и редактирование таблицы в UI —
// QSqlTableModel выигрывает по простоте кода
void simpleTableEditingScenario(QTableView *view)
{
auto *model = new QSqlTableModel(view);
model->setTable("products");
model->setEditStrategy(QSqlTableModel::OnFieldChange);
model->select();
view->setModel(model);
// Готово — пользователь может просматривать и редактировать таблицу прямо в view,
// вся работа с SQL для CRUD уже реализована внутри QSqlTableModel
}
// Сценарий 2: сложная бизнес-логика, не привязанная напрямую к UI —
// repository-слой даёт нужный контроль и тестируемость
class OrderProcessingService
{
public:
OrderProcessingService(std::shared_ptr<IOrderRepository> orderRepo,
std::shared_ptr<IInventoryRepository> inventoryRepo,
std::shared_ptr<INotificationService> notifications)
: m_orderRepo(orderRepo), m_inventoryRepo(inventoryRepo), m_notifications(notifications) {}
bool processOrder(int orderId)
{
auto order = m_orderRepo->findById(orderId);
if (!order) return false;
// Сложная бизнес-логика, которую невозможно выразить как простой SQL-запрос
// одной таблицы: проверка остатков, резервирование, отправка уведомлений
if (!m_inventoryRepo->reserveStock(order->productId, order->quantity)) {
m_notifications->notifyOutOfStock(order->customerId, order->productId);
return false;
}
order->status = OrderStatus::Processing;
m_orderRepo->save(*order);
m_notifications->notifyOrderProcessing(order->customerId, orderId);
return true;
}
private:
std::shared_ptr<IOrderRepository> m_orderRepo;
std::shared_ptr<IInventoryRepository> m_inventoryRepo;
std::shared_ptr<INotificationService> m_notifications;
};
Практическая таблица выбора:
| Критерий | QSqlTableModel | Самодельный repository-слой |
|-----------------------------------------|------------------------|----------------------------------|
| Прямая связка с QTableView | отлично, минимум кода | требует ручного связывания |
| Сложная бизнес-логика поверх данных | неудобно | естественно подходит |
| Unit-тестирование без реальной БД | сложно | легко через mock-репозитории |
| Запросы, объединяющие несколько таблиц | ограниченно (Relational)| произвольная гибкость |
| Объём кода для простого CRUD | минимальный | требует написания репозитория |
| Типобезопасность (С++ структуры) | работа через QModelIndex| полноценные C++ объекты |
Пояснения к коду
Сценарий 1 показывает, насколько компактно решается задача «показать и отредактировать таблицу в UI» через QSqlTableModel — буквально несколько строк кода без необходимости писать репозиторий, маппинг или сервисный слой вообще. Сценарий 2, напротив, показывает OrderProcessingService — бизнес-логику, которая объединяет несколько repository (заказы, склад, уведомления) для реализации правила, которое физически невозможно выразить как простой CRUD над одной таблицей — резервирование товара с проверкой остатков, изменение статуса заказа и отправка уведомления — эта логика естественно тестируется через mock-репозитории (статья 238), но плохо вписывается в модель QSqlTableModel, которая по своей природе сфокусирована на одной таблице.
Таблица выбора суммирует ключевые критерии решения — на практике многие реальные приложения используют оба подхода одновременно: QSqlTableModel для простых административных экранов «просмотреть и отредактировать справочник», и repository-слой для основной бизнес-логики приложения, не привязанной напрямую к конкретному отображению в UI.
Подводные камни
- Попытка реализовать сложную бизнес-логику, обходя ограничения
QSqlTableModel, через множество хаков и переопределений вместо честного перехода на repository-слой — если код начинает переопределятьdata()/setData()модели с нетривиальной логикой валидации, пересчётом связанных значений или вызовом внешних сервисов, это явный сигнал, что задача переросла естественные границы применимостиQSqlTableModel, и пора выделить отдельный слой бизнес-логики. - Полный отказ от
QSqlTableModel«из принципа» даже для простых административных экранов, где он отлично подходит — избыточное усложнение архитектуры репозиториями, сервисами и mock-тестами для задачи «показать таблицу пользователей администратору с возможностью редактирования» создаёт ненужный объём кода без реальной выгоды, если для этого конкретного экрана не нужна сложная логика валидации или взаимодействия с другими частями системы. - Смешивание прямого использования
QSqlTableModelи repository-слоя для одних и тех же данных в разных частях приложения без согласованной стратегии — если одна часть приложения читает/пишет таблицуusersчерезQSqlTableModel, а другая — черезUserRepository, оба способа должны быть согласованы относительно кеширования (статья 235) и валидации данных, иначе изменения, сделанные через один путь, могут быть не видны или не учтены другим путём без явной синхронизации. - Недооценка трудозатрат на полноценный repository-слой для команд без опыта в этом паттерне. Всё, что было показано в этом цикле статей — интерфейсы, маппинг, Unit of Work, кеширование, тестирование — требует реальных инвестиций времени на проектирование и написание, особенно первые несколько раз; для небольших команд или сжатых сроков иногда практичнее начать с более простого, прямого использования
QSqlQuery/QSqlTableModelи эволюционировать в сторону repository-слоя только когда реальная сложность проекта это явно потребует, а не проектировать сложную архитектуру заранее «на будущее», которое может не наступить.