Qt
September 2010 29

Обзор ORM для Qt

Введение



Добрый день, уважаемые коллеги-программисты!

Вот уже год в мире Qt происходят очень интересные события. Здесь вам и недавний выпуск версии 4.7, и концепция QML, и значительная интеграция библиотеки в мобильные платформы. В Qt водятся самые правильные тролли; мне нравится то, что они делают, и куда развивается эта библиотека. Готов даже поспорить, что она – лучшая в своем классе, но те, кто пишут на Qt, и так это знают.

Есть кое-что ещё, изменившееся за годовой период. Для Qt стали появляться ORM-библиотеки, одна за другой, как грибы. Свято место пусто не бывает? Спрос есть, вот вам и предложение. О том, что происходит в мире Qt ORM, читайте в этой статье. Я постараюсь дать максимум информации по использованию и механизмам, применяемым в обозреваемых библиотеках; но ни одна из них не может быть освящена полностью по причине, что любая ORM – весьма сложный комплекс из программистских решений.

(Замечу сразу, что статья в некоторой степени рекламная, поскольку появилась она из-за моей собственной ORM; однако, справедливости ради, я не только пиарю себя, но и даю общую оценку того, что сейчас есть по теме. Прошу отнестись с пониманием: намерения самые благие).


QxOrm, ver. 1.1.1



Автор / владелец: «QxOrm France»
Сайты: официальный, на SourceForge
Лицензия: GPLv3 + коммерческая
Зависимости: Qt (4.5+), boost (1.38+)
Период разработки: начало 2010, последнее изменение – апрель 2010
Документация: неполная, на французском
Примеры: есть, самые базовые

Главная цель разработки – предоставить механизмы persistence (через QtSql), serialization (через boost::serialization) и reflection.

Библиотека выглядит весьма сильно и, кажется, умеет много всего. Построена по принципу DAO (Data Access Object), когда есть класс-отображение строки в таблице, и какими-то методами из БД извлекается список таких строк. Чтобы это было возможно, в QxOrm используются очень хитрые механизмы, в том числе шаблоны (очень много шаблонов), макросы, наследование простое и множественное. Код весьма интересен для ознакомления, если вы – любитель программистских ухищрений или, например, вам нравится Александреску.

Судя по примерам, коду и описаниям, реализованы следующие возможности.
  • Отображение данных таблицы в любой stl / boost контейнер. Предполагаю, что контейнеры Qt тоже подойдут.
  • Некоторой сложности кэширование.
  • Генерация наипростейших запросов: insert, update, delete, create table, и, конечно, select.
  • Обертка над QSqlDatabase, свой собственный QxSqlQuery.
  • Связи «один-к-одному», «один-ко-многим», «многие-ко-многим» на уровне кода.
  • Собственная stl-like коллекция QxCollection типа ключ/значение с хешированием, сортировкой по ключу и значению.
  • Шаблонная реализация цикла foreach (!), о принципах действия которого я могу только догадываться.
  • Реализован шаблонный синглтон.

Плюсы:
  • Общая ориентированность не на базы данных, а на абстрактное хранилище, в том числе XML. Правда, это достигается за счет Boost – «по воробьям из базуки».
  • Хорошие возможности ORM, передача выбранных значений прямо в контейнеры.
  • Куча дополнительных возможностей: сериализация, контейнеры, кеширование и др.
  • Простой синтаксис работы с мапперами. Видно, что автор ориентировался на что-то существующее, когда проектировал интерфейсы. Всё логично, аккуратно, очень похоже на Boost и STL. Стиля Qt – минимум.
  • Технологичность библиотеки; впрочем именно это и минус, потому что изучать ее внутреннюю кухню очень сложно.

Минусы:
  • Общая ориентированность не на базы данных, а на абстрактное хранилище.
  • Слабая поддержка SQL; в частности, нет генерации WHERE, JOIN, GROUP BY, ORDER BY и многого другого. Например, для извлечения конкретных данных по фильтру -необходимо выбрать их все, а затем применять алгоритмы STL / Boost. (Впрочем, я не во всем уверен; возможно, что-то пропустил. Всё же в исходниках нет генерации WHERE – это факт.)
  • Слабая документация. Возможно, автор полагает, что примеров и туториалов достаточно, но я так не думаю. Мы, Qt-программисты, приучены к хорошей и полной документации. Нет описаний классов, методов и констант. Ну, а что есть – на французском.
  • Запутанность библиотеки. Даже не надейтесь, что сможете дописать туда что-нибудь свое.
  • Зависимость от Boost.
  • Лицензия. Нельзя сказать, что она полностью свободна; хотите продавать продукт на основе библиотеки? Платите автору.
  • Главное: библиотека не развивается, документация не переводится. Автор сначала широко распиарил QxOrm в зарубежных Интернетах, а затем исчез. Версия 1.1.1 была первой и единственной.

