Обзор 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());
    }
    Поддержать автора
    Поделиться публикацией

    Комментарии 20

      +2
      Фундаментально.
      Ещё бы сделали обзор не QT ORM — цены бы не было!
        +1
        поддерживаю, было бы неплохо увидить развернутую статью на эту тему, в принципе наработки в данной области интересуют, хотельсь бы вообще не думать на уровне реляционных таблиц, но пока не получается :)
          0
          Было бы здорово, но здесь нужен кто-то, знающий такие языки как Ruby, Java, PHP, — а я не из их числа. У меня не получится оценить ORM на них, ну, разве что если прокачать программистскую интуицию…
        +3
        туго вобщем с ORM в куте пока :) ну хоть движется процесс и то хорошо
          0
          Да, так и есть. Туго.
          +1
          Тут много чего хорошего написано! Но сил нет… завтра почитаю!
            +1
            Хм… возьму на заметку, тем более, что лицензия LGPL, впрочем мы для себя пока решили, что лучше делать автомат калашникова, чем универсального солдата и тупо сами пишем SQL запросы на старом добром ANSI SQL
              0
              Это тоже вариант, и вполне распространенный.
                +1
                Я не очень то люблю сущности плодить. С ORM сталкивался в zend framework и честно говоря она меня часто в тупик ставила, хотя решение на голом SQLе я прекрасно себе представлял.
                Впрочем если например нужно сделать некий генератор формочек, то тут без ORM уже никак фактически.
                  0
                  Чтобы не плодить сущности, ORM тоже используется. Так или иначе, они придуманы, чтобы программисту было удобнее работать.
              +1
              ORM или неORM — вопрос стоит часто. Все зависит от проекта.
              А за развернутую информацию огромное спасибо.
                0
                Это холиварный вопрос. :) Пожалуйста.
                +1
                QxOrm — вырвиглазный код, извините.
                  0
                  Так это к автору. Он любитель всяких контейнеров, шаблонов и прочего заворота программистской мысли.
                  0
                  как можно писать бизнес-логику на ассемблере 21 века?..
                    0
                    Вы о каком ассемблере говорите? Что-то не понятно… И какие есть альтернативы?
                      0
                      альтернативы: C#, python, java и пр.
                        0
                        Альтернативы Qtу нет, насколько мне известно.
                          0
                          Если говорить об основной направленности Qt — построение GUI, то есть wxWidgets, GTK.
                          Если говорить о других направлениях (сеть, БД и пр.), то есть, к примеру, POCO.
                            0
                            Мягко говоря, GTK и Qt — вещи разных уровней. Про POCO и wxWidgets ничего не знаю толком, так что промолчу.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое