QxOrm C++

Home Download Quick sample Tutorial Faq Link

QxOrm >> Tutoriel Version courante : QxOrm 1.1.3 (LGPL) Version française du site Web site english version
Dans ce chapitre, nous allons traiter plusieurs fonctionnalités de la librarie QxOrm avec la création d'un projet de gestion d'un blog en C++.
Le tutoriel qxBlog est constitué des étapes suivantes :
1- Projet qxBlog - gestion d'un blog en C++ avec les tables suivantes :
  • blog : 1 blog est écrit par 1 author, peut avoir plusieurs comment et peut être associé à plusieurs category
  • author : 1 author peut rédiger plusieurs blog
  • comment : 1 comment est associé à 1 blog
  • category : 1 category référence plusieurs blog

2- Le projet qxBlog du tutoriel a l'arborescence suivante :

./qxBlog/
./qxBlog/qxBlog.pro
./qxBlog/qxBlog.sqlite
./qxBlog/include/precompiled.h
./qxBlog/include/export.h
./qxBlog/include/author.h
./qxBlog/include/blog.h
./qxBlog/include/category.h
./qxBlog/include/comment.h
./qxBlog/src/author.cpp
./qxBlog/src/blog.cpp
./qxBlog/src/category.cpp
./qxBlog/src/comment.cpp
./qxBlog/src/main.cpp

Remarque : le code de ce tutoriel est disponible dans le dossier ./test/qxBlog/ de la distribution de QxOrm.
Le fichier qxBlog.sqlite correspond à la base de données du tutoriel au format sqlite.


3- Contenu du fichier qxBlog.pro :

Ce fichier permet la compilation du projet par l'outil qmake fourni par la librairie Qt.
L'outil qmake est multi-plateforme, ce qui signifie que le projet qxBlog peut être compilé sous Windows, Linux, Mac, etc...
Le fichier qxBlog.pro décrit la liste de tous les fichiers du projet (header + source) ainsi que les dépendances du projet.
Le projet qxBlog est dépendant de la librarie QxOrm qui est elle-même dépendante des excellentes librairies boost et Qt.
Pour simplifier la gestion des dépendances du projet, il est possible d'inclure le fichier QxOrm.pri.
Une information importante à renseigner dans le fichier qxBlog.pro est la définition d'une variable indiquant que l'on est en train de compiler le projet (voir le fichier export.h). En effet, tout comme le mécanisme des dll sous Windows, la librairie QxOrm a besoin de cette information pour exporter ou importer certaines classes. Pour notre projet, cette variable se nomme _BUILDING_QX_BLOG.

qxBlog.pro


4- Contenu du fichier export.h :

L'écriture d'une dll sous Windows impose d'avoir un fichier de ce type pour la gestion des 'export/import' des classes, fonctions, variables, etc...
QxOrm utilise ce même mécanisme pour fournir certaines fonctionnalités : le fichier export.h est donc indispensable pour tous les projets dépendants de QxOrm.

Remarque : pour simplifier, il est possible de schématiser le mécanisme des dll de la façon suivante :

  • lorsque la dll est compilée, l'outil de compilation exporte chaque classe
  • lorsque 1 autre dll dépendante de la précédente est compilée, l'outil de compilation importe les classes nécessaires de la dll précédente

#ifndef _QX_BLOG_EXPORT_H_
#define _QX_BLOG_EXPORT_H_

#ifdef _BUILDING_QX_BLOG
#define QX_BLOG_DLL_EXPORT QX_DLL_EXPORT_HELPER
#else // _BUILDING_QX_BLOG
#define QX_BLOG_DLL_EXPORT QX_DLL_IMPORT_HELPER
#endif // _BUILDING_QX_BLOG