Как ни парадоксально, при всех минусах QxOrm – чуть ли не единственное полноценное ORM-решение, совместимое с Qt. И это единственное решение, где есть кеширование, что немаловажно для сложных проектов. Вы увидите, что данный крохотный обзор всё же больше прочих, так как другие ORM вряд ли могут сравниться с QxOrm. Однако, используя библиотеку в большом проекте, вы можете захотеть еще какую-нибудь возможность, особенно если вы работаете не с абстрактным хранилищем, а полноценной СУБД, – но её не будет. Вы захотите исправить какой-то баг, – но это не так-то просто. Вам придется изобретать много велосипедов и костылей. Проект в итоге неизбежно превратится в химеру. Напротив, для небольшого проекта, где нужно лишь несколько хороших функций, библиотека может быть полезна, – в той мере, в какой вы не боитесь подтянуть и Boost.

Пример класса-маппера (весь код взят из документации):

class drug
{
public:
   long id;
   QString name;
   QString description;
 
   drug() : id(0) { ; }
   virtual ~drug() { ; }
};
 
QX_REGISTER_HPP_MY_TEST_EXE(drug, qx::trait::no_base_class_defined1)
 
 
QX_REGISTER_CPP_MY_TEST_EXE(drug)   // This macro is necessary to register 'drug' class in QxOrm context
 
namespace qx {
template <> void register_class(QxClass<drug> & t)
{
  t.id(& drug::id"id");               // Register 'drug::id' <=> primary key in your database
  t.data(& drug::name"name"1);      // Register 'drug::name' property with key 'name' and version '1'
  t.data(& drug::description"desc");  // Register 'drug::description' property with key 'desc'
}}
 
 

Пример использования:

  // Create table 'drug' into database to store drugs
   QSqlError daoError = qx::dao::create_table<drug>();
 
   // Insert drugs from container to database
   // 'id' property of 'd1', 'd2' and 'd3' are auto-updated
   daoError = qx::dao::insert(lst_drug);
 
   // Modify and update the second drug into database
   d2->name = "name2 modified";
   d2->description = "desc2 modified";
   daoError = qx::dao::update(d2);
 
   // Delete the first drug from database
   daoError = qx::dao::delete_by_id(d1);
 
   // Count drugs into database
   long lDrugCount = qx::dao::count<drug>();
 
   // Fetch drug with id '3' into a new variable
   drug_ptr d_tmp; d_tmp.reset(new drug());
   d_tmp->id = 3;
   daoError = qx::dao::fetch_by_id(d_tmp);
 
   // Export drugs from container to a file under xml format (serialization)
   qx::serialization::xml::to_file(lst_drug, "./export_drugs.xml");
 
   // Import drugs from xml file into a new container
   type_lst_drug lst_drug_tmp;
   qx::serialization::xml::from_file(lst_drug_tmp, "./export_drugs.xml");
 
   // Clone a drug
   drug_ptr d_clone = qx::clone(* d1);
 
   // Create a new drug by class name (factory)
   boost::any d_any = qx::create("drug");
 
   // Insert drugs container into 'qx::cache'
   qx::cache::set("drugs", lst_drug);
 



QDjango, ver. ???


Автор: Jeremy Lainé, Bolloré telecom
Сайты: официальный, mailing list
Лицензия: GPLv3, LGPLv3
Зависимости: Qt (4.5+)
В разработке с 3 июня 2010
Документация: полная, на английском, doxygen-generated
Примеры: есть, самые базовые.

Главная цель: создать свободную ORM для Qt, максимально похожую на Django.

О достоинствах и недостатках данной разработки говорить пока рано, библиотека еще ничего не умеет. Судя по всему, это будет DAO / Active Record-ORM. Сейчас уже можно генерировать SELECT, извлекать данные в контейнер, создавать таблицы через генерацию CREATE TABLE. Автор сразу же начал прописывать генерацию WHERE; причем поддерживаются операторы AND и OR. То есть, можно создать многоуровневый фильтр, и синтаксис задания фильтров тоже удачный. В разработке активно используются те же методы, что и в QxOrm: шаблоны, наследование… На их основе, надо полагать, автор собирается создать огромные фермы хорошего ООП-кода.
Но это – и всё. Будем верить, что года через полтора из QDjango вырастет мощная ORM-система, а пока о ее применении в проектах говорить не приходится.

