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

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

qa9uyserftbj6r5cn-4rvgjcvty.jpeg
Ожидание — реальность.

История о метатипах 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 bros MEMBER m_bros)
    Q_PROPERTY(QList drows MEMBER m_drows)
public:
    QString m_name;
    Mom m_mama;
    Dad m_papa;
    QList m_bros;
    QList m_drows;
};
Q_DECLARE_METATYPE(Ur)
bool init()
{ 
        qRegisterType("Ur");
        qRegisterType("Dad");
        qRegisterType("Mom");
        qRegisterType("Brother");
        qRegisterType("Car");
}
bool serialize(QList const& urs)
{ 
    /* SQL hell */ 
}


После 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 bros MEMBER m_bros)
    Q_PROPERTY(QList drows MEMBER m_drows)
public:
    QString m_name;
    Mom m_mama;
    Dad m_papa;
    QList m_bros;
    QList m_drows;
};
ORM_DECLARE_METATYPE(Ur)
bool init()
{ 
        ormRegisterType("Ur");
        ormRegisterType("Dad");
        ormRegisterType("Mom");
        ormRegisterType("Brother");
        ormRegisterType("Car");
}
bool serialize(QList const& urs)
{ 
         ORM orm;
         orm.create(); // if not exists
         orm.insert(urs);
}


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");         -> ormRegisterType("Ur");
        qRegisterType("Dad");        -> ormRegisterType("Ur");
        qRegisterType("Mom");        -> ormRegisterType("Ur");
        qRegisterType("Brother");-> ormRegisterType("Ur");
        qRegisterType("Car");        -> ormRegisterType("Ur");

         /* sql hell */ -> ORM orm;
                           orm.create(); // if not exists
                           orm.insert(urs);

Making of…


Шаг 1. Получать метаинформацию, список полей класса и их значения.


Спасибо разработчикам Qt, мы это можем делать по щелчку пальцев и id метакласса. Выглядит это примерно так:

const QMetaObject * object = QMetaType::metaObjectForType(id);
if (object) {
    for (int i = 0; i < object->propertyCount(); ++i) {
        QMetaProperty property = object->property(i);
        columns << property.name();
        types << property.userType();
    }
}


Чтение и запись так же не вызывают проблем. Почти. QMetaProperty имеет пару методов чтения-записи для объектов. И ещё одну пару для гаджетов. Поэтому на этапе чтения-записи нам нужно определиться, в кого мы пишем. Делается это так:

bool isQObject(QMetaObject const& meta) {
    return meta.inherits(QMetaType::metaObjectForType(QMetaType::QObjectStar));
}


Тогда чтение и запись производятся следующим образом:

inline bool write(bool isQObject, QVariant & writeInto, 
                  QMetaProperty property, QVariant const& value) {
    if (isQObject) return property.write(writeInto.value(), value);
    else return property.writeOnGadget(writeInto.data(), value);
}

inline QVariant read(bool isQObject, QVariant const& readFrom, 
                     QMetaProperty property) {
    if (isQObject) {
        QObject * object = readFrom.value();
        return property.read(object);
    }
    else {
        return property.readOnGadget(readFrom.value());
    }
}


Казалось бы, readOnGadget, в конце концов, вызывает тот же read, так что зачем городить весь этот код? Совместимость и отсутствие гарантий, что такое поведение не изменится.

И ещё один нюанс. При сохранении Q_ENUM в QVariant его значение кастуется в int. В базу данных тоже поступает int. Но записать int в свойство типа Q_ENUM мы не можем. Поэтому перед записью мы должны проверить, является ли указанное свойство перечислением — и вызвать явное преобразование в таком случае. Звучит страшнее, чем есть на самом деле.

if (property.isEnumType()) {
   variant.convert(property.userType());
}

Шаг 2. Создавать произвольные структуры по метаинформации.


Снова бьём челом разработчикам за класс QVariant и его конструктор QVariant (int id, void* copy). С его помощью можно создать любую структуру с пустым конструктором — и это хорошая новость. Плохая новость: наследники QObject в список не входят. Хорошая новость: их можно делать с помощью QMetaObject: newInstance ().

Создание экземпляра произвольного типа будет выглядеть примерно так:

QVariant make_variant(QMetaObject const& meta) {
    QVariant variant;
    if (isQObject(meta)) {
        QObject * obj = meta.newInstance();
        if (obj) {
            obj->setObjectName("orm_made");
            obj->setParent(QCoreApplication::instance());
            variant = QVariant::fromValue(obj);
        }
    }
    else {
        variant = QVariant((classtype), nullptr);
    }
    if (!variant.isValid()){
        qWarning() << "Unable to create instance of type " << meta.className();
    }
    if (isQObject(meta) && variant.value() == nullptr) {
        qWarning() << "Unable to create instance of QObject " << meta.className();
    }
    return variant;
}