#ifdef _BUILDING_QX_BLOG
#define QX_REGISTER_HPP_QX_BLOG QX_REGISTER_HPP_EXPORT_DLL
#define QX_REGISTER_CPP_QX_BLOG QX_REGISTER_CPP_EXPORT_DLL
#else // _BUILDING_QX_BLOG
#define QX_REGISTER_HPP_QX_BLOG QX_REGISTER_HPP_IMPORT_DLL
#define QX_REGISTER_CPP_QX_BLOG QX_REGISTER_CPP_IMPORT_DLL
#endif // _BUILDING_QX_BLOG

#endif // _QX_BLOG_EXPORT_H_


5- Contenu du fichier precompiled.h :

Il s'agit d'un en-tête précompilé (precompiled header) permettant d'optimiser les temps de compilation du projet.
En effet, QxOrm utilise les techniques de méta-programmation pour fournir l'ensemble de ses fonctionnalités.
La méta-programmation étant couteuse en temps de compilation, votre projet se compilera beaucoup plus vite avec ce fichier precompiled.h.
Un autre avantage (et non des moindres) est que le fichier QxOrm.h regroupe les fonctionnalités de base des librairies boost et Qt.
Il n'est donc plus nécessaire d'écrire #include <QtCore/QString.h> pour utiliser la classe QString de Qt par exemple.
De même, pour utiliser les pointeurs intelligents de boost, il n'est plus nécessaire d'écrire #include <boost/shared_ptr.hpp>.

#ifndef _QX_BLOG_PRECOMPILED_HEADER_H_
#define _QX_BLOG_PRECOMPILED_HEADER_H_

#include <QxOrm.h>

#include "export.h"

#endif // _QX_BLOG_PRECOMPILED_HEADER_H_


6- Contenu des fichiers author.h et author.cpp :

1 author est une personne qui peut rédiger plusieurs blog : nous allons ainsi montrer comment utiliser la relation de type one-to-many.
Au niveau base de données, voici les 2 tables qui correspondent :

qxBlog.table.author

Dans le code C++, les propriétés de la classe author sont le symétrique des champs de la table author dans la base de données.
Donc 1 instance de la classe author dans le code C++ correspond à une ligne de la table author dans la base de données.
Ce principe permet d'écrire du code C++ simple de compréhension et facile à maintenir.

Nous ajoutons 1 méthode à notre classe author : int age() qui calculera l'âge en fonction de la date de naissance envoyée par la base de données.
Nous définissons également 2 typedef pour représenter un pointeur (smart-pointer) vers un objet author ainsi qu'une collection de author.
La classe author a un identifiant de type QString (par défaut, un identifiant dans le contexte QxOrm est de type long) : nous utilisons la macro QX_REGISTER_PRIMARY_KEY(author, QString) pour spécialiser le template.
La classe author s'écrit de la façon suivante :

#ifndef _QX_BLOG_AUTHOR_H_
#define _QX_BLOG_AUTHOR_H_

class blog;

class QX_BLOG_DLL_EXPORT author
{
public:
// -- typedef
   typedef boost::shared_ptr<blog> blog_ptr;
   typedef std::vector<blog_ptr> list_blog;
// -- enum
   enum enum_sex { male, female, unknown };
// -- properties
   QString     m_id;
   QString     m_name;
   QDate       m_birthdate;
   enum_sex    m_sex;
   list_blog   m_blogX;
// -- contructor, virtual destructor
   author() : m_id(0), m_sex(unknown) { ; }
   virtual ~author() { ; }
// -- methods
   int age() const;
};

QX_REGISTER_PRIMARY_KEY(author, QString)
QX_REGISTER_HPP_QX_BLOG(author, qx::trait::no_base_class_defined, 0)

typedef boost::shared_ptr<author> author_ptr;
typedef qx::QxCollection<QString, author_ptr> list_author;

#endif // _QX_BLOG_AUTHOR_H_

#include "../include/precompiled.h"

#include "../include/author.h"
#include "../include/blog.h"

#include <QxMemLeak.h>

QX_REGISTER_CPP_QX_BLOG(author)