Пример использования, взятый у автора.

// all users
QDjangoQuerySet<User> users;
 
// find all users whose password is "foo" and whose username is not "bar"
QDjangoQuerySet<User> someUsers;
someUsers = users.filter(QDjangoWhere("password", QDjangoWhere::Equals"foo") &&
                         QDjangoWhere("username", QDjangoWhere::NotEquals"bar"));
 
// find all users whose username is "foo" or "bar"
someUsers = users.filter(QDjangoWhere("username", QDjangoWhere::Equals"foo") ||
                         QDjangoWhere("username", QDjangoWhere::Equals"bar"));
 
// find all users whose username starts with "f":
someUsers = users.filter(QDjangoWhere("username", QDjangoWhere::StartsWith"f"));
 
// limit number of results
someUsers = users.limit(0100);
 
// get number of matching users
int numberOfUsers = someUsers.size();
 
// retrieve the first matching user
User *firstUser = someUsers.at(0);
 
// free memory
delete firstUser;
 
// retrieve list of usernames and passwords for matching users
QList< QList<QVariant> > propertyLists = someUsers.valuesList(QStringList() << "username" << "password");
 
// delete all the users in the queryset
someUsers.remove();

Класс User:

class User : public QDjangoModel
{
    Q_OBJECT
    Q_PROPERTY(QString username READ username WRITE setUsername)
    Q_PROPERTY(QString password READ password WRITE setPassword)
 
    Q_CLASSINFO("username""max_length=255")
    Q_CLASSINFO("password""max_length=128")
 
public:
    QString username() const;
    void setUsername(const QString &username);
 
    QString password() const;
    void setPassword(const QString &password);
 
private:
    QString m_username;
    QString m_password;
};


QtPersistence, ver. 0.1.1


Автор: Matt Rogers
Сайты: на SourceForge
Лицензия: LGPLv3
Зависимости: Qt (4.5+)
Период разработки: конец 2009 – начало 2010 г.
Документация: нет
Примеры: плохие в unit-тестах

Главная цель: создать ORM для Qt, основанную на подходе Active Record, похожую на некоторые (?) ORM для Ruby.

Еще одна библиотека, которая практически ничего не умеет. Что хуже: и не развивается; похоже, автор забросил этот проект. Собственно, все, что она может – это с помощью класса-маппера записывать данные в базу данных.

Единственные примеры использования найдены в unit-тестах (основанных на самописном модуле тестирвования).

class FakeBook : public QPersistantObject
{
Q_OBJECT
Q_PROPERTY(QString author READ author WRITE setAuthor)
Q_PROPERTY(int yearPublished READ yearPublished WRITE setYearPublished)
public:
    Q_INVOKABLE FakeBook(QObject* parent = 0) : QPersistantObject(parent) {}
    virtual ~FakeBook() {}
 
    void setAuthor(const QString& a) { m_author = a; }
    QString author() const { return m_author; }
 
    void setYearPublished(int year) { m_yearPublished = year; }
    int yearPublished() const { return m_yearPublished; }
 
private:
    QString m_author;
    int m_yearPublished;
};
 
    FakeBook* b = new FakeBook;
    b->setAuthor("Matt Rogers");
    b->setYearPublished(2009);
 
    bool objectSaved = b->save();
    delete b;
    ASSERT_TRUE(objectSaved);
 
    b = new FakeBook;
    b->setAuthor("Not Matt Rogers");
    b->setYearPublished(1999);
 
    objectSaved = b->save();
    delete b;
    ASSERT_TRUE(objectSaved);


QsT SQL Tools (QST), ver. 0.4.2a release


Автор: Александр Гранин (я :) )
Сайты: на SourceForge, форум
Лицензия: GPLv3, LGPLv3
Зависимости: Qt (4.5+)
В разработке с сентября 2009 г.
Документация: полная, doxygen-generated, только на русском
Примеры: есть, в коде, в unit-тестах; так же мною созданы специальные проекты-примеры TradeDB для версий 0.3 и 0.4 – полноценные приложения БД.

Главная цель – облегчить программирование приложений БД под Qt.