Шаг 3. Реализовать сериализацию тривиальных типов.


Под тривиальными типами будем понимать числа, строки и бинарные поля. Вроде бы задача простая, снова берём QVariant и в бой. Но есть нюанс. В ряде случаев нам может захотеться сделать «тривиальными» иные типы, например, изображение. С одной стороны, можно было бы просто проверять, есть ли у метатипа нужные конвертеры и использовать их. Но это не самый удачный способ, тем более, что он чреват возникновением конфликтов, так что лучше иметь списки типов и способы их сохранения: в строку, в BLOB или отдать на откуп Qt. На этом же шаге лучше заиметь список тех типов, с которыми вы предпочтёте не связываться. Из стандартных это могут быть JSON-объекты или QModelIndex. Опять же, никакой магии, статические списки.

Шаг 4. Реализовать сериализацию нетривиальных типов: структур, указателей, контейнеров.


И опять, разработчики постарались: их QVariant решает эту задачу. Или нет?

Проблема 1: связность указателя и типа, шаблона и типов-параметров.

Для произвольного метакласса нельзя ни получить связные с ним метаклассы указателей (или структуры), ни получить тип, хранимый в шаблоне. Это очень печально, хотя и вполне предсказуемо. Откуда ей взяться?

Неоткуда.

Можно, конечно, поиграться с именем класса, пощекотав параметры шаблонов, но это очень нежное решение, которое ломается о грубую реальность typedef. Что же, иного не остаётся, придётся завести свою функцию для регистрации типов.

template  int orm::Register(const char * c)
{
            int type = qMetaTypeId();
            if (!type) {
                if (c) {
                    type = qRegisterMetaType(c);
                }
                else {
                    type = qRegisterMetaType();
                }
            }
            Config::addPointerStub(orm::Pointers::registerTypePointers());
            orm::Containers::registerSequentialContainers();
            return type;
}


А вместе с ней и статический массивчик под это дело. Точнее, QMap, где ключом будет id метакласса, а значением — структура, хранящая все связные типы.

Выглядит это, конечно, пошловато, но работает.
Серьёзно, вы вряд ли тут найдёте что-нибудь принципиально новое.
// 
struct ORMPointerStub {
    int T =0;  // T
    int pT=0; // T*
    int ST=0; // QSharedPointer
    int WT=0; // QWeakPointer
    int sT=0; // std::shared_ptr
    int wT=0; // std::weak_ptr
};
// 
static QMap pointerMap;
void ORM_Config::addPointerStub(const orm_pointers::ORMPointerStub & stub)
{
    if (stub. T) pointerMap[stub. T] = stub;
    if (stub.pT) pointerMap[stub.pT] = stub;
    if (stub.ST) pointerMap[stub.ST] = stub;
    if (stub.WT) pointerMap[stub.WT] = stub;
    if (stub.sT) pointerMap[stub.sT] = stub;
    if (stub.wT) pointerMap[stub.wT] = stub;
}
// 
template  void* toVPointer (                T  const& t)
    { return reinterpret_cast(const_cast(&t       )); }
template  void* toVPointerP(                T *       t)
    { return reinterpret_cast(                t        ); }
template  void* toVPointerS(QSharedPointer  const& t)
    { return reinterpret_cast(const_cast( t.data())); }
template  void* toVPointers(std::shared_ptr const& t)
    { return reinterpret_cast(const_cast( t.get ())); }

template  T* fromVoidP(void* t)
    { return                    reinterpret_cast(t) ; }
template  QSharedPointer  fromVoidS(void* t)
    { return QSharedPointer (reinterpret_cast(t)); }
template  std::shared_ptr fromVoids(void* t)
    { return std::shared_ptr(reinterpret_cast(t)); }

template  ORMPointerStub registerTypePointersEx()
{
    ORMPointerStub stub;
    stub.T = qMetaTypeId();
    stub.pT = qRegisterMetaType();
    stub.ST = qRegisterMetaType>();
    stub.WT = qRegisterMetaType>();
    stub.sT = qRegisterMetaType>();
    stub.wT = qRegisterMetaType>();

    QMetaType::registerConverter<                T , void*>(&toVPointer );
    QMetaType::registerConverter<                T*, void*>(&toVPointerP);
    QMetaType::registerConverter, void*>(&toVPointerS);
    QMetaType::registerConverter, void*>(&toVPointers);

    QMetaType::registerConverter(&fromVoidP);
    QMetaType::registerConverter>(&fromVoidS);
    QMetaType::registerConverter>(&fromVoids);
    return stub;
}

Как вы могли заметить, тут уже были зарегистрированы конвертеры T>void*, T*>void* и void*>T*. Ничего особенного, они нам потребуются для спокойной работы с QMetaProperty, так как в select, где будут создаваться элементы, мы будем делать простые указатели, а передавать вообще универсальный void*. Нужный тип указателя будет создан самим QVariant в момент записи.

Проблема 2: обработка контейнеров.

С контейнерами не всё так плохо. Для последовательных есть простой способ узнать, является ли переданный нам тип зарегистрированным:

bool isSequentialContainer(int metaTypeID){
    return QMetaType::hasRegisteredConverterFunction(metaTypeID, 
                qMetaTypeId());
}

Пробежаться по нему:

QSequentialIterable sequentialIterable = myList.value();
for (QVariant variant : sequentialIterable) {
    // do stuff
}


И даже получить ID хранимого метатипа (осторожно — глаза!)

inline int getSequentialContainerStoredType(int metaTypeID)
{
    return (*(QVariant(static_cast(metaTypeID))
                .value()).end()).userType();
// да, .end()).userType();
// мне стыдно, хорошо?
}


Так что сохранение данных становится делом чисто техническим. Остаётся лишь справиться со всем многообразием контейнеров. Моя реализация затрагивает лишь те, которые можно получить кастами из QList. Во-первых, потому, что результатом QSqlQuery является QVariantList, а, во-вторых, потому, что он может кастоваться во все основные Qt и std контейнеры. (Есть и третья причина, шаблонная магия std плохо впихивается в универсальные короткие решения.)

template  QList qListFromQVariantList(QVariant const& variantList)
{
    QList list;
    QSequentialIterable sequentialIterable = variantList.value();
    for (QVariant const& variant : sequentialIterable) {
        if(v.canConvert()) {
            list << variant.value();
        }
    }
    return list;
}
template  QVector       qVectorFromQVariantList(QVariant const& v) 
    { return qListFromQVariantList(v).toVector              (); }
template  std::list     stdListFromQVariantList(QVariant const& v) 
    { return qListFromQVariantList(v).toStdList             (); }
template  std::vector stdVectorFromQVariantList(QVariant const& v) 
    { return qListFromQVariantList(v).toVector().toStdVector(); }

template  void registerTypeSequentialContainers()
{
    qMetaTypeId>() ? qMetaTypeId>() 
                                  : qRegisterMetaType>();
    qMetaTypeId>() ? qMetaTypeId>() 
                                  : qRegisterMetaType>();
    qMetaTypeId>() ? qMetaTypeId>() 
                                  : qRegisterMetaType>();
    qMetaTypeId>() ? qMetaTypeId>() 
                                  : qRegisterMetaType>();
    QMetaType::registerConverter>(&(    qListFromQVariantList));
    QMetaType::registerConverter>(&(  qVectorFromQVariantList));
    QMetaType::registerConverter>(&(  stdListFromQVariantList));
    QMetaType::registerConverter>(&(stdVectorFromQVariantList));
}


С ассоциативными контейнерами и парами дела обстоят хуже. Несмотря на то, что для них есть аналогичный по функциональности с QSequentialIterable класс QAssociativeIterable, некоторые сценарии его использования приводят к вылетам программы. Поэтому нас снова ожидают старые друзья: структура и статический массив, которые нужны для выяснения хранившегося в контейнере типа. Кроме того, нам потребуется тип-прокладка, который бы смог сохранить промежуточные результаты select для каждой строки. Можно было бы использовать QPair, но я решил создать собственный тип, чтобы избежать конфликтов преобразования.

// Код становится всё больше и всё скучнее. Если интересно, https://github.com/iiiCpu/Tiny-qORM/blob/master/ORM/orm.h