namespace qx {
template <> void register_class(QxClass<author> & t)
{
   t.id(& author::m_id, "author_id");

   t.data(& author::m_name, "name");
   t.data(& author::m_birthdate, "birthdate");
   t.data(& author::m_sex, "sex");

   t.relationOneToMany(& author::m_blogX, "list_blog", "author_id");

   t.fct_0<int>(& author::age, "age");
}}

int author::age() const
{
   if (! m_birthdate.isValid()) { return -1; }
   return (QDate::currentDate().year() - m_birthdate.year());
}


7- Contenu des fichiers comment.h et comment.cpp :

1 comment est associé à 1 blog et 1 blog peut contenir plusieurs comment : nous allons ainsi montrer comment utiliser la relation de type many-to-one.
Au niveau base de données, voici les 2 tables qui correspondent (nous reprenons la table blog vue précédemment) :

qxBlog.table.comment

De même que pour la classe author, nous définissons 2 typedef pour représenter un pointeur vers un objet comment ainsi qu'une collection de comment.
La classe comment s'écrit de la façon suivante :

#ifndef _QX_BLOG_COMMENT_H_
#define _QX_BLOG_COMMENT_H_

class blog;

class QX_BLOG_DLL_EXPORT comment
{
public:
// -- typedef
   typedef boost::shared_ptr<blog> blog_ptr;
// -- properties
   long        m_id;
   QString     m_text;
   QDateTime   m_dt_create;
   blog_ptr    m_blog;
// -- contructor, virtual destructor
   comment() : m_id(0) { ; }
   virtual ~comment() { ; }
};

QX_REGISTER_HPP_QX_BLOG(comment, qx::trait::no_base_class_defined, 0)

typedef boost::shared_ptr<comment> comment_ptr;
typedef QList<comment_ptr> list_comment;

#endif // _QX_BLOG_COMMENT_H_

#include "../include/precompiled.h"

#include "../include/comment.h"
#include "../include/blog.h"

#include <QxMemLeak.h>

QX_REGISTER_CPP_QX_BLOG(comment)

namespace qx {
template <> void register_class(QxClass<comment> & t)
{
   t.id(& comment::m_id, "comment_id");

   t.data(& comment::m_text, "comment_text");
   t.data(& comment::m_dt_create, "date_creation");

   t.relationManyToOne(& comment::m_blog, "blog_id");
}}


8- Contenu des fichiers category.h et category.cpp :

1 category référence plusieurs blog et 1 blog peut appartenir à plusieurs category : nous allons ainsi montrer comment utiliser la relation de type many-to-many.
Ce type de relation implique une table supplémentaire dans la base de données pour stocker la liste des id de chaque côté des relations.
Au niveau base de données, voici les 3 tables qui correspondent (nous reprenons la table blog vue précédemment) :

qxBlog.table.category

De même que pour les classes author et comment, nous définissons 2 typedef pour représenter un pointeur vers un objet category ainsi qu'une collection de category.
La classe category s'écrit de la façon suivante :

#ifndef _QX_BLOG_CATEGORY_H_
#define _QX_BLOG_CATEGORY_H_

class blog;

class QX_BLOG_DLL_EXPORT category
{
public:
// -- typedef
   typedef boost::shared_ptr<blog> blog_ptr;
   typedef qx::QxCollection<long, blog_ptr> list_blog;
// -- properties
   long        m_id;
   QString     m_name;
   QString     m_desc;
   list_blog   m_blogX;
// -- contructor, virtual destructor
   category() : m_id(0) { ; }
   virtual ~category() { ; }
};

QX_REGISTER_HPP_QX_BLOG(category, qx::trait::no_base_class_defined, 0)

typedef QSharedPointer<category> category_ptr;
typedef qx::QxCollection<long, category_ptr> list_category;

#endif // _QX_BLOG_CATEGORY_H_

#include "../include/precompiled.h"

#include "../include/category.h"
#include "../include/blog.h"