Говорить о своей ORM нелегко. Попал на Хабр я именно благодаря ей, написав статью в песочницу. Статья интерес не вызвала… Но то была версия 0.3 – и даже не релизная, а pre-alpha. Сейчас я далеко ушел в разработке QST, и доступна уже 0.5.1 pre-alpha; но всё же впереди есть очень много всего, что нужно сделать.

Прежде всего: это не обычная ORM. Библиотеку я начал писать, еще не зная этого термина; мне был нужен инструмент генерации запросов, чтобы не писать их, и чтобы они были сосредоточены в одном слое: так проще было за ними следить. О таких подходах, как Active Record, я и не знал. В итоге получилось то, что получилось: самобытная ORM, которая не совсем ORM. В ней нельзя настроить поля класса, которые бы отобразились на поля таблицы; в ней нельзя писать (читать) данные прямо в (из) БД, используя лишь присвоение. Зато можно много чего другого.

Возможности, они же плюсы библиотеки.
  • Генерация простых (в смысле – не иерархических, без прибамбасов) SQL-запросов типа SELECT, INSERT, UPDATE, DELETE и EXECUTE (в случае PostgreSQL это SELECT).
  • Концепция DFD ¬– Declarative Field Descriptor. По ней вы описываете, как генерировать запрос; дополнительно, для SELECT, вы описываете, как использовать полученные данные в Qt-представлениях (QTableView, QListView, QComboBox).
  • Интеграция с Qt Interview. Какая-никакая, а есть. Помимо не-Active-Record подхода, это главное отличие QST от всего прочего. Например, вы можете указать, какой ширины должны быть столбцы у QTableView для определенного запроса, как они должны быть озаглавлены. Или можете выбрать значения данных, связанные с текущей строкой в каком-то view.
  • Множественные поименованные запросы.
  • Для каждого запроса – возможность подключить множество разных view.
  • Генерация WHERE-секции. Фильтры можно задавать очень разные; главный минус: условия должны сочетаться через оператор AND.
  • Автоизвлечение имени поля, если оно указано как «max(field_name)», «sum(price*count) as summa». В первом случае к полю можно обращаться как полностью («max(field_name)»), так и сокращенно («field_name»). Во втором случае – только через «summa».
  • Многофункциональный класс подключения – обертка над QSqlDatabase. Может выполнять тестирование подключения, хранить настройки, подключаться с разными именами и удалять подключения.
  • В общем-то, нетрудное использование; главное – понять смысл того, как библиотека работает.
  • Древовидная модель данных. Очень хочу наконец её переписать, поскольку не хочу, чтобы в моей библиотеке присутствовали new и delete в таком виде. Это более чем возможно, и приведет к более безопасной работе с памятью.
  • Косвенно облегчает преобразование данных на пути «Программа – БД».
  • Весьма обширные возможности по настройке генерации SQL; например, если вы описали будущий запрос, включив в него фильтры, то когда фильтр невалиден, он просто не генерируется. Однако же, если валиден – приводится к формату вашего SQL-диалекта и добавляется в секцию WHERE. Так же автоматически расставляются функторы сравнения; для строк это LIKE, для чисел – «=». Впрочем, их легко переопределить.
  • И другие.

Минусы? Конечно, есть, и достаточно много!
  • Непривычная концепция, самобытность. Много всего придется делать руками; точнее – создавать классы-хэндлеры и прописывать DFD для разных типов запросов.
  • Поддержка SQL хоть и больше, чем во всех рассмотренных библиотеках, всё же еще недостаточна. Сейчас бьюсь над несколькими задачами; вероятно, в ближайших версиях движок генерации будет переписан.
  • Нет ни кеширования, ни сериализации, ни рефлексии, – ни реального Object Relational Mapping. Нет и возможности создавать отношения (relations: 1:1, 1:n, n:n). В этом, надо признать, QxOrm впереди планеты всей.
  • Нет кеширования. Хотя я думаю, как его лучше реализовать.
  • Нельзя так же легко извлекать данные в структуры, как это задумывалось во всех других ORM. Однако, сам подход в QST таков, что предлагает не думать об отдельных наборах данных; вместо этого лучше мыслить на уровне моделей и представлений, а так же отдельных значений конкретной записи.
  • Библиотека не столь технологична, как другие. Да, есть внутри и шаблоны, и наследование, – но это ничто в сравнении с той же QxOrm. Программисту, во всяком случае, с этим возиться не надо.
  • Некоторая неочевидность, – во всяком случае, сначала. Много всего делается автоматически (конвертация, например), и сразу этого можно не заметить, даже несмотря на полную документацию.
  • И другие.


