Ожидание — реальность.
История о метатипах 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<Brother> bros MEMBER m_bros) Q_PROPERTY(QList<int> drows MEMBER m_drows) public: QString m_name; Mom m_mama; Dad m_papa; QList<Brother> m_bros; QList<int> m_drows; }; Q_DECLARE_METATYPE(Ur)
bool init() { qRegisterType<Ur>("Ur"); qRegisterType<Dad>("Dad"); qRegisterType<Mom>("Mom"); qRegisterType<Brother>("Brother"); qRegisterType<Car>("Car"); } bool serialize(QList<Ur> 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<Brother> bros MEMBER m_bros) Q_PROPERTY(QList<int> drows MEMBER m_drows) public: QString m_name; Mom m_mama; Dad m_papa; QList<Brother> m_bros; QList<int> m_drows; }; ORM_DECLARE_METATYPE(Ur)
bool init() { ormRegisterType<Ur>("Ur"); ormRegisterType<Dad>("Dad"); ormRegisterType<Mom>("Mom"); ormRegisterType<Brother>("Brother"); ormRegisterType<Car>("Car"); } bool serialize(QList<Ur> const& urs) { ORM orm; orm.create<Ur>(); // 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>("Ur"); -> ormRegisterType<Ur>("Ur"); qRegisterType<Dad>("Dad"); -> ormRegisterType<Dad>("Ur"); qRegisterType<Mom>("Mom"); -> ormRegisterType<Mom>("Ur"); qRegisterType<Brother>("Brother");-> ormRegisterType<Brother>("Ur"); qRegisterType<Car>("Car"); -> ormRegisterType<Car>("Ur"); /* sql hell */ -> ORM orm; orm.create<Ur>(); // 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<QObject*>(), value); else return property.writeOnGadget(writeInto.data(), value); } inline QVariant read(bool isQObject, QVariant const& readFrom, QMetaProperty property) { if (isQObject) { QObject * object = readFrom.value<QObject*>(); return property.read(object); } else { return property.readOnGadget(readFrom.value<void*>()); } }
Казалось бы, 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<QObject*>() == nullptr) { qWarning() << "Unable to create instance of QObject " << meta.className(); } return variant; }
Шаг 3. Реализовать сериализацию тривиальных типов.
Под тривиальными типами будем понимать числа, строки и бинарные поля. Вроде бы задача простая, снова берём QVariant и в бой. Но есть нюанс. В ряде случаев нам может захотеться сделать «тривиальными» иные типы, например, изображение. С одной стороны, можно было бы просто проверять, есть ли у метатипа нужные конвертеры и использовать их. Но это не самый удачный способ, тем более, что он чреват возникновением конфликтов, так что лучше иметь списки типов и способы их сохранения: в строку, в BLOB или отдать на откуп Qt. На этом же шаге лучше заиметь список тех типов, с которыми вы предпочтёте не связываться. Из стандартных это могут быть JSON-объекты или QModelIndex. Опять же, никакой магии, статические списки.
Шаг 4. Реализовать сериализацию нетривиальных типов: структур, указателей, контейнеров.
И опять, разработчики постарались: их QVariant решает эту задачу. Или нет?
Проблема 1: связность указателя и типа, шаблона и типов-параметров.
Для произвольного метакласса нельзя ни получить связные с ним метаклассы указателей (или структуры), ни получить тип, хранимый в шаблоне. Это очень печально, хотя и вполне предсказуемо. Откуда ей взяться?
Неоткуда.
Можно, конечно, поиграться с именем класса, пощекотав параметры шаблонов, но это очень нежное решение, которое ломается о грубую реальность typedef. Что же, иного не остаётся, придётся завести свою функцию для регистрации типов.
template <typename T> int orm::Register(const char * c) { int type = qMetaTypeId<T>(); if (!type) { if (c) { type = qRegisterMetaType<T>(c); } else { type = qRegisterMetaType<T>(); } } Config::addPointerStub(orm::Pointers::registerTypePointers<T>()); orm::Containers::registerSequentialContainers<T>(); return type; }
А вместе с ней и статический массивчик под это дело. Точнее, QMap, где ключом будет id метакласса, а значением — структура, хранящая все связные типы.
// <h> struct ORMPointerStub { int T =0; // T int pT=0; // T* int ST=0; // QSharedPointer<T> int WT=0; // QWeakPointer<T> int sT=0; // std::shared_ptr<T> int wT=0; // std::weak_ptr<T> }; // <cpp> static QMap<int, orm_pointers::ORMPointerStub> 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; } // <h> template <typename T> void* toVPointer ( T const& t) { return reinterpret_cast<void*>(const_cast<T*>(&t )); } template <typename T> void* toVPointerP( T * t) { return reinterpret_cast<void*>( t ); } template <typename T> void* toVPointerS(QSharedPointer <T> const& t) { return reinterpret_cast<void*>(const_cast<T*>( t.data())); } template <typename T> void* toVPointers(std::shared_ptr<T> const& t) { return reinterpret_cast<void*>(const_cast<T*>( t.get ())); } template <typename T> T* fromVoidP(void* t) { return reinterpret_cast<T*>(t) ; } template <typename T> QSharedPointer <T> fromVoidS(void* t) { return QSharedPointer <T>(reinterpret_cast<T*>(t)); } template <typename T> std::shared_ptr<T> fromVoids(void* t) { return std::shared_ptr<T>(reinterpret_cast<T*>(t)); } template <typename T> ORMPointerStub registerTypePointersEx() { ORMPointerStub stub; stub.T = qMetaTypeId<T>(); stub.pT = qRegisterMetaType<T*>(); stub.ST = qRegisterMetaType<QSharedPointer <T>>(); stub.WT = qRegisterMetaType<QWeakPointer <T>>(); stub.sT = qRegisterMetaType<std::shared_ptr<T>>(); stub.wT = qRegisterMetaType<std::weak_ptr <T>>(); QMetaType::registerConverter< T , void*>(&toVPointer <T>); QMetaType::registerConverter< T*, void*>(&toVPointerP<T>); QMetaType::registerConverter<QSharedPointer <T>, void*>(&toVPointerS<T>); QMetaType::registerConverter<std::shared_ptr<T>, void*>(&toVPointers<T>); QMetaType::registerConverter<void*, T*>(&fromVoidP<T>); QMetaType::registerConverter<void*, QSharedPointer <T>>(&fromVoidS<T>); QMetaType::registerConverter<void*, std::shared_ptr<T>>(&fromVoids<T>); return stub; }
Как вы могли заметить, тут уже были зарегистрированы конвертеры T>void*, T*>void* и void*>T*. Ничего особенного, они нам потребуются для спокойной работы с QMetaProperty, так как в select, где будут создаваться элементы, мы будем делать простые указатели, а передавать вообще универсальный void*. Нужный тип указателя будет создан самим QVariant в момент записи.
Проблема 2: обработка контейнеров.
С контейнерами не всё так плохо. Для последовательных есть простой способ узнать, является ли переданный нам тип зарегистрированным:
bool isSequentialContainer(int metaTypeID){ return QMetaType::hasRegisteredConverterFunction(metaTypeID, qMetaTypeId<QtMetaTypePrivate::QSequentialIterableImpl>()); }
Пробежаться по нему:
QSequentialIterable sequentialIterable = myList.value<QSequentialIterable>(); for (QVariant variant : sequentialIterable) { // do stuff }
И даже получить ID хранимого метатипа (осторожно — глаза!)
inline int getSequentialContainerStoredType(int metaTypeID) { return (*(QVariant(static_cast<QVariant::Type>(metaTypeID)) .value<QSequentialIterable>()).end()).userType(); // да, .end()).userType(); // мне стыдно, хорошо? }
Так что сохранение данных становится делом чисто техническим. Остаётся лишь справиться со всем многообразием контейнеров. Моя реализация затрагивает лишь те, которые можно получить кастами из QList. Во-первых, потому, что результатом QSqlQuery является QVariantList, а, во-вторых, потому, что он может кастоваться во все основные Qt и std контейнеры. (Есть и третья причина, шаблонная магия std плохо впихивается в универсальные короткие решения.)
template <typename T> QList<T> qListFromQVariantList(QVariant const& variantList) { QList<T> list; QSequentialIterable sequentialIterable = variantList.value<QSequentialIterable>(); for (QVariant const& variant : sequentialIterable) { if(v.canConvert<T>()) { list << variant.value<T>(); } } return list; } template <typename T> QVector <T> qVectorFromQVariantList(QVariant const& v) { return qListFromQVariantList<T>(v).toVector (); } template <typename T> std::list <T> stdListFromQVariantList(QVariant const& v) { return qListFromQVariantList<T>(v).toStdList (); } template <typename T> std::vector<T> stdVectorFromQVariantList(QVariant const& v) { return qListFromQVariantList<T>(v).toVector().toStdVector(); } template <typename T> void registerTypeSequentialContainers() { qMetaTypeId<QList <T>>() ? qMetaTypeId<QList <T>>() : qRegisterMetaType<QList <T>>(); qMetaTypeId<QVector <T>>() ? qMetaTypeId<QVector <T>>() : qRegisterMetaType<QVector <T>>(); qMetaTypeId<std::list <T>>() ? qMetaTypeId<std::list <T>>() : qRegisterMetaType<std::list <T>>(); qMetaTypeId<std::vector<T>>() ? qMetaTypeId<std::vector<T>>() : qRegisterMetaType<std::vector<T>>(); QMetaType::registerConverter<QVariantList, QList <T>>(&( qListFromQVariantList<T>)); QMetaType::registerConverter<QVariantList, QVector <T>>(&( qVectorFromQVariantList<T>)); QMetaType::registerConverter<QVariantList, std::list <T>>(&( stdListFromQVariantList<T>)); QMetaType::registerConverter<QVariantList, std::vector<T>>(&(stdVectorFromQVariantList<T>)); }
С ассоциативными контейнерами и парами дела обстоят хуже. Несмотря на то, что для них есть аналогичный по функциональности с QSequentialIterable класс QAssociativeIterable, некоторые сценарии его использования приводят к вылетам программы. Поэтому нас снова ожидают старые друзья: структура и статический массив, которые нужны для выяснения хранившегося в контейнере типа. Кроме того, нам потребуется тип-прокладка, который бы смог сохранить промежуточные результаты select для каждой строки. Можно было бы использовать QPair<QVariant,QVariant>, но я решил создать собственный тип, чтобы избежать конфликтов преобразования.
// Код становится всё больше и всё скучнее. Если интересно, 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 <typename K, typename T> QMap<K,T> qMapFromQVariantMap(QVariant const& v) { QMap<K,T> list; QAssociativeIterable ai = v.value<QAssociativeIterable>(); QAssociativeIterable::const_iterator it = ai.begin(); const QAssociativeIterable::const_iterator end = ai.end(); for ( ; it != end; ++it) { if(it.key().canConvert<K>() && it.value().canConvert<T>()) { list.insert(it.key().value<K>(), it.value().value<T>()); } } return list; } template <typename K, typename T> QList<ORM_QVariantPair> qMapToPairListStub(QMap<K,T> const& v) { QList<ORM_QVariantPair> 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 <typename K, typename T> void registerQPair() { ORM_Config::addPairType(qMetaTypeId<K>(), qMetaTypeId<T>(), qMetaTypeId<QPair <K,T>>() ? qMetaTypeId<QPair <K,T>>() : qRegisterMetaType<QPair <K,T>>()); QMetaType::registerConverter<QVariant, QPair<K,T>>(&(qPairFromQVariant<K,T>)); QMetaType::registerConverter<QVariantList, QPair<K,T>>(&(qPairFromQVariantList<K,T>)); QMetaType::registerConverter<ORM_QVariantPair, QPair<K,T>>(&(qPairFromPairStub<K,T>)); QMetaType::registerConverter<QPair<K,T>, ORM_QVariantPair>(&(toQPairStub<K,T>)); } template <typename K, typename T> void registerQMap() { registerQPair<K,T>(); ORM_Config::addContainerPairType(qMetaTypeId<K>(), qMetaTypeId<T>(), qMetaTypeId<QMap <K,T>>() ? qMetaTypeId<QMap <K,T>>() : qRegisterMetaType<QMap <K,T>>()); QMetaType::registerConverter<QMap<K,T>, QList<ORM_QVariantPair>>(&(qMapToPairListStub<K,T>)); QMetaType::registerConverter<QVariantMap , QMap<K,T>>(&(qMapFromQVariantMap<K,T>)); QMetaType::registerConverter<QVariantList , QMap<K,T>>(&(qMapFromQVariantList<K,T>)); QMetaType::registerConverter<QList <ORM_QVariantPair>, QMap<K,T>>(&(qMapFromPairListStub<K,T>)); } 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<int> 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<int> query_types; query_columns << ORM_Impl::orm_rowidName; query_types << qMetaTypeId<long long>(); 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<int> &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> car READ getCar WRITE setCar SCRIPTABLE false) public: Car m_car; ormReferenсe<Car> getCar() const { return ormReferenсe<Car>(&m_car); } void setCar(ormReferenсe<Car> car) { if (car) m_car = *car; } };
Такое решение позволяет значительно ускорить ORM. Падение скорости работы всё ещё значительное, в разы, но уже не на порядки. Тем не менее, решение это flawed by design, требующее изменять кучу кода. А если это в любом случае нужно делать, не проще ли сразу написать генератор SQL запросов? Увы, проще, и работает такой код разительно быстрее. Потому моя достаточно большая и интересная работа осталась пылиться в углу.
Вместо вывода
Жалею ли я, что потратил несколько месяцев на её написание? Чёрт подери, нет! Это было очень интересное погружение внутрь существующей и работающей метасистемы, которое немного изменило мой взгляд на программирование. Я предполагал такой результат, когда приступал к работе. Надеялся на лучшее, но предполагал примерно такой. Я получил его на выходе. И он меня устроил!
Послесловие
Статья, как и сам код, были написаны 4 года назад и отложены для проверки и правки. За эти 4 года вышло 2 стандарта C++ и одна мажорная версия Qt, но никаких существенных правок внесено не было. Я даже не проверил, работает ли ORM в 6-ой версии. (UPD: Работает после небольших правок deprecated методов и типов) Тем не менее, вернувшись назад, я посчитал, что её стоит опубликовать. Хотя бы для того, чтобы воодушевить других на исследование. Ведь если они достигнут большего успеха, чем я, — я тоже останусь в выигрыше. Будет на одну полезную библиотеку больше! А если не достигнут — то, как минимум, они будут знать, что они не одни такие, и что их результат, каким бы разочаровывающим он не был, — это всё равно результат.
ссылка на оригинал статьи https://habr.com/ru/post/452588/
Добавить комментарий