#include <QxMemLeak.h>

QX_REGISTER_CPP_QX_BLOG(category)

namespace qx {
template <> void register_class(QxClass<category> & t)
{
   t.id(& category::m_id, "category_id");

   t.data(& category::m_name, "name");
   t.data(& category::m_desc, "description");

   t.relationManyToMany(& category::m_blogX, "list_blog", "category_blog", "category_id", "blog_id");
}}


9- Contenu des fichiers blog.h et blog.cpp :

1 blog est écrit par 1 author, peut avoir plusieurs comment et peut être associé à plusieurs category. Cette classe contient donc 3 relations : one-to-many, many-to-one et many-to-many.

Remarque : QxOrm gère également le type de relation one-to-one qui est cependant beaucoup moins utilisée que les autres relations.
Un exemple de type de relation one-to-one est disponible en annexe de ce tutoriel avec la classe/table person : 1 person correspond à 1 author.

De même que pour les autres classes, nous définissons 2 typedef pour représenter un pointeur vers un objet blog ainsi qu'une collection de blog.
La classe blog s'écrit de la façon suivante :

#ifndef _QX_BLOG_BLOG_H_
#define _QX_BLOG_BLOG_H_

#include "author.h"
#include "comment.h"
#include "category.h"

class QX_BLOG_DLL_EXPORT blog
{
public:
// -- properties
   long           m_id;
   QString        m_text;
   QDateTime      m_dt_creation;
   author_ptr     m_author;
   list_comment   m_commentX;
   list_category  m_categoryX;
// -- contructor, virtual destructor
   blog() : m_id(0) { ; }
   virtual ~blog() { ; }
};

QX_REGISTER_HPP_QX_BLOG(blog, qx::trait::no_base_class_defined, 0)

typedef boost::shared_ptr<blog> blog_ptr;
typedef std::vector<blog_ptr> list_blog;

#endif // _QX_BLOG_BLOG_H_

#include "../include/precompiled.h"

#include "../include/blog.h"

#include <QxMemLeak.h>

QX_REGISTER_CPP_QX_BLOG(blog)

namespace qx {
template <> void register_class(QxClass<blog> & t)
{
   t.id(& blog::m_id, "blog_id");

   t.data(& blog::m_text, "blog_text");
   t.data(& blog::m_dt_creation, "date_creation");

   t.relationManyToOne(& blog::m_author, "author_id");
   t.relationOneToMany(& blog::m_commentX, "list_comment", "blog_id");
   t.relationManyToMany(& blog::m_categoryX, "list_category", "category_blog", "blog_id", "category_id");
}}


10- Contenu du fichier main.cpp :

QxOrm permet de communiquer de manière simple et performante avec de nombreuses bases de données (voir la liste des bases de données sur le site de Qt).
Outre la persistance, QxOrm possède également d'autres fonctionnalités intéressantes liées à la gestion des données :
  • la serialization automatique des données ou collections de données au format binaire et xml
  • la reflection permettant d'accéder à la définition des classes paramétrées dans le contexte QxOrm ainsi que l'appel automatique des méthodes de classes

#include "../include/precompiled.h"

#include <QtGui/qapplication.h>

#include "../include/blog.h"
#include "../include/author.h"
#include "../include/comment.h"
#include "../include/category.h"

#include <QxMemLeak.h>

int main(int argc, char * argv[])
{
   // Qt application
   QApplication app(argc, argv);

   // Parameters to connect to database
   qx::QxSqlDatabase::getSingleton()->setDriverName("QSQLITE");
   qx::QxSqlDatabase::getSingleton()->setDatabaseName("./qxBlog.sqlite");
   qx::QxSqlDatabase::getSingleton()->setHostName("localhost");
   qx::QxSqlDatabase::getSingleton()->setUserName("root");
   qx::QxSqlDatabase::getSingleton()->setPassword("");

   // Ensure there is no element in database
   QSqlError daoError = qx::dao::delete_all<author>();
   daoError = qx::dao::delete_all<comment>();
   daoError = qx::dao::delete_all<category>();
   daoError = qx::dao::delete_all<blog>();

   // Create a list of 3 author
   author_ptr author_1; author_1.reset(new author());
   author_ptr author_2; author_2.reset(new author());
   author_ptr author_3; author_3.reset(new author());

   author_1->m_id = "author_id_1"; author_1->m_name = "author_1";
   author_1->m_sex = author::male; author_1->m_birthdate = QDate::currentDate();
   author_2->m_id = "author_id_2"; author_2->m_name = "author_2";
   author_2->m_sex = author::female; author_2->m_birthdate = QDate::currentDate();
   author_3->m_id = "author_id_3"; author_3->m_name = "author_3";
   author_3->m_sex = author::female; author_3->m_birthdate = QDate::currentDate();

   list_author authorX;
   authorX.insert(author_1->m_id, author_1);
   authorX.insert(author_2->m_id, author_2);
   authorX.insert(author_3->m_id, author_3);

   // Insert list of 3 author into database
   daoError = qx::dao::insert(authorX);
   qAssert(qx::dao::count<author>() == 3);

   // Clone author n°2 : 'author_id_2'
   author_ptr author_clone = qx::clone(* author_2);
   qAssert(author_clone->m_id == "author_id_2");
   qAssert(author_clone->m_sex == author::female);

   // Create a query to fetch only female author : 'author_id_2' and 'author_id_3'
   qx::QxSqlQuery query("WHERE author.sex = :sex");
   query.bind(":sex", author::female);

   list_author list_of_female_author;
   daoError = qx::dao::fetch_by_query(query, list_of_female_author);
   qAssert(list_of_female_author.count() == 2);

   // Dump list of female author (xml serialization)
   qx::dump(list_of_female_author);

   // Create 3 categories
   category_ptr category_1 = category_ptr(new category());
   category_ptr category_2 = category_ptr(new category());
   category_ptr category_3 = category_ptr(new category());

   category_1->m_name = "category_1"; category_1->m_desc = "desc_1";
   category_2->m_name = "category_2"; category_2->m_desc = "desc_2";
   category_3->m_name = "category_3"; category_3->m_desc = "desc_3";

   { // Create a scope to destroy temporary connexion to database

   // Open a transaction to database
   QSqlDatabase db = qx::QxSqlDatabase::getDatabase();
   bool bCommit = db.transaction();

   // Insert 3 categories into database, use 'db' parameter for the transaction
   daoError = qx::dao::insert(category_1, (& db));    bCommit = (bCommit && ! daoError.isValid());
   daoError = qx::dao::insert(category_2, (& db));    bCommit = (bCommit && ! daoError.isValid());
   daoError = qx::dao::insert(category_3, (& db));    bCommit = (bCommit && ! daoError.isValid());

   qAssert(bCommit);
   qAssert(category_1->m_id != 0);
   qAssert(category_2->m_id != 0);
   qAssert(category_3->m_id != 0);

   // Terminate transaction => commit or rollback if there is error
   if (bCommit) { db.commit(); }
   else { db.rollback(); }

   } // End of scope : 'db' is destroyed

   // Create a blog with the class name (factory)
   boost::any blog_any = qx::create("blog");
   blog_ptr blog_1 = boost::any_cast<blog_ptr>(blog_any);
   blog_1->m_text = "blog_text_1";
   blog_1->m_dt_creation = QDateTime::currentDateTime();
   blog_1->m_author = author_1;

   // Insert 'blog_1' into database with 'save()' method
   daoError = qx::dao::save(blog_1);

   // Modify 'blog_1' properties and save into database
   blog_1->m_text = "update blog_text_1";
   blog_1->m_author = author_2;
   daoError = qx::dao::save(blog_1);

   // Add 2 comments to 'blog_1'
   comment_ptr comment_1; comment_1.reset(new comment());
   comment_ptr comment_2; comment_2.reset(new comment());

   comment_1->m_text = "comment_1 text";
   comment_1->m_dt_create = QDateTime::currentDateTime();
   comment_1->m_blog = blog_1;
   comment_2->m_text = "comment_2 text";
   comment_2->m_dt_create = QDateTime::currentDateTime();
   comment_2->m_blog = blog_1;

   daoError = qx::dao::insert(comment_1);
   daoError = qx::dao::insert(comment_2);
   qAssert(qx::dao::count<comment>() == 2);

   // Add 2 categories to 'blog_1' => must insert into extra-table 'category_blog'
   blog_1->m_categoryX.insert(category_1->m_id, category_1);
   blog_1->m_categoryX.insert(category_3->m_id, category_3);
   daoError = qx::dao::save_with_relation("list_category", blog_1);

   // Fetch blog into a new variable with all relation : 'author', 'comment' and 'category'
   blog_ptr blog_tmp; blog_tmp.reset(new blog());
   blog_tmp->m_id = blog_1->m_id;
   daoError = qx::dao::fetch_by_id_with_all_relation(blog_tmp);

   qAssert(blog_tmp->m_commentX.count() == 2);
   qAssert(blog_tmp->m_categoryX.count() == 2);
   qAssert(blog_tmp->m_text == "update blog_text_1");
   qAssert(blog_tmp->m_author && blog_tmp->m_author->m_id == "author_id_2");

   // Dump 'blog_tmp' result from database (xml serialization)
   qx::dump(blog_tmp);

   // Call 'age()' method with class name and method name (reflection)
   qx_bool bInvokeOk = qx::QxClassX::invoke("author", "age", author_1);
   qAssert(bInvokeOk);

   return 0;
}

