Введение
В предыдущих статьях метод mapRowToUser() вручную перечислял каждое поле строки результата запроса — для структуры с десятком полей это многословно и подвержено ошибкам (легко перепутать порядок или забыть поле при добавлении нового). В статье разберём, как автоматизировать этот маппинг через рефлексию на основе Q_GADGET/QMetaObject, сократив объём ручного кода для каждой новой сущности.
Концепция
Q_GADGET — облегчённая версия Q_OBJECT для классов, не нуждающихся в сигналах/слотах, но желающих использовать метаобъектную систему Qt (в частности, рефлексию через QMetaObject) — то есть Q_PROPERTY работает и без наследования от QObject. Используя QMetaObject::propertyCount()/property(), можно написать общую функцию, которая для любого класса с Q_GADGET и набором Q_PROPERTY автоматически читает соответствующие столбцы из QSqlRecord по имени свойства, избавляя от необходимости писать индивидуальный маппинг-метод для каждой новой сущности.
Пример кода: Маппинг QSqlRecord на
// user.h — сущность с Q_GADGET вместо обычной структуры
#pragma once
#include <QObject>
#include <QString>
class User
{
Q_GADGET
Q_PROPERTY(int id MEMBER id)
Q_PROPERTY(QString email MEMBER email)
Q_PROPERTY(QString name MEMBER name)
Q_PROPERTY(bool active MEMBER active)
public:
int id = 0;
QString email;
QString name;
bool active = true;
};
// sqlmapper.h — обобщённая функция маппинга на основе рефлексии Q_GADGET
#pragma once
#include <QSqlRecord>
#include <QMetaObject>
#include <QMetaProperty>
template <typename T>
T mapRecordToObject(const QSqlRecord &record)
{
T object;
const QMetaObject *metaObject = &T::staticMetaObject;
for (int i = 0; i < metaObject->propertyCount(); ++i) {
const QMetaProperty property = metaObject->property(i);
const QString propertyName = property.name();
const int fieldIndex = record.indexOf(propertyName);
if (fieldIndex == -1) {
continue; // в результате запроса нет столбца с таким именем — пропускаем
}
property.writeOnGadget(&object, record.value(fieldIndex));
}
return object;
}
// Обратная операция — заполнение списка значений для INSERT/UPDATE из объекта
template <typename T>
QVariantMap mapObjectToFieldValues(const T &object, const QStringList &excludeFields = {})
{
QVariantMap values;
const QMetaObject *metaObject = &T::staticMetaObject;
for (int i = 0; i < metaObject->propertyCount(); ++i) {
const QMetaProperty property = metaObject->property(i);
const QString propertyName = property.name();
if (excludeFields.contains(propertyName)) {
continue;
}
values[propertyName] = property.readOnGadget(&object);
}
return values;
}
// Использование в репозитории — без единой строчки ручного перечисления полей
#include "sqlmapper.h"
QVector<User> SqlUserRepository::findAll()
{
QVector<User> result;
QSqlQuery query(m_db);
query.exec("SELECT id, email, name, active FROM users ORDER BY name");
while (query.next()) {
result.append(mapRecordToObject<User>(query.record()));
}
return result;
}
bool SqlUserRepository::insertNewUser(User &user)
{
const QVariantMap fields = mapObjectToFieldValues(user, {"id"}); // id генерируется базой, не вставляем его
QStringList columnNames = fields.keys();
QStringList placeholders;
for (const QString &name : columnNames) {
placeholders << (":" + name);
}
QSqlQuery query(m_db);
query.prepare(QString("INSERT INTO users (%1) VALUES (%2)")
.arg(columnNames.join(", "), placeholders.join(", ")));
for (const QString &name : columnNames) {
query.bindValue(":" + name, fields[name]);
}
if (!query.exec()) return false;
user.id = query.lastInsertId().toInt();
return true;
}
Пояснения к коду
User теперь объявлен с Q_GADGET и набором Q_PROPERTY ... MEMBER ... — синтаксис MEMBER связывает свойство непосредственно с полем класса без необходимости писать отдельные getter/setter методы, что особенно удобно для простых структур данных. mapRecordToObject<T>() — шаблонная функция, работающая с любым типом T, имеющим Q_GADGET: она перебирает все объявленные свойства через metaObject->property(i) и для каждого ищет столбец с совпадающим именем в QSqlRecord через record.indexOf(), записывая найденное значение через property.writeOnGadget().
mapObjectToFieldValues() выполняет обратную операцию — собирает имена и значения всех свойств объекта в QVariantMap, что используется в insertNewUser() для динамического построения SQL-запроса INSERT без ручного перечисления каждого столбца — параметр excludeFields позволяет исключить, например, поле id, которое генерируется самой базой данных и не должно передаваться при вставке.
Подводные камни
- Совпадение имени свойства
Q_PROPERTYи имени столбца в SQL должно быть точным, иначеrecord.indexOf()вернёт -1, и поле будет тихо пропущено без явной ошибки — если в базе данных столбец называетсяuser_email(snake_case, частая конвенция в SQL), а свойство в C++ называетсяemail(camelCase, конвенция Qt/C++), автоматический маппинг не сработает без дополнительного слоя преобразования имён. - Производительность рефлексии через
QMetaObjectниже прямого обращения к полям, поскольку каждое обращение проходит через дополнительный уровень индирекции метаобъектной системы — для большинства приложений эта разница незаметна, но при обработке очень больших объёмов записей (десятки тысяч строк за одну операцию) ручной, прямой маппинг может оказаться заметно быстрее. - Отсутствие валидации типов при автоматическом маппинге. Если значение в базе данных имеет тип, не совместимый напрямую с типом свойства (например, столбец содержит текстовое представление числа, а свойство объявлено как
int),QVariantпопытается выполнить неявное преобразование — это может либо сработать корректно благодаря гибкостиQVariant, либо привести к «нулевому» или неожиданному значению без явной ошибки, что нужно явно проверять для критичных полей. Q_GADGETне поддерживает сигналы, в отличие отQ_OBJECT— если впоследствии понадобится, например, уведомлять об изменении полей сущности через сигналы (как вQ_PROPERTY ... NOTIFY ...), потребуется переход на полноценныйQObject-наследник, что меняет модель владения объектами (нужен родитель, нельзя свободно копировать как обычную структуру) — выбор междуQ_GADGETиQObjectдля сущностей данных стоит делать осознанно, с учётом этого ограничения.