В целом, библиотека ещё в развитии, но уже умеет многое всего. Я её применяю в своей работе; и как фрилансер пишу на ней еще один проект. В целом, на программиста ложится гораздо меньше работы, чем с той с QxOrm или с QDjango, – это видно по исходникам примеров. Описал хэндлеры, загрузил в них view – получай возможности, которые почти все расположены в главном классе (QstAbstractModelHandler). Всё, что нужно, я внедряю потихоньку, но ко мне всегда можно обратиться, – обязательно помогу. В отличие от. Поэтому нескромно предлагаю поддержать меня в этом непростом начинании. Хоть даже пожеланием удачи; а лучше – любым отзывом. Буду признателен.

Пример класса-хэндлера и DFD-описателя для запроса SELECT.

Обратите внимание, что в поля QstField передается так же информация для настройки представлений: отображаемость поля, заголовок и ширина колонки.
// personshandler.h
const int PERSONS = 1;
const int PERSONS_FULL_NAME = 2;
 
class PersonsHandler : public QstAbstractModelHandler
{
private:
 
QstBatch _selector(const int &queryNumber) const;
QstBatch _executor(const int &queryNumber) const;
};
 
// personshandler.cpp
QstBatch PersonsHandler::_selector(const int &queryNumber) const
{
QstBatch batch;
 
if (queryNumber == PERSONS)
{
batch.addSource("vPersons");
batch << QstField(RolePrimaryKey, "ID")
<< QstField("Address_ID")
 
<< QstField("LastName", FieldVisible,  "Фамилия"100)
<< QstField("FirstName", FieldVisible,  "Имя"100)
<< QstField("ParentName", FieldVisible,  "Отчество"100)
<< QstField("vcBirthDate", FieldVisible,  "Дата\nрождения"90)
<< QstField("Phone", FieldVisible,  "Контактный\nтелефон"120)
<< QstField("[E-Mail]", FieldVisible,  "e-mail"120)
 
<< QstField("ID", value(ID_VALUE), PurposeWhere)
;
}
else
if (queryNumber == PERSONS_FULL_NAME)
{
batch.addSource("vPersons");
                batch
 
<< QstField("FullName", FieldVisible,  "Полное имя"300)
<< QstField("LastName", FieldVisible,  "Фамилия"100)
<< QstField("FirstName", FieldVisible,  "Имя"100)
<< QstField("ParentName", FieldVisible,  "Отчество"100)
<< QstField("vcBirthDate", FieldVisible,  "Дата\nрождения"90)
<< QstField("Phone", FieldVisible,  "Контактный\nтелефон"120)
<< QstField("[E-Mail]", FieldVisible,  "e-mail"120)
 
<< QstField("ID", value(ID_VALUE), PurposeWhere)
 
 
                                << QstField(RolePrimaryKey, "ID")
                                << QstField("Address_ID")
                                ;
}
else
{
Q_ASSERT(false);
}
 
return batch;
}

Настройка представления:

//  PersonsHandler _personsHandler;
// QstPlainQueryModel _personsModel;  // - описаны в классе PersonsForm.
 
void PersonsForm::loadPersons()
{
_personsHandler.reload(PERSONS, &_personsModel);
_personsHandler.setTableView(ui->tv_PersonsTableView);
}
 
QVariant PersonsForm::personID() const
{
return _personsHandler.keyValueOfView();
}

Использование:

void PersonsForm::loadPersonalDocumentInfo()
{
PersonalDocumentsHandler th;
th.setValue("Person_ID", personID());
QVariantMap valMap = th.SelectToMap(PERSONAL_DOCUMENTS,
QStringList()
<< "DocTypeName"
<< "SerialNumber"
<< "Number"
<< "vcIssueDate"
<< "GivenBy");
ui->le_DocumentTypeLineEdit->setText(valMap["DocTypeName"].toString());
ui->le_SerialNumberLineEdit->setText(valMap["SerialNumber"].toString());
ui->le_NumberLineEdit->setText(valMap["Number"].toString());
ui->le_IssueDateDateEdit->setDate(valMap["vcIssueDate"].toDate());
ui->le_GivenByLineEdit->setText(valMap["GivenBy"].toString());
}
+33
13k 56
Support the author
Comments 20
Top of the day