Cliquez ici pour afficher les traces après exécution du programme qxBlog...

Remarque importante : QxOrm n'a pas pour objectif de 'cacher' le code sql (par défaut toutes les requêtes sont tracées). Même si QxOrm simplifie énormément le code C++ et optimise ainsi les temps de développement et de maintenance d'un produit, il est très important d'avoir de bonnes connaissances sql.
QxOrm ne résoudra pas toutes les problématiques liées aux bases de données, il sera alors nécessaire d'utiliser directement le module QtSql de Qt en écrivant soi-même les requêtes sql ou les procédures stockées.
Enfin, il faut être extrêmement vigilant au nombre de requêtes effectuées entre le programme C++ et la base de données : un trop grand nombre d'appels à la base de données peut écrouler les performances d'une application. Il est important de maîtriser la notion de relation entre les classes (voir le syndrôme n+1 que l'on peut rencontrer avec Hibernate ou tout autre ORM).

Autre remarque : Il est important de signaler que les méthodes void register_class(...) liées à vos classes sont appelées automatiquement et lorsque c'est nécessaire par la librairie QxOrm.
Par rapport à d'autres librairies qui nécessitent un appel pour enregistrer les types de chaque classe, QxOrm permet un enregistrement automatique des classes, ce qui simplifie encore un peu plus le développement d'applications.


11- Annexe - exemple de relation one-to-one avec la classe person :

Ajoutons à notre projet la classe person (fichiers person.h et person.cpp) : 1 person correspond à 1 author dans la base de données.
Ils partagent donc le même identifiant, c'est ce qu'on appelle une relation de type one-to-one.
Voici les 2 tables de notre base de données (nous reprenons la table author vue précédemment) :

qxBlog.table.person

Remarque : Nous ajoutons à la table person le champ mother_id. Nous pouvons ainsi connaître la mère (de type person) associée à 1 person, ce qui correspond à une relation many-to-one sur la même table person. De plus, si 1 person est une mère, nous pouvons connaître la liste de ses enfants (de type person), ce qui correspond à une relation one-to-many sur la même table person.



QxOrm