Чаще всего на хабре люди делятся историями своего успеха. Вроде, «Ребята, я написал свою ORM, качайте, ставьте ллойсы!» Эта история будет немного другая. В ней я расскажу о неуспехе, который считаю своим серьёзным достижением.
Ожидание — реальность.
История о метатипах Qt, написании велосипедов, превышении максимального числа записей в объектном файле и, неожиданно, инструменте, который работает так, как и было задумано.
С чего всё началось? Предсказуемо, с лени. Как-то раз появилась задача (де-)сериализации структур в SQL. Большого числа структур, несколько сотен. Естественно, с разными уровнями вложенности, с указателями и контейнерами. Помимо прочего, имелась определяющая особенность: все они уже имели привязку к QJSEngine, то есть имели полноценную метасистему Qt.
С такими вводными не мудрено было придти к написанию своей ORM и поставить весьма амбициозные цели:
1) Минимальная модификация сохраняемых структур. В лучшем случае, без оной вообще, в худшем — Ctrl+Shift+F.
2) Работа с любыми типами, контейнерами и указателями.
3) Не самые страшные таблицы с возможностью их использования вне ORM.
И обозначить предсказуемые ограничения:
1) Таблицы создаются только для классов с метаинформацией (Q_OBJECT\Q_GADGET) для их свойств (Q_PROPERTY). Все зарегистрированные в метасистеме типы, не имеющие метаинформации, будут сохраняться либо в виде строк, либо в виде сырых данных. Если преобразование не существует или тип неизвестен, он пропускается.
Забегая вперёд, получилось следующее:
Спасибо разработчикам Qt, мы это можем делать по щелчку пальцев и id метакласса. Выглядит это примерно так:
Чтение и запись так же не вызывают проблем. Почти. QMetaProperty имеет пару методов чтения-записи для объектов. И ещё одну пару для гаджетов. Поэтому на этапе чтения-записи нам нужно определиться, в кого мы пишем. Делается это так:
Тогда чтение и запись производятся следующим образом:
Казалось бы, readOnGadget, в конце концов, вызывает тот же read, так что зачем городить весь этот код? Совместимость и отсутствие гарантий, что такое поведение не изменится.
И ещё один нюанс. При сохранении Q_ENUM в QVariant его значение кастуется в int. В базу данных тоже поступает int. Но записать int в свойство типа Q_ENUM мы не можем. Поэтому перед записью мы должны проверить, является ли указанное свойство перечислением — и вызвать явное преобразование в таком случае. Звучит страшнее, чем есть на самом деле.
Снова бьём челом разработчикам за класс QVariant и его конструктор QVariant(int id, void* copy). С его помощью можно создать любую структуру с пустым конструктором — и это хорошая новость. Плохая новость: наследники QObject в список не входят. Хорошая новость: их можно делать с помощью QMetaObject::newInstance().
Создание экземпляра произвольного типа будет выглядеть примерно так:
Под тривиальными типами будем понимать числа, строки и бинарные поля. Вроде бы задача простая, снова берём QVariant и в бой. Но есть нюанс. В ряде случаев нам может захотеться сделать «тривиальными» иные типы, например, изображение. С одной стороны, можно было бы просто проверять, есть ли у метатипа нужные конвертеры и использовать их. Но это не самый удачный способ, тем более, что он чреват возникновением конфликтов, так что лучше иметь списки типов и способы их сохранения: в строку, в BLOB или отдать на откуп Qt. На этом же шаге лучше заиметь список тех типов, с которыми вы предпочтёте не связываться. Из стандартных это могут быть JSON-объекты или QModelIndex. Опять же, никакой магии, статические списки.
И опять, разработчики постарались: их QVariant решает эту задачу. Или нет?
Проблема 1: связность указателя и типа, шаблона и типов-параметров.
Для произвольного метакласса нельзя ни получить связные с ним метаклассы указателей (или структуры), ни получить тип, хранимый в шаблоне. Это очень печально, хотя и вполне предсказуемо. Откуда ей взяться?
Неоткуда.
Можно, конечно, поиграться с именем класса, пощекотав параметры шаблонов, но это очень нежное решение, которое ломается о грубую реальность typedef. Что же, иного не остаётся, придётся завести свою функцию для регистрации типов.
А вместе с ней и статический массивчик под это дело. Точнее, QMap, где ключом будет id метакласса, а значением — структура, хранящая все связные типы.
Проблема 2: обработка контейнеров.
С контейнерами не всё так плохо. Для последовательных есть простой способ узнать, является ли переданный нам тип зарегистрированным:
И даже получить ID хранимого метатипа (осторожно — глаза!)
Так что сохранение данных становится делом чисто техническим. Остаётся лишь справиться со всем многообразием контейнеров. Моя реализация затрагивает лишь те, которые можно получить кастами из QList. Во-первых, потому, что результатом QSqlQuery является QVariantList, а, во-вторых, потому, что он может кастоваться во все основные Qt и std контейнеры. (Есть и третья причина, шаблонная магия std плохо впихивается в универсальные короткие решения.)
С ассоциативными контейнерами и парами дела обстоят хуже. Несмотря на то, что для них есть аналогичный по функциональности с QSequentialIterable класс QAssociativeIterable, некоторые сценарии его использования приводят к вылетам программы. Поэтому нас снова ожидают старые друзья: структура и статический массив, которые нужны для выяснения хранившегося в контейнере типа. Кроме того, нам потребуется тип-прокладка, который бы смог сохранить промежуточные результаты select для каждой строки. Можно было бы использовать QPair<QVariant,QVariant>, но я решил создать собственный тип, чтобы избежать конфликтов преобразования.
Проблема 3: использование контейнеров.
У контейнеров есть ещё одна проблема: они не являются структурой. Вот такой вот внезапный удар поддых от Капитана Очевидности! На самом деле, всё просто: у контейнеров нет полей и метаобъекта, а, значит, мы должны их обрабатывать отдельно, пропихивая заглушки. Точнее, не так. Нам нужно обрабатывать отдельно последовательные контейнеры с тривиальными типами и отдельно — ассоциативные контейнеры, так как последовательные контейнеры из структур запросто обрабатываются, как простые структуры. С первыми можно схитрить, преобразовав их в строку или BLOB (нужные методы в QList есть из коробки). Со вторыми же ничего не поделать: придётся дублировать все методы, пропихивая вместо настоящих Q_PROPERTY заглушки key и value.
В итоге мы получили однородный доступ на чтение и запись ко всем используемым типам и структурам с возможностью их рекурсивного обхода.
Для написания SQL запроса нам достаточно иметь метатип класса, имя поля в родительской структуре, имя родительской таблицы, список имён и метатипов полей. Из первых трёх сконструируем имя таблицы, из остального столбцы.
О чём не стоит забывать:
1) Нормализация имён. Дело не только в регистре, типы могут содержать в себе скобки и запятые шаблонов, двоеточия пространств имён. От всего этого многообразия следует избавляться.
2) Приведения типов. Если работа ведётся с SQLite, то всё просто: кто бы ты ни был, ты — строка. Но если используются другие БД, порой, без каста не обойтись. Значит, при вставке или обновлении нормализованное значение (плейсхолдер) нужно дополнительно преобразовать, да и при выборе тоже.
Думаю, многим ответ уже очевиден. Скорость работы. На простых структурах падение скорости составляет 10% на запись и 100% на чтение. На структуре с глубиной вложенности 1 — уже 30% и 700%. На глубине 2 — 50% и 2000%. С повышением вложенности скорость работы падает экспоненциально.
Причина тому ровно одна. Метасистема Qt. Она устроена так, что в ней происходит очень много копирований. Вернее, в ней производится минимально необходимое число копирований для реалтайма, но, тем не менее, весьма большое. Когда производится сериализация данных, нужно один раз скопировать значение в QVariant, и больше никаких копирований не производится. Когда же происходит десериализация — это песня! Копирование структур происходит на каждом вызове write\writeOnGadget — и от них совершенно нельзя избавиться.
Есть ли другой подход, при котором нам не нужно делать копирования? Есть. Объявлять все вложенные структуры указателями.
Такое решение позволяет значительно ускорить ORM. Падение скорости работы всё ещё значительное, в разы, но уже не на порядки. Тем не менее, решение это flawed by design, требующее изменять кучу кода. А если это в любом случае нужно делать, не проще ли сразу написать генератор SQL запросов? Увы, проще, и работает такой код разительно быстрее. Потому моя достаточно большая и интересная работа осталась пылиться в углу.
Жалею ли я, что потратил несколько месяцев на её написание? Чёрт подери, нет! Это было очень интересное погружение внутрь существующей и работающей метасистемы, которое немного изменило мой взгляд на программирование. Я предполагал такой результат, когда приступал к работе. Надеялся на лучшее, но предполагал примерно такой. Я получил его на выходе. И он меня устроил!
Статья, как и сам код, были написаны 4 года назад и отложены для проверки и правки. За эти 4 года вышло 2 стандарта C++ и одна мажорная версия Qt, но никаких существенных правок внесено не было. Я даже не проверил, работает ли ORM в 6-ой версии. (UPD: Работает после небольших правок deprecated методов и типов) Тем не менее, вернувшись назад, я посчитал, что её стоит опубликовать. Хотя бы для того, чтобы воодушевить других на исследование. Ведь если они достигнут большего успеха, чем я, — я тоже останусь в выигрыше. Будет на одну полезную библиотеку больше! А если не достигнут — то, как минимум, они будут знать, что они не одни такие, и что их результат, каким бы разочаровывающим он не был, — это всё равно результат.
Ожидание — реальность.
История о метатипах 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 метакласса, а значением — структура, хранящая все связные типы.
Выглядит это, конечно, пошловато, но работает.
Серьёзно, вы вряд ли тут найдёте что-нибудь принципиально новое.
Как вы могли заметить, тут уже были зарегистрированы конвертеры T>void*, T*>void* и void*>T*. Ничего особенного, они нам потребуются для спокойной работы с QMetaProperty, так как в select, где будут создаваться элементы, мы будем делать простые указатели, а передавать вообще универсальный void*. Нужный тип указателя будет создан самим QVariant в момент записи.
// <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 методов и типов) Тем не менее, вернувшись назад, я посчитал, что её стоит опубликовать. Хотя бы для того, чтобы воодушевить других на исследование. Ведь если они достигнут большего успеха, чем я, — я тоже останусь в выигрыше. Будет на одну полезную библиотеку больше! А если не достигнут — то, как минимум, они будут знать, что они не одни такие, и что их результат, каким бы разочаровывающим он не был, — это всё равно результат.