Как стать автором
Обновить

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

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

Публикации

Истории

Работа

Программист C++
109 вакансий
QT разработчик
4 вакансии

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань