Чаще всего на хабре люди делятся историями своего успеха. Вроде, «Ребята, я написал свою 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 методов и типов) Тем не менее, вернувшись назад, я посчитал, что её стоит опубликовать. Хотя бы для того, чтобы воодушевить других на исследование. Ведь если они достигнут большего успеха, чем я, — я тоже останусь в выигрыше. Будет на одну полезную библиотеку больше! А если не достигнут — то, как минимум, они будут знать, что они не одни такие, и что их результат, каким бы разочаровывающим он не был, — это всё равно результат.

Комментарии (5)


  1. kai3341
    30.09.2022 11:33

    Наработки интересные и однозначно заслуживают внимания. За ORM я бы порекомендовал поизучать принципы работы ORM на других языках. Я сам питонщик, и у нас напитоне много всяких разных ORM: от попсовой DjangoORM (однозадачная хр%нь) до гибкой SQLAlchemy (которая меня очень порадовала гибкой и расширяемой поддержкой SQL)

    И касательно "без счастливого конца" -- недавно сам грустил -- не знаю, как тестировать написанный код


    1. iCpu Автор
      30.09.2022 20:22

      Очень сложно проводить аналогии между ORM для Питона и плюсов. В том смысле, что расширение функционала сортировкой-фильтрами - is not a rocket science - дело чисто техническое. Решается через стек объектов-команд и усложнённый генератор запросов. А вот с наращиванием производительности - большая беда.

      На плюсах красивая рефлексия на данный момент практически отсутствует - и её было ещё меньше в начале-середине 2018 года, когда этот код писался. Поэтому любая попытка сделать ORM обречена либо на значительное изменение кода и использование кучи меток, либо на внешний препроцессор. В Qt метасистема пошла по обоим путям: с одной стороны, необходимо описывать структуру класса, оставляя макросы Q_OBJECT Q_GADGET и тп. С другой, поверх этого кода проходится moc, который для всех помеченных классов создаёт весьма уродливый код: там и касты в void*, и магические числа, и прочее прочее. Был даже цикл статей о том, как это работает - и почему. И, в общем-то, ничего сверхъестественного под капотом нет. Уродливое, не самое гибкое, порой, весьма неэффективное решение - но не сверхъестественное. И, главное, рабочее.

      Проблема ещё и в том, что и сам moc, и сгенерированный им код вообще не дружат с шаблонами. Автор moc даже сделал версию, поддерживающую шаблоны, потом написал "не, фигня какая-то получается" - и забросил её. Не могу его осуждать (но осуждаю). И constexpr в метасистеме не сказать, чтобы часто встречается. Поэтому, в оригинальном qt не получится перенести генерацию кода запросов на этап компиляции. Можно было бы, если бы да кабы. Но нельзя. А, значит, всё будет делаться на этапе исполнения, самым обобщённым (читай, медленным) способом. Отсюда и сумасшедшие показатели времени запроса select по сравнению с явным созданием структур. Правда, я не смотрел, что там подвезли в самые свежие версии. Быть может, теперь всё не так плохо.

      А по поводу вашей задачи, я не очень понял, в чём, собсно, проблема. Если у вас есть зоопарк, вы отдельно тестируете каждый вольер, отдельно - каждое животное. А потом тестируете их вместе, как единое целое. Или вы чисто технически не понимаете, как встроить тесты в ваши миксины? Начните с того, что тесты - это тоже миксины, которые чередуются с обычными миксинами и проверяют текущее состояние вашего приложения против известного нормального. Например, при рестарте последний завершающийся тестирующий миксин выключает свет и оставляет записку с PID'ом на пороге, а первый запускающийся тестовый проверяет, что этого PID'а с записки уже нет в живых. Или после каждого миксина каждого модуля ставится миксин тестов этого модуля против нужной версии и набора функций. И тд и тп.


      1. kai3341
        02.10.2022 03:18

        Очень сложно проводить аналогии между ORM для Питона и плюсов

        Аналогии -- интересный нюанс. Если вы про воспроизведение аналогичного API -- то это идея гиблая и ненужная. Я предлагаю смотреть, как решения устроены под капотом, и по мере необходимости эти решения в подходящем виде переносить себе. По сути я предлагаю обратить внимание на компилятор запросов, на предлагаемые клиентские подходы (см. bindparam), на кэшируемость объектов запросов, и так далее -- на низкоуровневые нюансы. На рефлексию, наконец -- это уберфича концепции ORM

        Поэтому, в оригинальном qt не получится перенести генерацию кода запросов на этап компиляции.

        Это не нужно. Нигде так не делают, это абсурдно. При необходимости подключиться вместо мускуля к постгресу вы на серьёзных щах предлагаете перекомпилировать клиентское приложение? Вместо этого сделайте такое API, чтобы объекты запросов (ВНИМАНИЕ -- это не только строки, это ещё и метаданые, во имя рефлексии) можно было кэшировать. Ни одно реальное приложение не обладает неограниченным списком запросов к БД, а CGI в далёком прошлом, чтобы переживать за единичное проседание производительности на первом запросе

        А по поводу вашей задачи, я не очень понял, в чём, собсно, проблема

        Кстати вообще не поняли. Как тестировать работу REPL? Как тестировать поведение REPL с учётом отправки команд и анализа результата? Как протестировать, не сломался ли REPL? Сейчас приходит понимание, что похожий с этой точки зрения инструмент уже есть, только он о другом и вообще из другого мира -- это selenium. Естественно, он не подходит для моей задачи. Значит, всё же надо городить огород с expect и его аналогами типа pexpect. И начинать с написания фреймворка для тестирования поведения терминала. Блин, оно мне надо?


        1. iCpu Автор
          02.10.2022 06:02

          По сути я предлагаю обратить внимание на компилятор запросов, на предлагаемые клиентские подходы (см. bindparam), на кэшируемость объектов запросов, и так далее -- на низкоуровневые нюансы.

          Учитывая поставленную задачу? Основные накладные расходы происходят внутри Qt при использовании сеттеров. Без изменения самого Qt любой выигрыш на кешированиях будет теряться в долях процентов. А без Qt нет рефлексии.

          Это не нужно. Нигде так не делают, это абсурдно. При необходимости подключиться вместо мускуля к постгресу вы на серьёзных щах предлагаете перекомпилировать клиентское приложение?

          Во-первых, я имел в виду прежде всего код оптимального обхода структур. Во-вторых, разве мускул и постгря так далеко разошлись от ANSI SQL, чтобы универсальные запросы не работали? Ну и в-третьих, а почему нет? Это плюсы, детка!

          Вместо этого сделайте такое API, чтобы объекты запросов (ВНИМАНИЕ -- это не только строки, это ещё и метаданые, во имя рефлексии) можно было кэшировать. 

          Плюсы - не Питон, они работают немного не так. Что толку кешировать типы, если передача всё равно будет по значению - с многократным копированием в процессе?

          Кстати вообще не поняли. Как тестировать работу REPL? Как тестировать поведение REPL с учётом отправки команд и анализа результата? Как протестировать, не сломался ли REPL?

          stdin + stdout? Терминал - и REPL - это обмен сообщений по стандартным пайпам. Весь REPL - это read_stdin->eval->write_stdout. А все вставки текста, автодополнения, цветные символы и прочие перделки - это управляющие последовательности и спецсимволы. Поэтому ваших страданий по тестированию зоопарка целиком я вдвойне не понял. Делайте прокси для пайп, записывайте тестовую сессию, воспроизводите её в режиме "без терминала".


          1. kai3341
            02.10.2022 15:11

            stdin + stdout? Терминал - и REPL - это обмен сообщений по стандартным пайпам.

            Очень внимательно читаем. Сути всё равно не уловили.