[recovery mode] Tiny-qORM: рассказ без счастливого конца
Чаще всего на хабре люди делятся историями своего успеха. Вроде, «Ребята, я написал свою ORM, качайте, ставьте ллойсы!» Эта история будет немного другая. В ней я расскажу о неуспехе, который считаю своим серьёзным достижением.
Ожидание — реальность.
История о метатипах Qt, написании велосипедов, превышении максимального числа записей в объектном файле и, неожиданно, инструменте, который работает так, как и было задумано.
С чего всё началось? Предсказуемо, с лени. Как-то раз появилась задача (де-)сериализации структур в SQL. Большого числа структур, несколько сотен. Естественно, с разными уровнями вложенности, с указателями и контейнерами. Помимо прочего, имелась определяющая особенность: все они уже имели привязку к QJSEngine, то есть имели полноценную метасистему Qt.
С такими вводными не мудрено было придти к написанию своей ORM и поставить весьма амбициозные цели:
1) Минимальная модификация сохраняемых структур. В лучшем случае, без оной вообще, в худшем — Ctrl+Shift+F.
2) Работа с любыми типами, контейнерами и указателями.
3) Не самые страшные таблицы с возможностью их использования вне ORM.
И обозначить предсказуемые ограничения:
1) Таблицы создаются только для классов с метаинформацией (Q_OBJECT\Q_GADGET) для их свойств (Q_PROPERTY). Все зарегистрированные в метасистеме типы, не имеющие метаинформации, будут сохраняться либо в виде строк, либо в виде сырых данных. Если преобразование не существует или тип неизвестен, он пропускается.
Забегая вперёд, получилось следующее:
struct Mom {
Q_GADGET
Q_PROPERTY(QString name MEMBER m_name)
Q_PROPERTY(She is MEMBER m_is)
public:
enum She {
Nice,
Sweet,
Beautiful,
Pretty,
Cozy,
Fansy,
Bear
}; Q_ENUM(She)
public:
QString m_name;
She m_is;
bool operator !=(Mom const& no) { return m_name != no.m_name; }
};
Q_DECLARE_METATYPE(Mom)
struct Car {
Q_GADGET
Q_PROPERTY(double gas MEMBER m_gas)
public:
double m_gas;
};
Q_DECLARE_METATYPE(Car)
struct Dad {
Q_GADGET
Q_PROPERTY(QString name MEMBER m_name)
Q_PROPERTY(Car * car MEMBER m_car)
public:
QString m_name;
Car * m_car = nullptr; // lost somewhere
bool operator !=(Dad const& no) { return m_name != no.m_name; }
};
Q_DECLARE_METATYPE(Dad)
struct Brother {
Q_GADGET
Q_PROPERTY(QString name MEMBER m_name)
Q_PROPERTY(int last_combo MEMBER m_lastCombo)
Q_PROPERTY(int total_punches MEMBER m_totalPunches)
public:
QString m_name;
int m_lastCombo;
int m_totalPunches;
bool operator !=(Brother const& no) { return m_name != no.m_name; }
bool operator ==(Brother const& no) { return m_name == no.m_name; }
};
Q_DECLARE_METATYPE(Brother)
struct Ur
{
Q_GADGET
Q_PROPERTY(QString name MEMBER m_name)
Q_PROPERTY(Mom mom MEMBER m_mama)
Q_PROPERTY(Dad dad MEMBER m_papa)
Q_PROPERTY(QList bros MEMBER m_bros)
Q_PROPERTY(QList drows MEMBER m_drows)
public:
QString m_name;
Mom m_mama;
Dad m_papa;
QList m_bros;
QList m_drows;
};
Q_DECLARE_METATYPE(Ur)
bool init()
{
qRegisterType("Ur");
qRegisterType("Dad");
qRegisterType("Mom");
qRegisterType("Brother");
qRegisterType("Car");
}
bool serialize(QList const& urs)
{
/* SQL hell */
}
struct Mom {
Q_GADGET
Q_PROPERTY(QString name MEMBER m_name)
Q_PROPERTY(She is MEMBER m_is)
public:
enum She {
Nice,
Sweet,
Beautiful,
Pretty,
Cozy,
Fansy,
Bear
}; Q_ENUM(She)
public:
QString m_name;
She m_is;
bool operator !=(Mom const& no) { return m_name != no.m_name; }
};
ORM_DECLARE_METATYPE(Mom)
struct Car {
Q_GADGET
Q_PROPERTY(double gas MEMBER m_gas)
public:
double m_gas;
};
ORM_DECLARE_METATYPE(Car)
struct Dad {
Q_GADGET
Q_PROPERTY(QString name MEMBER m_name)
Q_PROPERTY(Car * car MEMBER m_car)
public:
QString m_name;
Car * m_car = nullptr; // lost somewhere
bool operator !=(Dad const& no) { return m_name != no.m_name; }
};
ORM_DECLARE_METATYPE(Dad)
struct Brother {
Q_GADGET
Q_PROPERTY(QString name MEMBER m_name)
Q_PROPERTY(int last_combo MEMBER m_lastCombo)
Q_PROPERTY(int total_punches MEMBER m_totalPunches)
public:
QString m_name;
int m_lastCombo;
int m_totalPunches;
bool operator !=(Brother const& no) { return m_name != no.m_name; }
bool operator ==(Brother const& no) { return m_name == no.m_name; }
};
ORM_DECLARE_METATYPE(Brother)
struct Ur
{
Q_GADGET
Q_PROPERTY(QString name MEMBER m_name)
Q_PROPERTY(Mom mom MEMBER m_mama)
Q_PROPERTY(Dad dad MEMBER m_papa)
Q_PROPERTY(QList bros MEMBER m_bros)
Q_PROPERTY(QList drows MEMBER m_drows)
public:
QString m_name;
Mom m_mama;
Dad m_papa;
QList m_bros;
QList m_drows;
};
ORM_DECLARE_METATYPE(Ur)
bool init()
{
ormRegisterType("Ur");
ormRegisterType("Dad");
ormRegisterType("Mom");
ormRegisterType("Brother");
ormRegisterType("Car");
}
bool serialize(QList const& urs)
{
ORM orm;
orm.create(); // if not exists
orm.insert(urs);
}
Q_DECLARE_METATYPE(Mom) -> ORM_DECLARE_METATYPE(Mom)
Q_DECLARE_METATYPE(Car) -> ORM_DECLARE_METATYPE(Car)
Q_DECLARE_METATYPE(Dad) -> ORM_DECLARE_METATYPE(Dad)
Q_DECLARE_METATYPE(Brother) -> ORM_DECLARE_METATYPE(Brother)
Q_DECLARE_METATYPE(Ur) -> ORM_DECLARE_METATYPE(Ur)
qRegisterType("Ur"); -> ormRegisterType("Ur");
qRegisterType("Dad"); -> ormRegisterType("Ur");
qRegisterType("Mom"); -> ormRegisterType("Ur");
qRegisterType("Brother");-> ormRegisterType("Ur");
qRegisterType("Car"); -> ormRegisterType("Ur");
/* sql hell */ -> ORM orm;
orm.create(); // if not exists
orm.insert(urs);
Making of…
Шаг 1. Получать метаинформацию, список полей класса и их значения.
Спасибо разработчикам Qt, мы это можем делать по щелчку пальцев и id метакласса. Выглядит это примерно так:
const QMetaObject * object = QMetaType::metaObjectForType(id);
if (object) {
for (int i = 0; i < object->propertyCount(); ++i) {
QMetaProperty property = object->property(i);
columns << property.name();
types << property.userType();
}
}
Чтение и запись так же не вызывают проблем. Почти. QMetaProperty имеет пару методов чтения-записи для объектов. И ещё одну пару для гаджетов. Поэтому на этапе чтения-записи нам нужно определиться, в кого мы пишем. Делается это так:
bool isQObject(QMetaObject const& meta) {
return meta.inherits(QMetaType::metaObjectForType(QMetaType::QObjectStar));
}
Тогда чтение и запись производятся следующим образом:
inline bool write(bool isQObject, QVariant & writeInto,
QMetaProperty property, QVariant const& value) {
if (isQObject) return property.write(writeInto.value(), value);
else return property.writeOnGadget(writeInto.data(), value);
}
inline QVariant read(bool isQObject, QVariant const& readFrom,
QMetaProperty property) {
if (isQObject) {
QObject * object = readFrom.value();
return property.read(object);
}
else {
return property.readOnGadget(readFrom.value());
}
}
Казалось бы, readOnGadget, в конце концов, вызывает тот же read, так что зачем городить весь этот код? Совместимость и отсутствие гарантий, что такое поведение не изменится.
И ещё один нюанс. При сохранении Q_ENUM в QVariant его значение кастуется в int. В базу данных тоже поступает int. Но записать int в свойство типа Q_ENUM мы не можем. Поэтому перед записью мы должны проверить, является ли указанное свойство перечислением — и вызвать явное преобразование в таком случае. Звучит страшнее, чем есть на самом деле.
if (property.isEnumType()) {
variant.convert(property.userType());
}
Шаг 2. Создавать произвольные структуры по метаинформации.
Снова бьём челом разработчикам за класс QVariant и его конструктор QVariant (int id, void* copy). С его помощью можно создать любую структуру с пустым конструктором — и это хорошая новость. Плохая новость: наследники QObject в список не входят. Хорошая новость: их можно делать с помощью QMetaObject: newInstance ().
Создание экземпляра произвольного типа будет выглядеть примерно так:
QVariant make_variant(QMetaObject const& meta) {
QVariant variant;
if (isQObject(meta)) {
QObject * obj = meta.newInstance();
if (obj) {
obj->setObjectName("orm_made");
obj->setParent(QCoreApplication::instance());
variant = QVariant::fromValue(obj);
}
}
else {
variant = QVariant((classtype), nullptr);
}
if (!variant.isValid()){
qWarning() << "Unable to create instance of type " << meta.className();
}
if (isQObject(meta) && variant.value() == nullptr) {
qWarning() << "Unable to create instance of QObject " << meta.className();
}
return variant;
}
Шаг 3. Реализовать сериализацию тривиальных типов.
Под тривиальными типами будем понимать числа, строки и бинарные поля. Вроде бы задача простая, снова берём QVariant и в бой. Но есть нюанс. В ряде случаев нам может захотеться сделать «тривиальными» иные типы, например, изображение. С одной стороны, можно было бы просто проверять, есть ли у метатипа нужные конвертеры и использовать их. Но это не самый удачный способ, тем более, что он чреват возникновением конфликтов, так что лучше иметь списки типов и способы их сохранения: в строку, в BLOB или отдать на откуп Qt. На этом же шаге лучше заиметь список тех типов, с которыми вы предпочтёте не связываться. Из стандартных это могут быть JSON-объекты или QModelIndex. Опять же, никакой магии, статические списки.
Шаг 4. Реализовать сериализацию нетривиальных типов: структур, указателей, контейнеров.
И опять, разработчики постарались: их QVariant решает эту задачу. Или нет?
Проблема 1: связность указателя и типа, шаблона и типов-параметров.
Для произвольного метакласса нельзя ни получить связные с ним метаклассы указателей (или структуры), ни получить тип, хранимый в шаблоне. Это очень печально, хотя и вполне предсказуемо. Откуда ей взяться?
Неоткуда.
Можно, конечно, поиграться с именем класса, пощекотав параметры шаблонов, но это очень нежное решение, которое ломается о грубую реальность typedef. Что же, иного не остаётся, придётся завести свою функцию для регистрации типов.
template int orm::Register(const char * c)
{
int type = qMetaTypeId();
if (!type) {
if (c) {
type = qRegisterMetaType(c);
}
else {
type = qRegisterMetaType();
}
}
Config::addPointerStub(orm::Pointers::registerTypePointers());
orm::Containers::registerSequentialContainers();
return type;
}
А вместе с ней и статический массивчик под это дело. Точнее, QMap, где ключом будет id метакласса, а значением — структура, хранящая все связные типы.
//
struct ORMPointerStub {
int T =0; // T
int pT=0; // T*
int ST=0; // QSharedPointer
int WT=0; // QWeakPointer
int sT=0; // std::shared_ptr
int wT=0; // std::weak_ptr
};
//
static QMap pointerMap;
void ORM_Config::addPointerStub(const orm_pointers::ORMPointerStub & stub)
{
if (stub. T) pointerMap[stub. T] = stub;
if (stub.pT) pointerMap[stub.pT] = stub;
if (stub.ST) pointerMap[stub.ST] = stub;
if (stub.WT) pointerMap[stub.WT] = stub;
if (stub.sT) pointerMap[stub.sT] = stub;
if (stub.wT) pointerMap[stub.wT] = stub;
}
//
template void* toVPointer ( T const& t)
{ return reinterpret_cast(const_cast(&t )); }
template void* toVPointerP( T * t)
{ return reinterpret_cast( t ); }
template void* toVPointerS(QSharedPointer const& t)
{ return reinterpret_cast(const_cast( t.data())); }
template void* toVPointers(std::shared_ptr const& t)
{ return reinterpret_cast(const_cast( t.get ())); }
template T* fromVoidP(void* t)
{ return reinterpret_cast(t) ; }
template QSharedPointer fromVoidS(void* t)
{ return QSharedPointer (reinterpret_cast(t)); }
template std::shared_ptr fromVoids(void* t)
{ return std::shared_ptr(reinterpret_cast(t)); }
template ORMPointerStub registerTypePointersEx()
{
ORMPointerStub stub;
stub.T = qMetaTypeId();
stub.pT = qRegisterMetaType();
stub.ST = qRegisterMetaType>();
stub.WT = qRegisterMetaType>();
stub.sT = qRegisterMetaType>();
stub.wT = qRegisterMetaType>();
QMetaType::registerConverter< T , void*>(&toVPointer );
QMetaType::registerConverter< T*, void*>(&toVPointerP);
QMetaType::registerConverter, void*>(&toVPointerS);
QMetaType::registerConverter, void*>(&toVPointers);
QMetaType::registerConverter(&fromVoidP);
QMetaType::registerConverter>(&fromVoidS);
QMetaType::registerConverter>(&fromVoids);
return stub;
}
Как вы могли заметить, тут уже были зарегистрированы конвертеры T>void*, T*>void* и void*>T*. Ничего особенного, они нам потребуются для спокойной работы с QMetaProperty, так как в select, где будут создаваться элементы, мы будем делать простые указатели, а передавать вообще универсальный void*. Нужный тип указателя будет создан самим QVariant в момент записи.
Проблема 2: обработка контейнеров.
С контейнерами не всё так плохо. Для последовательных есть простой способ узнать, является ли переданный нам тип зарегистрированным:
bool isSequentialContainer(int metaTypeID){
return QMetaType::hasRegisteredConverterFunction(metaTypeID,
qMetaTypeId());
}
Пробежаться по нему:
QSequentialIterable sequentialIterable = myList.value();
for (QVariant variant : sequentialIterable) {
// do stuff
}
И даже получить ID хранимого метатипа (осторожно — глаза!)
inline int getSequentialContainerStoredType(int metaTypeID)
{
return (*(QVariant(static_cast(metaTypeID))
.value()).end()).userType();
// да, .end()).userType();
// мне стыдно, хорошо?
}
Так что сохранение данных становится делом чисто техническим. Остаётся лишь справиться со всем многообразием контейнеров. Моя реализация затрагивает лишь те, которые можно получить кастами из QList. Во-первых, потому, что результатом QSqlQuery является QVariantList, а, во-вторых, потому, что он может кастоваться во все основные Qt и std контейнеры. (Есть и третья причина, шаблонная магия std плохо впихивается в универсальные короткие решения.)
template QList qListFromQVariantList(QVariant const& variantList)
{
QList list;
QSequentialIterable sequentialIterable = variantList.value();
for (QVariant const& variant : sequentialIterable) {
if(v.canConvert()) {
list << variant.value();
}
}
return list;
}
template QVector qVectorFromQVariantList(QVariant const& v)
{ return qListFromQVariantList(v).toVector (); }
template std::list stdListFromQVariantList(QVariant const& v)
{ return qListFromQVariantList(v).toStdList (); }
template std::vector stdVectorFromQVariantList(QVariant const& v)
{ return qListFromQVariantList(v).toVector().toStdVector(); }
template void registerTypeSequentialContainers()
{
qMetaTypeId>() ? qMetaTypeId>()
: qRegisterMetaType>();
qMetaTypeId>() ? qMetaTypeId>()
: qRegisterMetaType>();
qMetaTypeId>() ? qMetaTypeId>()
: qRegisterMetaType>();
qMetaTypeId>() ? qMetaTypeId>()
: qRegisterMetaType>();
QMetaType::registerConverter>(&( qListFromQVariantList));
QMetaType::registerConverter>(&( qVectorFromQVariantList));
QMetaType::registerConverter>(&( stdListFromQVariantList));
QMetaType::registerConverter>(&(stdVectorFromQVariantList));
}
С ассоциативными контейнерами и парами дела обстоят хуже. Несмотря на то, что для них есть аналогичный по функциональности с QSequentialIterable класс QAssociativeIterable, некоторые сценарии его использования приводят к вылетам программы. Поэтому нас снова ожидают старые друзья: структура и статический массив, которые нужны для выяснения хранившегося в контейнере типа. Кроме того, нам потребуется тип-прокладка, который бы смог сохранить промежуточные результаты select для каждой строки. Можно было бы использовать QPair
// Код становится всё больше и всё скучнее. Если интересно, https://github.com/iiiCpu/Tiny-qORM/blob/master/ORM/orm.h
struct ORM_QVariantPair //: public ORMValue
{
Q_GADGET
Q_PROPERTY(QVariant key MEMBER key)
Q_PROPERTY(QVariant value MEMBER value)
public:
QVariant key, value;
QVariant& operator[](int index){ return index == 0 ? key : value; }
};
template QMap qMapFromQVariantMap(QVariant const& v)
{
QMap list;
QAssociativeIterable ai = v.value();
QAssociativeIterable::const_iterator it = ai.begin();
const QAssociativeIterable::const_iterator end = ai.end();
for ( ; it != end; ++it) {
if(it.key().canConvert() && it.value().canConvert()) {
list.insert(it.key().value(), it.value().value());
}
}
return list;
}
template QList qMapToPairListStub(QMap const& v)
{
QList psl;
for (auto i = v.begin(); i != v.end(); ++i) {
ORM_QVariantPair ps;
ps.key = QVariant::fromValue(i.key());
ps.value = QVariant::fromValue(i.value());
psl << ps;
}
return psl;
}
template void registerQPair()
{
ORM_Config::addPairType(qMetaTypeId(), qMetaTypeId(),
qMetaTypeId>() ? qMetaTypeId>() : qRegisterMetaType>());
QMetaType::registerConverter>(&(qPairFromQVariant));
QMetaType::registerConverter>(&(qPairFromQVariantList));
QMetaType::registerConverter>(&(qPairFromPairStub));
QMetaType::registerConverter, ORM_QVariantPair>(&(toQPairStub));
}
template void registerQMap()
{
registerQPair();
ORM_Config::addContainerPairType(qMetaTypeId(), qMetaTypeId(),
qMetaTypeId>() ? qMetaTypeId>() : qRegisterMetaType>());
QMetaType::registerConverter, QList>(&(qMapToPairListStub));
QMetaType::registerConverter>(&(qMapFromQVariantMap));
QMetaType::registerConverter>(&(qMapFromQVariantList));
QMetaType::registerConverter, QMap>(&(qMapFromPairListStub));
}
uint qHash(ORM_QVariantPair const& variantPair) noexcept;
Q_DECLARE_METATYPE(ORM_QVariantPair)
Проблема 3: использование контейнеров.
У контейнеров есть ещё одна проблема: они не являются структурой. Вот такой вот внезапный удар поддых от Капитана Очевидности! На самом деле, всё просто: у контейнеров нет полей и метаобъекта, а, значит, мы должны их обрабатывать отдельно, пропихивая заглушки. Точнее, не так. Нам нужно обрабатывать отдельно последовательные контейнеры с тривиальными типами и отдельно — ассоциативные контейнеры, так как последовательные контейнеры из структур запросто обрабатываются, как простые структуры. С первыми можно схитрить, преобразовав их в строку или BLOB (нужные методы в QList есть из коробки). Со вторыми же ничего не поделать: придётся дублировать все методы, пропихивая вместо настоящих Q_PROPERTY заглушки key и value.
QVariant ORM::meta_select(const QMetaObject &meta, QString const& parent_name,
QString const& property_name, long long parent_orm_rowid)
{
QString table_name = generate_table_name(parent_name, property_name,
QString(meta.className()),QueryType::Select);
int classtype = QMetaType::type(meta.className());
bool isQObject = ORM_Impl::isQObject(meta);
bool with_orm_rowid = ORM_Impl::withRowid(meta);
if (!selectQueries.contains(table_name)) {
QStringList query_columns;
QList query_types;
for (int i = 0; i < meta.propertyCount(); ++i) {
QMetaProperty property = meta.property(i);
if (ORM_Impl::isIgnored(property.userType())) {
continue;
}
QVariant ORM::meta_select_pair (int metaTypeID, QString const& parent_name,
QString const& property_name, long long parent_orm_rowid)
{
QString className = QMetaType::typeName(metaTypeID);
QString table_name = generate_table_name(parent_name, property_name, className, QueryType::Select);
int keyType = ORM_Impl::getAssociativeContainerStoredKeyType(metaTypeID);
int valueType = ORM_Impl::getAssociativeContainerStoredValueType(metaTypeID);
if (!selectQueries.contains(table_name)) {
QStringList query_columns;
QList query_types;
query_columns << ORM_Impl::orm_rowidName;
query_types << qMetaTypeId();
for (int column = 0; column < 2; ++column) {
int userType = column == 0 ? keyType : valueType;
QString name = column == 0 ? "key" : "value";
if (ORM_Impl::isIgnored(userType)) {
continue;
}
В итоге мы получили однородный доступ на чтение и запись ко всем используемым типам и структурам с возможностью их рекурсивного обхода.
Шаг 5. Написать SQL запросы.
Для написания SQL запроса нам достаточно иметь метатип класса, имя поля в родительской структуре, имя родительской таблицы, список имён и метатипов полей. Из первых трёх сконструируем имя таблицы, из остального столбцы.
QString ORM::generate_update_query(QString const& parent_name,
QString const& property_name, const QString &class_name,
const QStringList &names, const QList &types,
bool parent_orm_rowid) const
{
Q_UNUSED(types)
QString table_name = generate_table_name(parent_name,
property_name, class_name, QueryType::Update);
QString query_text = QString("UPDATE OR IGNORE %1 SET ").arg(table_name);
QStringList t_set;
for (int i = 0; i < names.size(); ++i) {
t_set << normalize(names[i], QueryType::Update) + " = " +
normalizeVar(":" + names[i], types[i], QueryType::Update);
}
query_text += t_set.join(',') + " WHERE " +
normalize(ORM_Impl::orm_rowidName, QueryType::Update) + " = :" +
ORM_Impl::orm_rowidName + " ";
if (parent_orm_rowid) {
query_text += " AND " + ORM_Impl::orm_parentRowidName + " = :" +
ORM_Impl::orm_parentRowidName + " ";
}
query_text += ";";
return query_text;
}
О чём не стоит забывать:
1) Нормализация имён. Дело не только в регистре, типы могут содержать в себе скобки и запятые шаблонов, двоеточия пространств имён. От всего этого многообразия следует избавляться.
QString ORM::normalize(const QString & str, QueryType queryType) const
{
Q_UNUSED(queryType)
QString s = str;
static QRegularExpression regExp1 {"(.)([A-Z]+)"};
static QRegularExpression regExp2 {"([a-z0-9])([A-Z])"};
static QRegularExpression regExp3 {"[:;,.<>]+"};
return "_" + s.replace(regExp1, "\\1_\\2")
.replace(regExp2, "\\1_\\2").toLower()
.replace(regExp3, "_");
}
2) Приведения типов. Если работа ведётся с SQLite, то всё просто: кто бы ты ни был, ты — строка. Но если используются другие БД, порой, без каста не обойтись. Значит, при вставке или обновлении нормализованное значение (плейсхолдер) нужно дополнительно преобразовать, да и при выборе тоже.
И в чём же проблема? Почему «неуспех»?
Думаю, многим ответ уже очевиден. Скорость работы. На простых структурах падение скорости составляет 10% на запись и 100% на чтение. На структуре с глубиной вложенности 1 — уже 30% и 700%. На глубине 2 — 50% и 2000%. С повышением вложенности скорость работы падает экспоненциально.
Simple sqlite[10000]:
ORM: insert= 2160 select= 56
QSqlQuery: insert= 1352 select= 53
RAW: insert= 1271 select= 3
Complex sqlite[10000]:
ORM: insert= 7231 select= 24095
QSqlQuery: insert= 4594 select= 127
RAW: insert= 1117 select= 7
struct U1 : public ORMValue
{
Q_GADGET
Q_PROPERTY(int index MEMBER m_i)
public:
int m_i = 0;
U1():m_i(0){}
U1& operator=(U1 const& o) { m_orm_rowid = o.m_orm_rowid; m_i = o.m_i; return *this; }
};
struct U3 : public ORMValue
{
Q_GADGET
Q_PROPERTY(int index MEMBER m_i)
public:
int m_i;
U3(int i = rand()):m_i(i){}
bool operator !=(U3 const& o) const { return m_i != o.m_i; }
U3& operator=(U3 const& o) { m_orm_rowid = o.m_orm_rowid; m_i = o.m_i; return *this; }
};
struct U2 : public ORMValue
{
Q_GADGET
Q_PROPERTY(Test3::U3 u3 MEMBER m_u3)
Q_PROPERTY(int index MEMBER m_i )
public:
U3 m_u3;
int m_i;
U2(int i = rand()):m_i(i){}
bool operator !=(U2 const& o) const { return m_i != o.m_i || m_u3 != o.m_u3; }
U2& operator=(U2 const& o) { m_orm_rowid = o.m_orm_rowid; m_u3 = o.m_u3; m_i = o.m_i; return *this; }
};
struct U1 : public ORMValue
{
Q_GADGET
Q_PROPERTY(Test3::U3* u3 MEMBER m_u3)
Q_PROPERTY(Test3::U2 u2 MEMBER m_u2)
Q_PROPERTY(int index MEMBER m_i)
public:
U3* m_u3 = nullptr;
U2 m_u2;
int m_i = 0;
U1():m_i(0){}
U1(U1 const& o):m_i(0){ m_orm_rowid = o.m_orm_rowid; m_u2 = o.m_u2; m_i = o.m_i; if (!o.m_u3) { delete m_u3; m_u3 = nullptr; } else { if (!m_u3) { m_u3 = new U3();} *m_u3 = *o.m_u3; } }
U1(U1 && o):m_i(0){ m_orm_rowid = o.m_orm_rowid; m_u2 = o.m_u2; m_i = o.m_i; delete m_u3; m_u3 = o.m_u3; o.m_u3 = nullptr; }
~U1(){ delete m_u3; }
U1& operator=(U1 const& o) { m_orm_rowid = o.m_orm_rowid; m_u2 = o.m_u2; m_i = o.m_i; if (!o.m_u3) { delete m_u3; m_u3 = nullptr; } else { if (!m_u3) { m_u3 = new U3();} *m_u3 = *o.m_u3; } return *this; }
};
Причина тому ровно одна. Метасистема Qt. Она устроена так, что в ней происходит очень много копирований. Вернее, в ней производится минимально необходимое число копирований для реалтайма, но, тем не менее, весьма большое. Когда производится сериализация данных, нужно один раз скопировать значение в QVariant, и больше никаких копирований не производится. Когда же происходит десериализация — это песня! Копирование структур происходит на каждом вызове write\writeOnGadget — и от них совершенно нельзя избавиться.
Есть ли другой подход, при котором нам не нужно делать копирования? Есть. Объявлять все вложенные структуры указателями.
struct Car {
Q_GADGET
Q_PROPERTY(double gas MEMBER m_gas)
public:
double m_gas;
};
struct Dad {
Q_GADGET
Q_PROPERTY(Car car MEMBER m_car STORED false)
Q_PROPERTY(ormReferenсe car READ getCar WRITE setCar SCRIPTABLE false)
public:
Car m_car;
ormReferenсe getCar() const { return ormReferenсe(&m_car); }
void setCar(ormReferenсe car) { if (car) m_car = *car; }
};
Такое решение позволяет значительно ускорить ORM. Падение скорости работы всё ещё значительное, в разы, но уже не на порядки. Тем не менее, решение это flawed by design, требующее изменять кучу кода. А если это в любом случае нужно делать, не проще ли сразу написать генератор SQL запросов? Увы, проще, и работает такой код разительно быстрее. Потому моя достаточно большая и интересная работа осталась пылиться в углу.
Вместо вывода
Жалею ли я, что потратил несколько месяцев на её написание? Чёрт подери, нет! Это было очень интересное погружение внутрь существующей и работающей метасистемы, которое немного изменило мой взгляд на программирование. Я предполагал такой результат, когда приступал к работе. Надеялся на лучшее, но предполагал примерно такой. Я получил его на выходе. И он меня устроил!
Послесловие
Статья, как и сам код, были написаны 4 года назад и отложены для проверки и правки. За эти 4 года вышло 2 стандарта C++ и одна мажорная версия Qt, но никаких существенных правок внесено не было. Я даже не проверил, работает ли ORM в 6-ой версии. (UPD: Работает после небольших правок deprecated методов и типов) Тем не менее, вернувшись назад, я посчитал, что её стоит опубликовать. Хотя бы для того, чтобы воодушевить других на исследование. Ведь если они достигнут большего успеха, чем я, — я тоже останусь в выигрыше. Будет на одну полезную библиотеку больше! А если не достигнут — то, как минимум, они будут знать, что они не одни такие, и что их результат, каким бы разочаровывающим он не был, — это всё равно результат.