Скрытый текст
Я смотрю, ты упорный. На.

    struct ORM_QVariantPair //: public ORMValue
    {
        Q_GADGET
        Q_PROPERTY(QVariant key MEMBER key)
        Q_PROPERTY(QVariant value MEMBER value)
    public:
        QVariant key, value;
        QVariant& operator[](int index){ return index == 0 ? key : value; }
    };

    template  QMap qMapFromQVariantMap(QVariant const& v)
    {
        QMap list;
        QAssociativeIterable ai = v.value();
        QAssociativeIterable::const_iterator it = ai.begin();
        const QAssociativeIterable::const_iterator end = ai.end();
        for ( ; it != end; ++it) {
            if(it.key().canConvert() && it.value().canConvert()) {
                list.insert(it.key().value(), it.value().value());
            }
        }
        return list;
    }

    template  QList qMapToPairListStub(QMap const& v)
    {
        QList psl;
        for (auto i = v.begin(); i != v.end(); ++i) {
            ORM_QVariantPair ps;
            ps.key = QVariant::fromValue(i.key());
            ps.value = QVariant::fromValue(i.value());
            psl << ps;
        }
        return psl;
    }

    template  void registerQPair()
    {
        ORM_Config::addPairType(qMetaTypeId(), qMetaTypeId(),
                                qMetaTypeId>() ? qMetaTypeId>() : qRegisterMetaType>());
        QMetaType::registerConverter>(&(qPairFromQVariant));
        QMetaType::registerConverter>(&(qPairFromQVariantList));
        QMetaType::registerConverter>(&(qPairFromPairStub));
        QMetaType::registerConverter, ORM_QVariantPair>(&(toQPairStub));
    }
    template  void registerQMap()
    {
        registerQPair();

        ORM_Config::addContainerPairType(qMetaTypeId(), qMetaTypeId(),
                                         qMetaTypeId>() ? qMetaTypeId>() : qRegisterMetaType>());
        QMetaType::registerConverter, QList>(&(qMapToPairListStub));
        QMetaType::registerConverter>(&(qMapFromQVariantMap));
        QMetaType::registerConverter>(&(qMapFromQVariantList));
        QMetaType::registerConverter, QMap>(&(qMapFromPairListStub));
    }

uint qHash(ORM_QVariantPair const& variantPair) noexcept;
Q_DECLARE_METATYPE(ORM_QVariantPair)


Проблема 3: использование контейнеров.

У контейнеров есть ещё одна проблема: они не являются структурой. Вот такой вот внезапный удар поддых от Капитана Очевидности! На самом деле, всё просто: у контейнеров нет полей и метаобъекта, а, значит, мы должны их обрабатывать отдельно, пропихивая заглушки. Точнее, не так. Нам нужно обрабатывать отдельно последовательные контейнеры с тривиальными типами и отдельно — ассоциативные контейнеры, так как последовательные контейнеры из структур запросто обрабатываются, как простые структуры. С первыми можно схитрить, преобразовав их в строку или BLOB (нужные методы в QList есть из коробки). Со вторыми же ничего не поделать: придётся дублировать все методы, пропихивая вместо настоящих Q_PROPERTY заглушки key и value.

