Tiny-qORM: рассказ без счастливого конца

от автора

Чаще всего на хабре люди делятся историями своего успеха. Вроде, «Ребята, я написал свою ORM, качайте, ставьте ллойсы!» Эта история будет немного другая. В ней я расскажу о неуспехе, который считаю своим серьёзным достижением.


Ожидание — реальность.

История о метатипах Qt, написании велосипедов, превышении максимального числа записей в объектном файле и, неожиданно, инструменте, который работает так, как и было задумано.

С чего всё началось? Предсказуемо, с лени. Как-то раз появилась задача (де-)сериализации структур в SQL. Большого числа структур, несколько сотен. Естественно, с разными уровнями вложенности, с указателями и контейнерами. Помимо прочего, имелась определяющая особенность: все они уже имели привязку к QJSEngine, то есть имели полноценную метасистему Qt.

С такими вводными не мудрено было придти к написанию своей ORM и поставить весьма амбициозные цели:
1) Минимальная модификация сохраняемых структур. В лучшем случае, без оной вообще, в худшем — Ctrl+Shift+F.
2) Работа с любыми типами, контейнерами и указателями.
3) Не самые страшные таблицы с возможностью их использования вне ORM.

И обозначить предсказуемые ограничения:
1) Таблицы создаются только для классов с метаинформацией (Q_OBJECT\Q_GADGET) для их свойств (Q_PROPERTY). Все зарегистрированные в метасистеме типы, не имеющие метаинформации, будут сохраняться либо в виде строк, либо в виде сырых данных. Если преобразование не существует или тип неизвестен, он пропускается.

Забегая вперёд, получилось следующее:

До ORM

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 */  } 

После ORM

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); } 

Diff

        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

Simple

    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; }     };

Complex

        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/