Обзор 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_defined, 1)
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(0, 100);
// 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());
}