До
QVariant ORM::meta_select(const QMetaObject &meta, QString const& parent_name, 
                          QString const& property_name, long long parent_orm_rowid)
{
    QString table_name = generate_table_name(parent_name, property_name, 
                                             QString(meta.className()),QueryType::Select);
    int classtype = QMetaType::type(meta.className());
    bool isQObject = ORM_Impl::isQObject(meta);
    bool with_orm_rowid = ORM_Impl::withRowid(meta);
    if (!selectQueries.contains(table_name)) {
        QStringList query_columns;
        QList query_types;
        for (int i = 0; i < meta.propertyCount(); ++i) {
            QMetaProperty property = meta.property(i);
            if (ORM_Impl::isIgnored(property.userType())) {
                continue;
            }


После
QVariant ORM::meta_select_pair  (int metaTypeID, QString const& parent_name, 
                               QString const& property_name, long long parent_orm_rowid)
{
    QString className = QMetaType::typeName(metaTypeID);
    QString table_name = generate_table_name(parent_name, property_name, className, QueryType::Select);
    int keyType = ORM_Impl::getAssociativeContainerStoredKeyType(metaTypeID);
    int valueType = ORM_Impl::getAssociativeContainerStoredValueType(metaTypeID);
    if (!selectQueries.contains(table_name)) {
        QStringList query_columns;
        QList query_types;
        query_columns << ORM_Impl::orm_rowidName;
        query_types << qMetaTypeId();
        for (int column = 0; column < 2; ++column) {
            int userType = column == 0 ? keyType : valueType;
            QString name = column == 0 ? "key" : "value";
            if (ORM_Impl::isIgnored(userType)) {
                continue;
            }


В итоге мы получили однородный доступ на чтение и запись ко всем используемым типам и структурам с возможностью их рекурсивного обхода.

Шаг 5. Написать SQL запросы.


Для написания SQL запроса нам достаточно иметь метатип класса, имя поля в родительской структуре, имя родительской таблицы, список имён и метатипов полей. Из первых трёх сконструируем имя таблицы, из остального столбцы.

 QString ORM::generate_update_query(QString const& parent_name,
                 QString const& property_name, const QString &class_name,
                 const QStringList &names, const QList &types,
                 bool parent_orm_rowid) const
{
    Q_UNUSED(types)
    QString table_name = generate_table_name(parent_name, 
                       property_name, class_name, QueryType::Update);
    QString query_text = QString("UPDATE OR IGNORE %1 SET ").arg(table_name);
    QStringList t_set;
    for (int i = 0; i < names.size(); ++i) {
        t_set << normalize(names[i], QueryType::Update) + " = " + 
                normalizeVar(":" + names[i], types[i], QueryType::Update);
    }
    query_text += t_set.join(',') + " WHERE " + 
                  normalize(ORM_Impl::orm_rowidName, QueryType::Update) + " = :" + 
                  ORM_Impl::orm_rowidName + " ";
    if (parent_orm_rowid) {
        query_text += " AND " + ORM_Impl::orm_parentRowidName + " = :" + 
                      ORM_Impl::orm_parentRowidName + " ";
    }
    query_text += ";";
    return query_text;
}


О чём не стоит забывать:
1) Нормализация имён. Дело не только в регистре, типы могут содержать в себе скобки и запятые шаблонов, двоеточия пространств имён. От всего этого многообразия следует избавляться.

QString ORM::normalize(const QString & str, QueryType queryType) const
{
    Q_UNUSED(queryType)
    QString s = str;
    static QRegularExpression regExp1 {"(.)([A-Z]+)"};
    static QRegularExpression regExp2 {"([a-z0-9])([A-Z])"};
    static QRegularExpression regExp3 {"[:;,.<>]+"};
    return "_" + s.replace(regExp1, "\\1_\\2")
                 .replace(regExp2, "\\1_\\2").toLower()
                 .replace(regExp3, "_");
}


2) Приведения типов. Если работа ведётся с SQLite, то всё просто: кто бы ты ни был, ты — строка. Но если используются другие БД, порой, без каста не обойтись. Значит, при вставке или обновлении нормализованное значение (плейсхолдер) нужно дополнительно преобразовать, да и при выборе тоже.

И в чём же проблема? Почему «неуспех»?


Думаю, многим ответ уже очевиден. Скорость работы. На простых структурах падение скорости составляет 10% на запись и 100% на чтение. На структуре с глубиной вложенности 1 — уже 30% и 700%. На глубине 2 — 50% и 2000%. С повышением вложенности скорость работы падает экспоненциально.

Simple sqlite[10000]:
ORM: insert= 2160 select= 56
QSqlQuery: insert= 1352 select= 53
RAW: insert= 1271 select= 3

Complex sqlite[10000]:
ORM: insert= 7231 select= 24095
QSqlQuery: insert= 4594 select= 127
RAW: insert= 1117 select= 7

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 READ getCar WRITE setCar SCRIPTABLE false)
public:
    Car m_car;
    ormReferenсe getCar() const { return ormReferenсe(&m_car); }
    void setCar(ormReferenсe car) { if (car) m_car = *car; }
};


Такое решение позволяет значительно ускорить ORM. Падение скорости работы всё ещё значительное, в разы, но уже не на порядки. Тем не менее, решение это flawed by design, требующее изменять кучу кода. А если это в любом случае нужно делать, не проще ли сразу написать генератор SQL запросов? Увы, проще, и работает такой код разительно быстрее. Потому моя достаточно большая и интересная работа осталась пылиться в углу.

Вместо вывода


Жалею ли я, что потратил несколько месяцев на её написание? Чёрт подери, нет! Это было очень интересное погружение внутрь существующей и работающей метасистемы, которое немного изменило мой взгляд на программирование. Я предполагал такой результат, когда приступал к работе. Надеялся на лучшее, но предполагал примерно такой. Я получил его на выходе. И он меня устроил!

Послесловие


Статья, как и сам код, были написаны 4 года назад и отложены для проверки и правки. За эти 4 года вышло 2 стандарта C++ и одна мажорная версия Qt, но никаких существенных правок внесено не было. Я даже не проверил, работает ли ORM в 6-ой версии. (UPD: Работает после небольших правок deprecated методов и типов) Тем не менее, вернувшись назад, я посчитал, что её стоит опубликовать. Хотя бы для того, чтобы воодушевить других на исследование. Ведь если они достигнут большего успеха, чем я, — я тоже останусь в выигрыше. Будет на одну полезную библиотеку больше! А если не достигнут — то, как минимум, они будут знать, что они не одни такие, и что их результат, каким бы разочаровывающим он не был, — это всё равно результат.

© Habrahabr.ru