QxOrm Windows Linux Macintosh C++

Accueil Téléchargement Exemple rapide Tutoriel (4)
Manuel (2)
Forum Nos clients

QxOrm >> Faq
Version courante :  QxOrm 1.4.7 - documentation en ligne de la bibliothèque QxOrm - GitHub
QxEntityEditor 1.2.5
Version française du site Web site english version
qt_ambassador
QxOrm library has been accepted into the Qt Ambassador Program

Qu'est-ce que QxOrm ?

QxOrm est une bibliothèque C++ open source de gestion de données (Object Relational Mapping, ORM).
QxOrm est développé par Lionel Marty, Ingénieur en développement logiciel depuis 2003.

À partir d'une simple fonction de paramétrage (que l'on peut comparer avec un fichier de mapping XML Hibernate), vous aurez accès aux fonctionnalités suivantes :
  • persistance : communication avec de nombreuses bases de données (avec support des relations 1-1, 1-n, n-1 et n-n) ;
  • sérialisation des données (flux binaire, XML et JSON) ;
  • moteur de réflexion (ou introspection) pour accéder aux classes, attributs et invoquer des méthodes.
QxOrm est dépendant des excellentes bibliothèques boost (compatible à partir de la version 1.38) et Qt (compatible à partir de la version 4.5.0).
La bibliothèque QxOrm a été retenue pour faire partie du programme Qt Ambassador.


Qu'est-ce que QxEntityEditor ?

QxEntityEditor est un éditeur graphique pour la bibliothèque QxOrm : QxEntityEditor permet de gérer graphiquement le modèle d'entités.
QxEntityEditor est multi-plateforme (disponible pour Windows, Linux et Mac OS X) et génère du code natif pour tous les environnements : bureau (Windows, Linux, Mac OS X), embarqué et mobile (Android, iOS, Windows Phone, Raspberry Pi, etc.).
Une vidéo de présentation de l'application QxEntityEditor est disponible.

QxEntityEditor est basé sur un système de plugins et propose diverses fonctionnalités pour importer/exporter le modèle de données :
  • génération automatique du code C++ (classes persistantes enregistrées dans le contexte QxOrm) ;
  • génération automatique des scripts SQL DDL (schéma de base de données) pour les bases SQLite, MySQL, PostgreSQL, Oracle et MS SQL Server ;
  • supporte l'évolution du schéma de base de données pour chaque version d'un projet (ALTER TABLE, ADD COLUMN, DROP INDEX, etc.) ;
  • génération automatique des classes C++ de services pour transférer le modèle de données sur le réseau, en utilisant le module QxService, pour créer rapidement des applications client/serveur ;
  • importation automatique des structures de bases de données existantes (par connexion ODBC) pour les bases SQLite, MySQL, PostgreSQL, Oracle et MS SQL Server ;
  • parce que chaque projet est différent, QxEntityEditor propose plusieurs outils pour personnaliser les fichiers générés (notamment un moteur javascript et un débogueur intégré).
QxEntityEditor

QxEntityEditor est développé par Lionel Marty, Ingénieur en développement logiciel depuis 2003.



Comment contacter QxOrm pour indiquer un bug ou poser une question ?

Si vous trouvez un bug ou si vous avez une question concernant le fonctionnement de la bibliothèque QxOrm, vous pouvez envoyer un mail à : support@qxorm.com.
Un forum (en anglais) dédié à QxOrm est disponible en cliquant ici.
Vous pouvez également retrouver la communauté française de QxOrm sur le forum de Developpez.com.


Comment installer et compiler QxOrm ?

Un tutoriel pour installer un environnement de développement avec QxOrm sous Windows est disponible en cliquant ici.

QxOrm utilise le processus qmake de la bibliothèque Qt pour générer les makefile et compiler le projet.
qmake est multiplateforme et fonctionne parfaitement sous Windows, Linux (Unix) et Mac.
Pour compiler QxOrm, il suffit d'exécuter les commandes suivantes :

qmake
make debug
make release

Sous Windows, des fichiers *.vcproj et *.sln sont disponibles pour les éditeurs Visual C++ 2008, Visual C++ 2010 et Visual C++ 2012.
Les fichiers *.pro sont lisibles par l'éditeur Qt Creator, et des plugins existent permettant de s'interfacer avec de nombreux éditeurs C++.
Les fichiers mingw_build_all_debug.bat et mingw_build_all_release.bat présents dans le dossier ./tools/ permettent de compiler rapidement QxOrm ainsi que tous les tests avec le compilateur MinGW sous Windows.
Les fichiers gcc_build_all_debug.sh et gcc_build_all_release.sh présents dans le dossier ./tools/ permettent de compiler rapidement QxOrm ainsi que tous les tests avec GCC sous Linux.
Enfin, les fichiers osx_build_all_debug.sh et osx_build_all_release.sh présents dans le dossier ./tools/ permettent de compiler rapidement QxOrm ainsi que tous les tests sous Mac (merci à Dominique Billet pour l'écriture des scripts).

Remarque : suivant l'environnement de développement, il peut être nécessaire de modifier le fichier QxOrm.pri pour paramétrer la configuration de la bibliothèque boost :

QX_BOOST_INCLUDE_PATH = $$quote(D:/Dvlp/_Libs/Boost/1_42/include)
QX_BOOST_LIB_PATH = $$quote(D:/Dvlp/_Libs/Boost/1_42/lib_shared)
QX_BOOST_LIB_SERIALIZATION_DEBUG = "boost_serialization-vc90-mt-gd-1_42"
QX_BOOST_LIB_SERIALIZATION_RELEASE = "boost_serialization-vc90-mt-1_42"


Quelles sont les bases de données prises en compte par QxOrm ?

QxOrm utilise le moteur QtSql de Qt basé sur un système de plugin.
Une liste détaillée des bases de données supportées est disponible sur le site de Qt en cliquant ici.
Le plugin ODBC (QODBC) assure une compatibilité avec de nombreuses bases de données.
Pour des performances optimales, il est conseillé d'utiliser un plugin spécifique à une base de données :
  • QMYSQL : MySQL ;
  • QPSQL : PostgreSQL (versions 7.3 and above) ;
  • QOCI : Oracle Call Interface Driver ;
  • QSQLITE : SQLite version 3 ;
  • QDB2 : IBM DB2 (version 7.1 and above) ;
  • QIBASE : Borland InterBase ;
  • QTDS : Sybase Adaptive Server.

Pourquoi QxOrm est dépendant de deux bibliothèques : boost et Qt ?

QxOrm utilise de nombreuses fonctionnalités disponibles dans les excellentes bibliothèques boost et Qt.
De plus, ces deux bibliothèques sont utilisées dans de nombreux projets à la fois professionnels et open-source.
Il existe un grand nombre de forums, de tutoriaux, et toute une communauté pour répondre à toutes les problématiques que vous pourriez rencontrer.
L'objectif de QxOrm n'est pas de redévelopper des fonctionnalités qui existent déjà mais de fournir un outil performant d'accès aux bases de données comme il en existe dans d'autres langages (Java avec Hibernate, .Net avec NHibernate, Ruby, Python, etc.).

Qt Qt : bibliothèque complète : IHM (QtGui), réseau (QtNetwork), XML (QtXml), base de données (QtSql), etc.
La documentation est excellente et le code C++ écrit à partir de cette bibliothèque est à la fois performant et simple de compréhension.
Depuis le rachat par Nokia puis Digia et sa nouvelle licence LGPL, Qt est sans contexte la bibliothèque phare du moment.
QxOrm est compatible avec les principaux objets définis par Qt : QObject, QString, QDate, QTime, QDateTime, QList, QHash, QSharedPointer, QScopedPointer, etc.
Il est conseillé d'installer et d'utiliser la dernière version de Qt disponible à l'adresse suivante : http://www.qt.io/

boost boost : de nombreux modules de la bibliothèque boost font partie de la nouvelle norme C++.
C'est une bibliothèque reconnue pour sa qualité, son code 'C++ moderne', sa documentation, sa portabilité, etc.
QxOrm utilise les fonctionnalités suivantes de boost : smart_pointer, serialization, type_traits, multi_index_container, unordered_container, any, tuple, foreach, function.
Il est conseillé d'installer et d'utiliser la dernière version de boost disponible à l'adresse suivante : http://www.boost.org/


Pourquoi QxOrm nécessite un en-tête précompilé (precompiled header) pour pouvoir être utilisé ?

QxOrm utilise les techniques de métaprogrammation C++ pour fournir une grande partie de ses fonctionnalités.
Vous n'avez pas besoin de savoir utiliser la métaprogrammation pour travailler avec la bibliothèque QxOrm.
En effet, QxOrm se veut simple d'utilisation et un code C++ écrit avec Qt et QxOrm est facile à lire, donc facile à développer et à maintenir.

Cependant, la métaprogrammation est couteuse en temps de compilation.
En utilisant un fichier precompiled.h, un projet C++ se compilera beaucoup plus vite.
Un seul fichier d'en-tête est nécessaire pour disposer de l'ensemble des fonctionnalités de QxOrm : le fichier QxOrm.h.


Est-il possible d'accélérer les temps de compilation d'un projet ?

Oui, si la serialization des données au format XML n'est pas utilisée dans le projet, vous pouvez désactiver cette fonctionnalité.
Les temps de compilation seront alors réduits mais vous n'aurez plus accès au namespace qx::serialization:xml.
Pour désactiver la serialization XML, il faut ouvrir le fichier de configuration QxOrm.pri et supprimer (ou mettre en commentaire) l'option de compilation _QX_ENABLE_BOOST_SERIALIZATION_XML.
Une recompilation de la bibliothèque QxOrm est nécessaire pour prendre en compte cette modification.

Une autre possibilité est d'utiliser les classes polymorphiques de la bibliothèque boost::serialization (à la place des classes template).
Cette fonctionnalité réduit les temps de compilation ainsi que la taille de l'éxecutable généré.
En contre-partie, la vitesse d'exécution du programme sera réduite puisqu'une partie du travail effectué lors de la compilation devra être réalisé à l'exécution de l'application.
Pour utiliser cette fonctionnalité dans QxOrm, vous devez activer l'option de compilation _QX_ENABLE_BOOST_SERIALIZATION_POLYMORPHIC dans le fichier de configuration QxOrm.pri.
Attention : les fonctions de serialization seront alors accessibles depuis les namespace suivants : qx::serialization::polymorphic_binary, qx::serialization::polymorphic_text et qx::serialization::polymorphic_xml.
Une recompilation de la bibliothèque QxOrm est nécessaire pour prendre en compte cette modification.

Enfin, il est également possible d'utiliser la macro Q_PROPERTY pour déclarer les propriétés si la classe hérite du type QObject.
Dans ce cas, il existe deux manières différentes pour enregistrer les propriétés dans le contexte QxOrm, dont une qui réduit sensiblement les temps de compilation du projet.
Pour plus d'informations sur cette fonctionnalité, rendez-vous sur cette Question-Réponse de la FAQ.

Remarque : il est également nécessaire de s'assurer que toutes les optimisations proposées par le compilateur sont activées, notamment au niveau de la compilation parallèle sur plusieurs processeurs :
  • MSVC++ : utiliser la variable d'environnement SET CL=/MP
  • GCC et Clang : préciser le nombre de processeurs utilisés en paramètre du process make, par exemple pour 8 coeurs : SET MAKE_COMMAND=make -j8


Quels sont les différents types de serialization disponibles ?

QxOrm utilise le framework de serialization proposé par la bibliothèque boost.
Il existe plusieurs types de serialization disponibles : binaire, XML, JSON, texte, etc.
Le fichier de configuration QxOrm.pri permet d'activer et/ou désactiver les différents types de serialization.

Chaque type de serialization possède ses propres caractéristiques :
  • binary : smallest, fastest, non-portable ;
  • text : larger, slower, portable ;
  • XML : largest, slowest, portable.
Remarque : le type binary n'est pas portable, ce qui signifie que des données ne peuvent pas s'échanger entre un sytème Windows et un système Unix par exemple.
Si vous devez faire transiter des données sur un réseau à travers plusieurs systèmes d'exploitation, il faut utiliser le type de serialization text ou XML.
QxOrm propose également une autre solution : le type de serialization portable_binary.
Le type portable_binary possède les mêmes caractéristiques que le type binary et permet d'échanger des données de manière portable.
Cependant, ce type de serialization n'est pas proposé officiellement par la bibliothèque boost, il est donc nécessaire de tester avant de l'utiliser dans un logiciel en production.


Pourquoi QxOrm fournit un nouveau type de container qx::QxCollection<Key, Value> ?

Il existe de nombreux container dans les bibliothèques stl, boost et Qt.
Il est donc légitime de se poser cette question : à quoi sert qx::QxCollection<Key, Value> ?
qx::QxCollection<Key, Value> est un nouveau container (basé sur l'excellente bibliothèque boost::multi_index_container) qui possède les fonctionnalités suivantes :
  • conserve l'ordre d'insertion des éléments dans la liste ;
  • accès rapide à un élément par son index : équivaut à std::vector<T> ou QList<T> par exemple ;
  • accès rapide à un élément par une clé (hash-map) : équivaut à QHash<Key, Value> ou boost::unordered_map<Key, Value> par exemple ;
  • fonctions de tri sur le type Key et sur le type Value.
Remarque : qx::QxCollection<Key, Value> est compatible avec la macro foreach fournie par la bibliothèque Qt ainsi que par la macro BOOST_FOREACH fournie par la bibliothèque boost.
Cependant, chaque élément renvoyé par ces deux macros correspond à un objet de type std::pair<Key, Value>.
Pour obtenir un résultat 'plus naturel' et plus lisible, il est conseillé d'utiliser la macro _foreach : cette macro utilise BOOST_FOREACH pour tous les container sauf pour qx::QxCollection<Key, Value>.
Dans ce cas, l'élément renvoyé correspond au type Value (voir par la suite l'exemple d'utilisation).
La macro _foreach est donc compatible avec tous les container (stl, Qt, boost, etc.) puisqu'elle utilise la macro BOOST_FOREACH.

Autre Remarque : qx::QxCollection<Key, Value> est particulièrement adapté pour recevoir des données issues d'une base de données.
En effet, ces données peuvent être triées (en utilisant ORDER BY dans une requête SQL par exemple), il est donc important de conserver l'ordre d'insertion des éléments dans la liste.
De plus, chaque donnée issue d'une base de données possède un identifiant unique. Il est donc intéressant de pouvoir accéder à un élément en fonction de cet identifiant unique de manière extrèmement rapide (hash-map).

Exemple d'utilisation de la collection qx::QxCollection<Key, Value> :

/* définition d'une classe drug avec 3 propriétés : 'code', 'name', 'description' */
class drug { public: QString code; QString name; QString desc; };

/* pointeur intelligent associé à la classe drug */
typedef boost::shared_ptr<drug> drug_ptr;

/* collection de drugs (accès rapide à un élément de la collection par la propriété 'code') */
qx::QxCollection<QString, drug_ptr> lstDrugs;

/* création de 3 nouveaux drugs */
drug_ptr d1; d1.reset(new drug()); d1->code = "code1"; d1->name = "name1"; d1->desc = "desc1";
drug_ptr d2; d2.reset(new drug()); d2->code = "code2"; d2->name = "name2"; d2->desc = "desc2";
drug_ptr d3; d3.reset(new drug()); d3->code = "code3"; d3->name = "name3"; d3->desc = "desc3";

/* insertion des 3 drugs dans la collection */
lstDrugs.insert(d1->code, d1);
lstDrugs.insert(d2->code, d2);
lstDrugs.insert(d3->code, d3);

/* parcours la collection en utilisant le mot-clé '_foreach' */
_foreach(drug_ptr p, lstDrugs)
{ qDebug() << qPrintable(p->name) << " " << qPrintable(p->desc); }

/* parcours la collection en utilisant une boucle 'for' */
for (long l = 0; l < lstDrugs.count(); ++l)
{
   drug_ptr p = lstDrugs.getByIndex(l);
   QString code = lstDrugs.getKeyByIndex(l);
   qDebug() << qPrintable(p->name) << " " << qPrintable(p->desc);
}

/* parcours la collection en utilisant le style Java avec 'QxCollectionIterator' */
qx::QxCollectionIterator<QString, drug_ptr> itr(lstDrugs);
while (itr.next())
{
   QString code = itr.key();
   qDebug() << qPrintable(itr.value()->name) << " " << qPrintable(itr.value()->desc);
}

/* effectue un tri croissant par clé (propriété 'code') et décroissant par valeur */
lstDrugs.sortByKey(true);
lstDrugs.sortByValue(false);

/* accès rapide à un drug par son 'code' */
drug_ptr p = lstDrugs.getByKey("code2");

/* accès rapide à un drug par son index (position) dans la collection */
drug_ptr p = lstDrugs.getByIndex(2);

/* teste si un drug existe dans la collection et si la liste est vide */
bool bExist = lstDrugs.exist("code3");
bool bEmpty = lstDrugs.empty();

/* supprime de la collection le 2ème élément */
lstDrugs.removeByIndex(2);

/* supprime de la collection l'élément avec le code 'code3' */
lstDrugs.removeByKey("code3");

/* efface tous les éléments de la collection */
lstDrugs.clear();


Pourquoi QxOrm fournit un nouveau type de pointeur intelligent qx::dao::ptr<T> ?

QxOrm est compatible avec les pointeurs intelligents des bibliothèques boost et Qt.
Le pointeur intelligent développé par QxOrm est basé sur QSharedPointer et apporte de nouvelles fonctionnalités s'il est utilisé avec les fonctions 'qx::dao::...'.
qx::dao::ptr<T> conserve automatiquement les valeurs issues de la base de données.
Il est ainsi possible de vérifier à tout moment si une instance d'objet a subi des modifications grâce à la méthode 'isDirty()' : cette méthode peut renvoyer la liste de toutes les propriétés ayant été modifiées.
qx::dao::ptr<T> peut également être utilisé par la fonction 'qx::dao::update_optimized()' pour mettre à jour en base de données uniquement les champs modifiés.
qx::dao::ptr<T> peut être utilisé avec un objet simple ou bien avec la plupart des containers : stl, boost, Qt et qx::QxCollection<Key, Value>.

Exemple d'utilisation du pointeur intelligent qx::dao::ptr<T> :

// exemple d'utilisation de la méthode 'isDirty()'
qx::dao::ptr<blog> blog_isdirty = qx::dao::ptr<blog>(new blog());
blog_isdirty->m_id = blog_1->m_id;
daoError = qx::dao::fetch_by_id(blog_isdirty);
qAssert(! daoError.isValid() && ! blog_isdirty.isDirty());

blog_isdirty->m_text = "blog property 'text' modified => blog is dirty !!!";
QStringList lstDiff; bool bDirty = blog_isdirty.isDirty(lstDiff);
qAssert(bDirty && (lstDiff.count() == 1) && (lstDiff.at(0) == "blog_text"));
if (bDirty) { qDebug("[QxOrm] test dirty 1 : blog is dirty => '%s'", qPrintable(lstDiff.join("|"))); }

// met à jour uniquement la propriété 'm_text' de l'instance 'blog_isdirty'
daoError = qx::dao::update_optimized(blog_isdirty);
qAssert(! daoError.isValid() && ! blog_isdirty.isDirty());
qx::dump(blog_isdirty);

// exemple d'utilisation de la méthode 'isDirty()' avec une liste d'objets
typedef qx::dao::ptr< QList<author_ptr> > type_lst_author_test_is_dirty;

type_lst_author_test_is_dirty container_isdirty = type_lst_author_test_is_dirty(new QList<author_ptr>());
daoError = qx::dao::fetch_all(container_isdirty);
qAssert(! daoError.isValid() && ! container_isdirty.isDirty() && (container_isdirty->count() == 3));

author_ptr author_ptr_dirty = container_isdirty->at(1);
author_ptr_dirty->m_name = "author name modified at index 1 => container is dirty !!!";
bDirty = container_isdirty.isDirty(lstDiff);
qAssert(bDirty && (lstDiff.count() == 1));
if (bDirty) { qDebug("[QxOrm] test dirty 2 : container is dirty => '%s'", qPrintable(lstDiff.join("|"))); }

author_ptr_dirty = container_isdirty->at(2);
author_ptr_dirty->m_birthdate = QDate(1998, 03, 06);
bDirty = container_isdirty.isDirty(lstDiff);
qAssert(bDirty && (lstDiff.count() == 2));
if (bDirty) { qDebug("[QxOrm] test dirty 3 : container is dirty => '%s'", qPrintable(lstDiff.join("|"))); }

// met à jour la propriété 'm_name' en position 1, la propriété 'm_birthdate' en position 2 et ne change rien en position 0
daoError = qx::dao::update_optimized(container_isdirty);
qAssert(! daoError.isValid() && ! container_isdirty.isDirty());
qx::dump(container_isdirty);

// récupère uniquement la propriété 'm_dt_creation' du blog
QStringList lstColumns = QStringList() << "date_creation";
list_blog lst_blog_with_only_date_creation;
daoError = qx::dao::fetch_all(lst_blog_with_only_date_creation, NULL, lstColumns);
qAssert(! daoError.isValid() && (lst_blog_with_only_date_creation.size() > 0));

if ((lst_blog_with_only_date_creation.size() > 0) && (lst_blog_with_only_date_creation[0] != NULL))
{ qAssert(lst_blog_with_only_date_creation[0]->m_text.isEmpty()); }

qx::dump(lst_blog_with_only_date_creation);


Faut-il utiliser QString ou std::string ?

QxOrm conseille d'utiliser la classe QString pour la gestion des chaînes de caractères.
Même si boost fournit de nombreuses fonctionnalités avec son module boost::string_algo, la classe QString est plus simple à utiliser et surtout prend en charge automatiquement les différents formats : Ascii, Utf8, Utf16, etc.
Cependant, QxOrm est compatible avec std::string et std::wstring si vous préférez utiliser ce type de chaîne de caractères.


Faut-il utiliser les pointeurs intelligents smart-pointer ?

QxOrm conseille fortement d'utiliser les pointeurs intelligents de boost ou Qt.
Le langage C++ ne possède pas de Garbage Collector comme Java ou C# par exemple.
L'utilisation des smart-pointer simplifie énormément la gestion de la mémoire en C++.
L'idéal dans un programme C++ est de n'avoir aucun appel à delete ou delete[].
De plus, les smart-pointer font partie de la nouvelle norme C++ : C++1x.
Il est donc essentiel aujourd'hui de connaître les classes suivantes :


La clé primaire est de type long par défaut. Est-il possible d'utiliser une clé de type QString ou autre ?

Par défaut, lorsqu'un mapping d'une classe C++ est écrit avec la méthode void qx::register_class<T>, l'identifiant associé à la classe est de type long (clé primaire avec auto-incrémentation dans la base de données).

Il est possible de définir un identifiant d'un autre type en utilisant la macro QX_REGISTER_PRIMARY_KEY.
Cette macro spécialise le template qx::trait::get_primary_key pour associer un type d'identifiant à une classe C++.

Par exemple, pour définir un identifiant unique de type QString pour la classe C++ myClass (mappée vers une table de la BDD avec une colonne de type VARCHAR pour clé primaire), il suffit d'écrire :
QX_REGISTER_PRIMARY_KEY(myClass, QString)

Voici un exemple d'utilisation de la macro QX_REGISTER_PRIMARY_KEY avec une classe author possédant un identifiant de type QString :

#ifndef _QX_BLOG_AUTHOR_H_
#define _QX_BLOG_AUTHOR_H_
 
class author
{
public:
// -- propriétés
   QString  m_id;
   QString  m_name;
// -- constructeur, destructeur virtuel
   author() { ; }
   virtual ~author() { ; }
};

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

#endif // _QX_BLOG_AUTHOR_H_


Comment définir une clé primaire sur plusieurs colonnes (composite key) ?

QxOrm supporte la notion de 'multi-columns primary key'.
L'identifiant de la classe doit être du type suivant :
  • QPair ou std::pair pour définir deux colonnes ;
  • boost::tuple pour définir de deux à neuf colonnes.
Il est nécessaire d'utiliser la macro QX_REGISTER_PRIMARY_KEY() pour spécialiser le template et ainsi définir le type d'identifiant sur plusieurs colonnes.
La liste des noms des colonnes doit être de la forme suivante : 'column1|column2|column3|etc.'.

Exemple d'utilisation avec la classe 'author' du projet 'qxBlogCompositeKey', cette classe possède un identifiant sur trois colonnes :

#ifndef _QX_BLOG_AUTHOR_H_
#define _QX_BLOG_AUTHOR_H_

class blog;

class QX_BLOG_DLL_EXPORT author
{

   QX_REGISTER_FRIEND_CLASS(author)

public:

// -- clé composée (clé primaire définie sur plusieurs colonnes dans la base de données)
   typedef boost::tuple<QString, long, QString> type_composite_key;
   static QString str_composite_key() { return "author_id_0|author_id_1|author_id_2"; }

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

// -- enum
   enum enum_sex { male, female, unknown };

// -- propriétés
   type_composite_key   m_id;
   QString              m_name;
   QDate                m_birthdate;
   enum_sex             m_sex;
   list_blog            m_blogX;

// -- constructeur, destructeur virtuel
   author() : m_id("", 0, ""), m_sex(unknown) { ; }
   virtual ~author() { ; }

// -- méthodes
   int age() const;

// -- méthodes d'accès à la clé composée
   type_composite_key getId() const    { return m_id; }
   QString getId_0() const             { return boost::tuples::get<0>(m_id); }
   long getId_1() const                { return boost::tuples::get<1>(m_id); }
   QString getId_2() const             { return boost::tuples::get<2>(m_id); }

// -- méthodes de modification de la clé composée
   void setId_0(const QString & s)     { boost::tuples::get<0>(m_id) = s; }
   void setId_1(long l)                { boost::tuples::get<1>(m_id) = l; }
   void setId_2(const QString & s)     { boost::tuples::get<2>(m_id) = s; }

};

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

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

#endif // _QX_BLOG_AUTHOR_H_

#include "../include/precompiled.h"
#include "../include/author.h"
#include "../include/blog.h"
#include <QxOrm_Impl.h>

QX_REGISTER_CPP_QX_BLOG(author)

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

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

   t.relationOneToMany(& author::m_blogX, blog::str_composite_key(), author::str_composite_key());

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

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


Comment enregistrer des membres private ou protected dans le contexte QxOrm ?

Pour enregistrer des membres private ou protected dans le contexte QxOrm (fonction qx::register_class<T>), il faut déclarer les friend class nécessaires.
Pour simplifier l'écriture avec les template C++, la bibliothèque QxOrm fournit la macro suivante : QX_REGISTER_FRIEND_CLASS(myClass).
Un exemple d'utilisation se trouve dans le dossier ./test/qxDllSample/dll1/ du package QxOrm avec la classe CPerson :

namespace qx {
namespace test {

class QX_DLL1_EXPORT CPerson : public QObject
{

   Q_OBJECT
   QX_REGISTER_FRIEND_CLASS(qx::test::CPerson)

   // etc...

};

} // namespace test
} // namespace qx



Comment activer/désactiver le module QxMemLeak pour la détection automatique des fuites mémoires ?

Le module QxMemLeak permet une détection rapide des fuites mémoire en mode Debug une fois l'exécution du programme terminée (avec indication du fichier et de la ligne => style MFC de Microsoft).
Ce module a été développé par Wu Yongwei et a subi quelques modifications pour être intégré dans QxOrm.
Si un autre outil est déjà utilisé (Valgrind par exemple), cette fonctionnalité ne doit pas être activée.
Pour activer/désactiver le module QxMemLeak, il suffit de modifier la constante _QX_USE_MEM_LEAK_DETECTION définie dans le fichier QxConfig.h.
Une recompilation de la bibliothèque QxOrm est nécessaire pour prendre en compte cette modification.


Comment gérer la notion d'héritage avec la base de données ?

On retrouve généralement dans les différents outils de type ORM trois différentes stratégies pour gérer la notion d'héritage avec la base de données : QxOrm utilise par défaut la stratégie Concrete Table Inheritance (les autres stratégies ne sont pas fonctionnelles à l'heure actuelle).
De nombreux tutoriaux et forums sont disponibles sur internet pour plus de détails sur cette notion d'héritage.
Un exemple d'utilisation avec une classe de base se trouve dans le dossier ./test/qxDllSample/dll2/ avec la classe BaseClassTrigger.


Comment utiliser les Trigger ?

Les Trigger de QxOrm permettent d'effectuer divers traitements avant et/ou après une insertion, une mise à jour ou bien une suppression dans la base de données.
Un exemple d'utilisation se trouve dans le dossier ./test/qxDllSample/dll2/ avec la classe BaseClassTrigger.
Cette classe contient cinq propriétés : m_id, m_dateCreation, m_dateModification, m_userCreation et m_userModification.
Ces propriétés se mettront à jour automatiquement pour chaque classe héritant de BaseClassTrigger (cf. les classes Foo et Bar du même projet).
Il est nécessaire de spécialiser le template 'QxDao_Trigger' pour profiter de cette fonctionnalité.

#ifndef _QX_BASE_CLASS_TRIGGER_H_
#define _QX_BASE_CLASS_TRIGGER_H_

class QX_DLL2_EXPORT BaseClassTrigger
{

   QX_REGISTER_FRIEND_CLASS(BaseClassTrigger)

protected:

   long        m_id;
   QDateTime   m_dateCreation;
   QDateTime   m_dateModification;
   QString     m_userCreation;
   QString     m_userModification;

public:

   BaseClassTrigger() : m_id(0)  { ; }
   virtual ~BaseClassTrigger()   { ; }

   long getId() const                     { return m_id; }
   QDateTime getDateCreation() const      { return m_dateCreation; }
   QDateTime getDateModification() const  { return m_dateModification; }
   QString getUserCreation() const        { return m_userCreation; }
   QString getUserModification() const    { return m_userModification; }

   void setId(long l)                              { m_id = l; }
   void setDateCreation(const QDateTime & dt)      { m_dateCreation = dt; }
   void setDateModification(const QDateTime & dt)  { m_dateModification = dt; }
   void setUserCreation(const QString & s)         { m_userCreation = s; }
   void setUserModification(const QString & s)     { m_userModification = s; }

   void onBeforeInsert(qx::dao::detail::IxDao_Helper * dao);
   void onBeforeUpdate(qx::dao::detail::IxDao_Helper * dao);

};

QX_REGISTER_HPP_QX_DLL2(BaseClassTrigger, qx::trait::no_base_class_defined, 0)

namespace qx {
namespace dao {
namespace detail {

template <>
struct QxDao_Trigger<BaseClassTrigger>
{

   static inline void onBeforeInsert(BaseClassTrigger * t, qx::dao::detail::IxDao_Helper * dao)
   { if (t) { t->onBeforeInsert(dao); } }
   static inline void onBeforeUpdate(BaseClassTrigger * t, qx::dao::detail::IxDao_Helper * dao)
   { if (t) { t->onBeforeUpdate(dao); } }
   static inline void onBeforeDelete(BaseClassTrigger * t, qx::dao::detail::IxDao_Helper * dao)
   { Q_UNUSED(t); Q_UNUSED(dao); }
   static inline void onBeforeFetch(BaseClassTrigger * t, qx::dao::detail::IxDao_Helper * dao)
   { Q_UNUSED(t); Q_UNUSED(dao); }
   static inline void onAfterInsert(BaseClassTrigger * t, qx::dao::detail::IxDao_Helper * dao)
   { Q_UNUSED(t); Q_UNUSED(dao); }
   static inline void onAfterUpdate(BaseClassTrigger * t, qx::dao::detail::IxDao_Helper * dao)
   { Q_UNUSED(t); Q_UNUSED(dao); }
   static inline void onAfterDelete(BaseClassTrigger * t, qx::dao::detail::IxDao_Helper * dao)
   { Q_UNUSED(t); Q_UNUSED(dao); }
   static inline void onAfterFetch(BaseClassTrigger * t, qx::dao::detail::IxDao_Helper * dao)
   { Q_UNUSED(t); Q_UNUSED(dao); }

};

} // namespace detail
} // namespace dao
} // namespace qx

#endif // _QX_BASE_CLASS_TRIGGER_H_

#include "../include/precompiled.h"
#include "../include/BaseClassTrigger.h"
#include <QxOrm_Impl.h>

QX_REGISTER_CPP_QX_DLL2(BaseClassTrigger)

namespace qx {
template <> void register_class(QxClass<BaseClassTrigger> & t)
{
   IxDataMember * pData = NULL;

   pData = t.id(& BaseClassTrigger::m_id, "id");

   pData = t.data(& BaseClassTrigger::m_dateCreation, "date_creation");
   pData = t.data(& BaseClassTrigger::m_dateModification, "date_modification");
   pData = t.data(& BaseClassTrigger::m_userCreation, "user_creation");
   pData = t.data(& BaseClassTrigger::m_userModification, "user_modification");
}}

void BaseClassTrigger::onBeforeInsert(qx::dao::detail::IxDao_Helper * dao)
{
   Q_UNUSED(dao);
   m_dateCreation = QDateTime::currentDateTime();
   m_dateModification = QDateTime::currentDateTime();
   m_userCreation = "current_user_1";
   m_userModification = "current_user_1";
}

void BaseClassTrigger::onBeforeUpdate(qx::dao::detail::IxDao_Helper * dao)
{
   Q_UNUSED(dao);
   m_dateModification = QDateTime::currentDateTime();
   m_userModification = "current_user_2";
}


Comment déclarer une classe abstraite dans le contexte QxOrm ?

Une classe abstraite C++ (contenant au moins une méthode virtuelle pure) ne peut pas être mappée avec une table d'une base de données (puisqu'elle ne peut pas être instanciée).
Cependant, il peut être intéressant de définir une classe abstraite contenant une liste de propriétés utilisées par plusieurs objets persistants.
Un exemple de classe abstraite se trouve dans le dossier ./test/qxDllSample/dll2/ de la distribution de QxOrm avec la classe BaseClassTrigger.
QxOrm propose le mécanisme suivant pour définir une classe abstraite dans le contexte QxOrm :
  • déclarer la classe avec la méthode 'void register_class' comme n'importe qu'elle autre classe ;
  • utiliser la macro QX_REGISTER_ABSTRACT_CLASS(className) juste après la définition de la classe.


Comment déclarer une classe définie dans un espace de nom (namespace) dans le contexte QxOrm ?

Si une classe est définie dans un espace de nom (namespace), alors une erreur de compilation se produit avec l'utilisation des macros : QX_REGISTER_HPP et QX_REGISTER_CPP.
Pour éviter ces erreurs de compilation, il est nécessaire d'utiliser les macros suivantes : QX_REGISTER_COMPLEX_CLASS_NAME_HPP et QX_REGISTER_COMPLEX_CLASS_NAME_CPP.

Vous trouverez un exemple d'utilisation dans le dossier ./test/qxDllSample/dll1/ de la distribution de QxOrm avec la classe CPerson définie dans l'espace de nom qx::test :
* QX_REGISTER_COMPLEX_CLASS_NAME_HPP_QX_DLL1(qx::test::CPerson, QObject, 0, qx_test_CPerson)

Les macros QX_REGISTER_COMPLEX_CLASS_NAME... nécessitent un paramètre supplémentaire (dans l'exemple ci-dessus il s'agit du paramètre qx_test_CPerson) afin de créer une variable globale.
Celle-ci est appelée dès le lancement de l'application.
La construction de cette instance globale déclare la classe dans le module QxFactory (modèle de conception fabrique ou design pattern factory).
Un objet C++ ne pouvant pas se nommer avec des caractères "::", le paramètre supplémentaire de la macro permet de remplacer tous les "::" par des "_".


Comment utiliser le mécanisme de suppression logique (soft delete) ?

Une suppression logique permet de ne pas effacer de ligne dans une table d'une base de données (contrairement à une suppression physique) : une colonne supplémentaire est ajoutée à la définition de la table pour indiquer que la ligne est supprimée ou non.
Cette colonne peut contenir soit un booléen (1 signifie ligne supprimée, 0 ou vide signifie ligne non supprimée), soit la date-heure de suppression de la ligne (si vide, la ligne est considérée comme non supprimée).
Il est donc à tout moment possible de réactiver une ligne supprimée en réinitialisant la valeur à vide dans la table de la base de données.

Pour activer le mécanisme de suppression logique avec la bibliothèque QxOrm, il faut utiliser la classe qx::QxSoftDelete dans la fonction de mapping qx::register_class<T>.
Voici un exemple d'utilisation avec une classe Bar contenant deux propriétés m_id et m_desc :

namespace qx {
template <> void register_class(QxClass<Bar> & t)
{
   t.setSoftDelete(qx::QxSoftDelete("deleted_at"));

   t.id(& Bar::m_id, "id");
   t.data(& Bar::m_desc, "desc");
}}

Les requêtes SQL générées automatiquement par la bibliothèque QxOrm vont prendre en compte ce paramètre de suppression logique pour ajouter les conditions nécessaires (ne pas récupérer les éléments supprimés, ne pas supprimer physiquement une ligne, etc.).
Par exemple, si vous exécutez les lignes suivantes avec la classe Bar :

Bar_ptr pBar; pBar.reset(new Bar());
pBar->setId(5);
QSqlError daoError = qx::dao::delete_by_id(pBar);     qAssert(! daoError.isValid());
qx_bool bDaoExist = qx::dao::exist(pBar);             qAssert(! bDaoExist);
daoError = qx::dao::delete_all<Bar>();                qAssert(! daoError.isValid());
long lBarCount = qx::dao::count<Bar>();               qAssert(lBarCount == 0);
daoError = qx::dao::destroy_all<Bar>();               qAssert(! daoError.isValid());

Vous obtiendrez les traces suivantes :

[QxOrm] sql query (93 ms) : UPDATE Bar SET deleted_at = '20110617115148615' WHERE id = :id
[QxOrm] sql query (0 ms) : SELECT Bar.id AS Bar_id_0, Bar.deleted_at FROM Bar WHERE Bar.id = :id AND (Bar.deleted_at IS NULL OR Bar.deleted_at = '')
[QxOrm] sql query (78 ms) : UPDATE Bar SET deleted_at = '20110617115148724'
[QxOrm] sql query (0 ms) : SELECT COUNT(*) FROM Bar WHERE (Bar.deleted_at IS NULL OR Bar.deleted_at = '')
[QxOrm] sql query (110 ms) : DELETE FROM Bar

Remarque : pour supprimer physiquement une ligne de la base de données, il faut utiliser les fonctions : qx::dao::destroy_by_id() et qx::dao::destroy_all().

Autre remarque : il peut être intéressant de définir au niveau du SGBD un index sur la colonne deleted_at (ou peu importe le nom que vous donnez) afin d'accélérer l'exécution des requêtes SQL.


Comment utiliser les sessions (classe qx::QxSession) pour simplifier la gestion des transactions des bases de données (C++ RAII) ?

Une transaction est une suite d'opérations effectuées comme une seule unité logique de travail.
Une fois terminée, la transaction est :
* soit validée (commit), alors toutes les modifications sont faites dans la base de données ;
* soit annulée (rollback), alors toutes les modifications ne sont pas enregistrée.

La classe qx::QxSession de la bibliothèque QxOrm permet de gérer automatiquement les transactions (validation, annulation) en utilisant le mécanisme C++ RAII :

{ // Ouverture d'un scope où une session sera instanciée

  // Création d'une session : une connection valide à la BDD est assignée à la session et une transaction est démarrée
  qx::QxSession session;

  // Exécution d'une série d'opérations avec la BDD (en utilisant l'opérateur += de la classe qx::QxSession et la connection de la session)
  session += qx::dao::insert(my_object, session.database());
  session += qx::dao::update(my_object, session.database());
  session += qx::dao::fetch_by_id(my_object, session.database());
  session += qx::dao::delete_by_id(my_object, session.database());

  // Si la session n'est pas valide (donc une erreur s'est produite) => affichage de la 1ère erreur de la session
  if (! session.isValid()) { qDebug("[QxOrm] session error : '%s'", qPrintable(session.firstError().text())); }

} // Fermeture du scope : la session est détruite (transaction => commit ou rollback automatique)

Remarque : une session peut déclencher une exception de type qx::dao::sql_error lorsqu'une erreur se produit (par défaut, aucune exception n'est déclenchée). Il est possible de paramétrer ce comportement en utilisant :
* soit le constructeur de la classe qx::QxSession (pour une session en particulier) ;
* soit le paramètre du singleton qx::QxSqlDatabase::getSingleton()->setSessionThrowable(bool b) (pour toutes les sessions).

Autre remarque : il est important de ne pas oublier de passer la connection à la base de données de la session à chaque fonction qx::dao::xxx (en utilisant la méthode session.database()).
De plus, il est possible d'initialiser une session avec sa propre connection (provenant d'un pool de connections par exemple) en utilisant le constructeur de la classe qx::QxSession.

La classe qx::QxSession propose également des méthodes de persistance (CRUD), ce qui peut simplifier l'écriture du code C++ suivant les habitudes de programmation.
Voici le même exemple en utilisant les méthodes de la classe qx::QxSession à la place des fonctions du namespace qx::dao :

{ // Ouverture d'un scope où une session sera instanciée

  // Création d'une session : une connection valide à la BDD est assignée à la session et une transaction est démarrée
  qx::QxSession session;

  // Exécution d'une série d'opérations avec la BDD
  session.insert(my_object);
  session.update(my_object);
  session.fetchById(my_object);
  session.deleteById(my_object);

  // Si la session n'est pas valide (donc une erreur s'est produite) => affichage de la 1ère erreur de la session
  if (! session.isValid()) { qDebug("[QxOrm] session error : '%s'", qPrintable(session.firstError().text())); }

} // Fermeture du scope : la session est détruite (transaction => commit ou rollback automatique)



Comment persister un type dont on ne possède pas le code source (classe provenant d'une bibliothèque tierce par exemple) ?

La bibliothèque QxOrm permet de persister n'importe quel type, même si ce dernier n'est pas enregistré dans le contexte QxOrm par la méthode qx::register_class<T>().

Il est nécessaire d'écrire les fonctions de sérialisation de la bibliothèque boost, en utilisant la méthode non intrusive (puisque le code source n'est pas disponible ou ne peut pas être modifié). Pour plus d'informations sur la sérialisation des données avec la bibliothèque boost, rendez-vous sur le tutoriel de developpez.com.

Par exemple, imaginons une classe 'ExtObject3D' provenant d'une bibliothèque tierce et dont le code source n'est pas disponible ou ne peut pas être modifié. Voici le code nécessaire pour pouvoir persister une instance de type 'ExtObject3D' en base de données :

#ifndef _PERSIST_EXTOBJECT3D_H_
#define _PERSIST_EXTOBJECT3D_H_

#include "ExtObject3D.h"

#include <boost/serialization/serialization.hpp>
#include <boost/serialization/split_free.hpp>
#include <boost/serialization/nvp.hpp>
 
namespace boost {
namespace serialization {

template <class Archive>
void save(Archive & ar, const ExtObject3D & t, unsigned int version)
{
   Q_UNUSED(version);
   double x(t.getX()), y(t.getY()), z(t.getZ()), angle(t.getAngle());

   ar << boost::serialization::make_nvp("x", x);
   ar << boost::serialization::make_nvp("y", y);
   ar << boost::serialization::make_nvp("z", z);
   ar << boost::serialization::make_nvp("angle", angle);
}

template <class Archive>
void load(Archive & ar, ExtObject3D & t, unsigned int version)
{
   Q_UNUSED(version);
   double x(0.0), y(0.0), z(0.0), angle(0.0);

   ar >> boost::serialization::make_nvp("x", x);
   ar >> boost::serialization::make_nvp("y", y);
   ar >> boost::serialization::make_nvp("z", z);
   ar >> boost::serialization::make_nvp("angle", angle);

   t.setX(x);
   t.setY(y);
   t.setZ(z);
   t.setAngle(angle);
}

} // namespace serialization
} // namespace boost
 
BOOST_SERIALIZATION_SPLIT_FREE(ExtObject3D)

#endif // _PERSIST_EXTOBJECT3D_H_

Le code ci-dessus est suffisant pour persister une instance de type 'ExtObject3D' en base de données : il est ainsi possible d'utiliser une propriété de type 'ExtObject3D' dans une classe persistante enregistrée dans le contexte QxOrm. Cette propriété peut être mappée sur une colonne de type TEXT ou VARCHAR en base de données.

Le comportement par défaut de la bibliothèque QxOrm est le suivant : l'instance est sérialisée au format XML avant d'être insérée ou mise à jour en base de données. Ce comportement par défaut peut être utile par exemple si l'on souhaite enregistrer une collection d'objets sans vouloir faire de relation (et donc gérer une autre table dans la base de données). Par exemple, si l'on utilise une propriété de type std::vector<mon_objet> dans une classe persistante sans relation associée, la liste d'éléments sera automatiquement enregistrée au format XML en base de données.

Remarque : ce comportement par défaut peut être facilement modifié pour un type donné. Le moteur QtSql utilise le type QVariant pour faire le lien entre le code C++ et la base de données. Le type QVariant peut contenir du texte, des valeurs numériques, du binaire, etc. Il peut donc être intéressant de spécialiser le comportement par défaut (sérialisation XML) si l'on souhaite stocker des données au format binaire ou bien optimiser les performances (la sérialisation XML peut être couteuse en temps d'exécution). Il suffit de proposer (en plus des fonctions de sérialisation boost) les conversions nécessaires en QVariant, par exemple avec la classe 'ExtObject3D' :

namespace qx {
namespace cvt {
namespace detail {

template <> struct QxConvert_ToVariant< ExtObject3D > {
static inline QVariant toVariant(const ExtObject3D & t, const QString & format, int index)
{ /* Ici je convertis ExtObject3D en QVariant */ } };

template <> struct QxConvert_FromVariant< ExtObject3D > {
static inline qx_bool fromVariant(const QVariant & v, ExtObject3D & t, const QString & format, int index)
{ /* Ici je convertis QVariant en ExtObject3D */; return qx_bool(true); } };

} // namespace detail
} // namespace cvt
} // namespace qx



Comment utiliser le moteur d'introspection (ou réflexion) de la bibliothèque QxOrm ?

Toute classe enregistrée dans le contexte QxOrm par la méthode qx::register_class<T>() peut être utilisée par le moteur d'introspection (ou réflexion) de la bibliothèque QxOrm. Le moteur d'introspection permet d'obtenir de façon dynamique (donc pendant l'exécution du programme) des informations propres à un type. Ces informations correspondent à des méta-données et décrivent de façon exhaustive les caractéristiques d'une classe (propriétés, méthodes, etc.). De nombreux langages de programmation (par exemple Java ou C#) intègrent nativement ce mécanisme, ce n'est pas le cas du C++, c'est pourquoi la bibliothèque QxOrm émule un moteur d'introspection.

Voici la liste des classes disponibles pour accéder aux méta-données :
  • qx::QxClassX : singleton permettant de parcourir l'ensemble des classes enregistrées dans le contexte QxOrm par la méthode qx::register_class<T>() ;
  • qx::IxClass : interface pour une classe enregistrée dans le contexte QxOrm ;
  • qx::IxDataMemberX : liste des propriétés associées à une classe ;
  • qx::IxDataMember : interface pour une propriété d'une classe ;
  • qx::IxFunctionX : liste des méthodes associées à une classe ;
  • qx::IxFunction : interface pour une méthode d'une classe.
Une instance de type qx::IxClass possède la liste des propriétés d'une classe (qx::IxDataMemberX) ainsi que la liste des méthodes d'une classe (qx::IxFunctionX).

Le moteur d'introspection de la bibliothèque QxOrm permet par exemple de :
  • créer dynamiquement une instance en fonction du nom d'une classe sous forme de chaîne de caractères (qx::create()) ;
  • accéder/modifier le contenu d'un champ d'un objet de façon dynamique en prenant pour paramètres un objet et le nom du champ qu'on souhaite accéder/modifier (qx::IxDataMember::getValue() et qx::IxDataMember::setValue()) ;
  • invoquer une méthode de classe de façon dynamique, en gérant bien entendu le passage des paramètres souhaités à la méthode (qx::IxFunction::invoke()) ;
  • accéder à la hiérarchie d'une classe (qx::IxClass::getBaseClass()).
Remarque : le module QxService de la bibliothèque QxOrm (cliquez ici pour accéder au tutoriel) permettant de créer un serveur d'applications C++ est basé sur le moteur d'introspection pour appeler dynamiquement les méthodes de type service (demande du client) sur le serveur.

Voici un exemple d'utilisation du moteur d'introspection de la bibliothèque QxOrm : comment lister toutes les classes, propriétés et méthodes enregistrées dans le contexte QxOrm ?

QString QxClassX::dumpAllClasses()
{
   QxClassX::registerAllClasses();
   QxCollection<QString, IxClass *> * pAllClasses = QxClassX::getAllClasses();
   if (! pAllClasses) { qAssert(false); return ""; }

   QString sDump;
   long lCount = pAllClasses->count();
   qDebug("[QxOrm] start dump all registered classes (%ld)", lCount);
   _foreach(IxClass * pClass, (* pAllClasses))
   { if (pClass) { sDump += pClass->dumpClass(); } }
   qDebug("[QxOrm] %s", "end dump all registered classes");

   return sDump;
}

QString IxClass::dumpClass() const
{
   QString sDump;
   sDump += "-- class '" + m_sKey + "' (name '" + m_sName + "', ";
   sDump += "description '" + m_sDescription + "', version '" + QString::number(m_lVersion) + "', ";
   sDump += "base class '" + (getBaseClass() ? getBaseClass()->getKey() : "") + "')\n";

   long lCount = (m_pDataMemberX ? m_pDataMemberX->count() : 0);
   sDump += "\t* list of registered properties (" + QString::number(lCount) + ")\n";
   if (m_pDataMemberX)
   {
      IxDataMember * pId = this->getId();
      for (long l = 0; l < lCount; l++)
      {
         IxDataMember * p = m_pDataMemberX->get(l); if (! p) { continue; }
         IxSqlRelation * pRelation = p->getSqlRelation();
         QString sInfos = p->getKey() + ((p == pId) ? QString(" (id)") : QString());
         sInfos += (pRelation ? (QString(" (") + pRelation->getDescription() + QString(")")) : QString());
         sDump += "\t\t" + sInfos + "\n";
      }
   }

   lCount = (m_pFctMemberX ? m_pFctMemberX->count() : 0);
   sDump += "\t* list of registered functions (" + QString::number(lCount) + ")\n";
   if (m_pFctMemberX)
   {
      _foreach_if(IxFunction_ptr p, (* m_pFctMemberX), (p))
      { QString sKey = p->getKey(); sDump += "\t\t" + sKey + "\n"; }
   }

   qDebug("%s", qPrintable(sDump));
   return sDump;
}

Si on utilise la méthode qx::QxClassX::dumpAllClasses() avec le tutoriel qxBlog, voici le résultat obtenu :

[QxOrm] start dump all registered classes (4)
-- class 'author' (name 'author', description '', version '0', base class '')
	* list of registered properties (5)
		author_id (id)
		name
		birthdate
		sex
		list_blog (relation one-to-many)
	* list of registered functions (1)
		age

-- class 'blog' (name 'blog', description '', version '0', base class '')
	* list of registered properties (6)
		blog_id (id)
		blog_text
		date_creation
		author_id (relation many-to-one)
		list_comment (relation one-to-many)
		list_category (relation many-to-many)
	* list of registered functions (0)

-- class 'comment' (name 'comment', description '', version '0', base class '')
	* list of registered properties (4)
		comment_id (id)
		comment_text
		date_creation
		blog_id (relation many-to-one)
	* list of registered functions (0)

-- class 'category' (name 'category', description '', version '0', base class '')
	* list of registered properties (4)
		category_id (id)
		name
		description
		list_blog (relation many-to-many)
	* list of registered functions (0)

[QxOrm] end dump all registered classes

Remarque : il est possible d'ajouter de nouvelles informations au moteur d'introspection en utilisant la notion de property bag. En effet, les classes qx::IxClass, qx::IxDataMember et qx::IxFunction possèdent chacune une liste d'éléments de type QVariant accessibles par clé de type QString (voir la classe qx::QxPropertyBag pour plus de détails sur cette notion).



Comment déclarer automatiquement les méta-propriétés de Qt (définies par la macro Q_PROPERTY) dans le contexte QxOrm ?

Toute classe héritant du type QObject peut déclarer ses propriétés avec la macro Q_PROPERTY : les propriétés deviennent alors des méta-propriétés. Ce mécanisme permet au framework Qt de proposer un moteur d'introspection grâce au pré-compilateur moc. Les méta-propriétés peuvent alors être utilisées par exemple par le moteur QML, QtScript, etc.

La bibliothèque QxOrm nécessite une déclaration de chacune des propriétés d'une classe dans la fonction de mapping void qx::register_class<T>() afin de proposer l'ensemble de ses fonctionnalités (persistance des données, sérialisation XML, JSON et binaire, etc.). Il est possible de déclarer automatiquement dans le contexte QxOrm l'ensemble des méta-propriétés sans maintenir une fonction de mapping void qx::register_class<T>() : la macro QX_REGISTER_ALL_QT_PROPERTIES() utilise le moteur d'introspection de Qt pour parcourir la liste des méta-propriétés.

Voici un exemple d'utilisation avec la classe TestQtProperty se trouvant dans le dossier ./test/qxDllSample/dll1/include/ de la distribution QxOrm :

#ifndef _QX_TEST_QT_META_PROPERTY_H_
#define _QX_TEST_QT_META_PROPERTY_H_
 
class QX_DLL1_EXPORT TestQtProperty : public QObject
{

   Q_OBJECT
   Q_PROPERTY(int id READ id WRITE setId)
   Q_PROPERTY(long number READ number WRITE setNumber)
   Q_PROPERTY(QString desc READ desc WRITE setDesc)
   Q_PROPERTY(QDateTime birthDate READ birthDate WRITE setBirthDate)
   Q_PROPERTY(QVariant photo READ photo WRITE setPhoto)

protected:

   int         m_id;
   long        m_number;
   QString     m_desc;
   QDateTime   m_birthDate;
   QVariant    m_photo;

public:

   TestQtProperty() : QObject(), m_id(0), m_number(0) { ; }
   virtual ~TestQtProperty() { ; }

   int id() const                { return m_id; }
   long number() const           { return m_number; }
   QString desc() const          { return m_desc; }
   QDateTime birthDate() const   { return m_birthDate; }
   QVariant photo() const        { return m_photo; }

   void setId(int i)                         { m_id = i; }
   void setNumber(long l)                    { m_number = l; }
   void setDesc(const QString & s)           { m_desc = s; }
   void setBirthDate(const QDateTime & dt)   { m_birthDate = dt; }
   void setPhoto(const QVariant & v)         { m_photo = v; }
 
};

QX_REGISTER_HPP_QX_DLL1(TestQtProperty, QObject, 0)

#endif // _QX_TEST_QT_META_PROPERTY_H_

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

#include "../include/TestQtProperty.h"

#include <QxOrm_Impl.h>
 
QX_REGISTER_CPP_QX_DLL1(TestQtProperty)
QX_REGISTER_ALL_QT_PROPERTIES(TestQtProperty, "id")

Pour ceux qui ne souhaitent pas utiliser la macro QX_REGISTER_ALL_QT_PROPERTIES, il est possible d'écrire à la place les quatre lignes de code suivantes :

namespace qx {
template <> void register_class(QxClass<TestQtProperty> & t)
{ qx::register_all_qt_properties<TestQtProperty>(t, "id"); }
} // namespace qx

Remarque : le deuxième paramètre de la macro QX_REGISTER_ALL_QT_PROPERTIES permet d'indiquer la propriété qui servira de clé primaire dans la base de données. Si ce paramètre est vide, cela signifie que la classe ne possède pas de clé primaire ou bien que celle-ci est définie dans une classe de base.

Toute propriété définie avec la macro Q_PROPERTY peut s'enregistrer dans le contexte QxOrm de deux manières différentes :
1- par la méthode classique : t.data(& MyQObject::my_property, "my_property", 0);
2- ou bien sans mentionner le pointeur vers la donnée membre de la classe : t.data("my_property", 0);

Peu importe la méthode d'enregistrement des propriétés dans le contexte QxOrm, elles seront accessibles par la même interface qx::IxDataMember et proposent donc les mêmes fonctionnalités. Il est possible d'utiliser les deux méthodes dans une même fonction de mapping void qx::register_class<T>(). Chaque méthode d'enregistrement présente des avantages et inconvénients.

Voici la liste des avantages de la deuxième méthode d'enregistrement des propriétés dans le contexte QxOrm :
  • temps de compilation du projet beaucoup plus rapide ;
  • taille de l'exécutable généré plus petite ;
  • forte intégration avec le moteur d'introspection du framework Qt ;
  • pas besoin de maintenir la fonction de mapping en utilisant la macro QX_REGISTER_ALL_QT_PROPERTIES.
Voici les inconvénients par rapport à la méthode classique d'enregistrement des propriétés :
  • nécessite un héritage de la classe QObject pour pouvoir utiliser la macro Q_PROPERTY ;
  • exécution du programme plus lente (utilisation du type QVariant à la place des template C++) ;
  • ne supporte pas la notion de relation entre tables de la base de données (one-to-one, one-to-many, many-to-one et many-to-many) ;
  • pas d'accès au pointeur sur la donnée membre de la classe (conversion nécessaire au type QVariant pour accéder et modifier une valeur).


Comment construire une requête pour interroger la base de données sans écrire de SQL avec la classe qx::QxSqlQuery ?

La classe qx::QxSqlQuery (ou bien son alias qx_query) permet d'interroger la base de données (trier, filtrer, etc.) de deux manières différentes : Le principal avantage de la première méthode (écriture manuelle des requêtes SQL) est de pouvoir utiliser certaines optimisations spécifiques à chaque base de données.
La deuxième méthode (utilisation du code C++ pour générer la requête SQL) permet de mapper automatiquement les paramètres SQL sans utiliser la fonction qx::QxSqlQuery::bind().

Voici un exemple d'utilisation de la classe qx::QxSqlQuery avec écriture manuelle d'une requête SQL :

// Construit une requête pour récupérer uniquement les 'author' de type 'female'
qx::QxSqlQuery query("WHERE author.sex = :sex");
query.bind(":sex", author::female);

QList<author> list_of_female;
QSqlError daoError = qx::dao::fetch_by_query(query, list_of_female);
for (long l = 0; l < list_of_female.count(); l++)
{ /* traitement avec la collection issue de la base de données */ }

La bibliothèque QxOrm supporte trois syntaxes pour l'écriture des paramètres SQL.
Le type de syntaxe peut être modifié de façon globale à un projet en utilisant la méthode suivante : qx::QxSqlDatabase::getSingleton()->setSqlPlaceHolderStyle().
Les trois paramètres possibles pour cette méthode sont :
  • ph_style_2_point_name : "WHERE author.sex = :sex" (syntaxe par défaut) ;
  • ph_style_at_name : "WHERE author.sex = @sex" ;
  • ph_style_question_mark : "WHERE author.sex = ?".
Voici le même exemple en utilisant les méthodes C++ de la classe qx::QxSqlQuery (ou bien son alias qx_query) pour générer la requête automatiquement :

// Construit une requête pour récupérer uniquement les 'author' de type 'female'
qx_query query;
query.where("author.sex").isEqualTo(author::female);

QList<author> list_of_female;
QSqlError daoError = qx::dao::fetch_by_query(query, list_of_female);
for (long l = 0; l < list_of_female.count(); l++)
{ /* traitement avec la collection issue de la base de données */ }

Cette utilisation de la classe qx::QxSqlQuery présente l'avantage de ne pas avoir à mapper les paramètres de la requête, tout en restant très proche de l'écriture manuelle d'une requête SQL.
Les paramètres seront automatiquement injectés en utilisant la syntaxe définie de manière globale par la méthode : qx::QxSqlDatabase::getSingleton()->getSqlPlaceHolderStyle().

Voici un exemple présentant différentes méthodes disponibles avec la classe qx::QxSqlQuery (ou bien son alias qx_query) :

qx_query query;
query.where("sex").isEqualTo(author::female)
     .and_("age").isGreaterThan(38)
     .or_("last_name").isNotEqualTo("Dupont")
     .or_("first_name").like("Alfred")
     .and_OpenParenthesis("id").isLessThanOrEqualTo(999)
     .and_("birth_date").isBetween(date1, date2)
     .closeParenthesis()
     .or_("id").in(50, 999, 11, 23, 78945)
     .and_("is_deleted").isNotNull()
     .orderAsc("last_name", "first_name", "sex")
     .limit(50, 150);

Ce qui produira le code SQL suivant pour les bases de données MySQL, PostgreSQL et SQLite (pour Oracle et SQLServer, le traitement de la méthode limit() est différent) :

WHERE sex = :sex_1_0 
AND age > :age_3_0 
OR last_name <> :last_name_5_0 
OR first_name LIKE :first_name_7_0 
AND ( id <= :id_10_0 AND birth_date BETWEEN :birth_date_12_0_1 AND :birth_date_12_0_2 ) 
OR id IN (:id_15_0_0, :id_15_0_1, :id_15_0_2, :id_15_0_3, :id_15_0_4) 
AND is_deleted IS NOT NULL 
ORDER BY last_name ASC, first_name ASC, sex ASC 
LIMIT :limit_rows_count_19_0 OFFSET :offset_start_row_19_0

Voici la liste des fonctions et méthodes disponibles pour utiliser la classe qx::QxSqlQuery (ou bien son alias qx_query) :

// avec les fonctions du namespace qx::dao
qx::dao::count<T>()
qx::dao::fetch_by_query<T>()
qx::dao::update_by_query<T>()
qx::dao::delete_by_query<T>()
qx::dao::destroy_by_query<T>()
qx::dao::fetch_by_query_with_relation<T>()
qx::dao::fetch_by_query_with_all_relation<T>()
qx::dao::update_by_query_with_relation<T>()
qx::dao::update_by_query_with_all_relation<T>()
qx::dao::update_optimized_by_query<T>()

// avec la classe qx::QxSession
qx::QxSession::count<T>()
qx::QxSession::fetchByQuery<T>()
qx::QxSession::update<T>()
qx::QxSession::deleteByQuery<T>()
qx::QxSession::destroyByQuery<T>()

// avec la classe qx::QxRepository<T>
qx::QxRepository<T>::count()
qx::QxRepository<T>::fetchByQuery()
qx::QxRepository<T>::update()
qx::QxRepository<T>::deleteByQuery()
qx::QxRepository<T>::destroyByQuery()

Remarque : certaines de ces fonctions ont également deux autres paramètres optionnels :
  • const QStringList & columns : pour indiquer la liste des colonnes à récupérer (par défaut, toutes les colonnes sont récupérées) ;
  • const QStringList & relation : pour indiquer les jointures (one-to-one, one-to-many, many-to-one et many-to-many définies dans la fonction de mapping void qx::register_class<T>()) entre les tables de la base de données (par défaut, aucune relation).


Comment utiliser le cache (fonctions du namespace qx::cache) pour stocker tous types de données ?

Le cache proposé par la bibliothèque QxOrm (module QxCache) est thread-safe et permet de stocker facilement n'importe quel type de données.
Les fonctions pour accéder au cache se trouvent dans le namespace qx::cache.
Le cache permet une optimisation du programme : il est possible par exemple de stocker des éléments issus d'une requête effectuée en base de données.

Chaque élément stocké dans le cache est associé à une clé de type QString : cette clé permet de retrouver rapidement un élément du cache.
Si un nouvel élément est stocké dans le cache avec une clé qui existe déjà, alors l'ancien élément associé à cette clé est effacé automatiquement du cache.

Le cache de la bibliothèque QxOrm ne gère pas la durée de vie des objets : il n'y a aucun delete effectué par le cache.
C'est pourquoi il est fortement recommandé (mais ce n'est pas une obligation) de privilégier le stockage de pointeurs intelligents : par exemple, boost::shared_ptr<T> pour la bibliothèque boost ou bien QSharedPointer<T> pour la bibliothèque Qt.

Le cache peut avoir un coût relatif maximum pour éviter une utilisation de la mémoire trop importante : chaque élément inséré dans le cache peut indiquer un coût représentant une estimation de sa taille mémoire (par exemple, le nombre d'éléments d'une collection).
Lorsque le coût maximum du cache est atteint, les premiers éléments insérés dans le cache sont supprimés (en respectant l'ordre d'insertion dans le cache) jusqu'à ce que la limite du cache ne soit plus dépassée.

Il est possible d'associer à chaque élément du cache une date-heure d'insertion.
Si aucune date-heure n'est renseignée, alors la date-heure courante est prise en compte.
Ce mécanisme permet de vérifier si un élément stocké dans le cache nécessite une mise à jour ou non.

Voici un exemple d'utilisation du cache de la bibliothèque QxOrm (fonctions du namespace qx::cache) :

// Défini le coût maximum du cache à 500
qx::cache::max_cost(500);

// Récupère une liste de 'author' de la base de données
boost::shared_ptr< QList<author> > list_author;
QSqlError daoError = qx::dao::fetch_all(list_author);

// Insère la liste de 'author' dans le cache
qx::cache::set("list_author", list_author);

// Récupère une liste de 'blog' de la base de données
QSharedPointer< std::vector<blog> > list_blog;
daoError = qx::dao::fetch_all(list_blog);

// Insère la liste de 'blog' dans le cache (coût = nombre de 'blog')
qx::cache::set("list_blog", list_blog, list_blog.count());

// Pointeur vers un objet de type 'comment'
comment_ptr my_comment;
my_comment.reset(new comment(50));
daoError = qx::dao::fetch_by_id(my_comment);

// Insère le 'comment' dans le cache en précisant une date-heure d'insertion
qx::cache::set("comment", my_comment, 1, my_comment->dateModif());

// Récupère la liste de 'blog' stockée dans le cache
list_blog = qx::cache::get< QSharedPointer< std::vector<blog> > >("list_blog");

// Récupère la liste de 'blog' sans préciser le type
qx_bool bGetOk = qx::cache::get("list_blog", list_blog);

// Supprime du cache la liste de 'author'
bool bRemoveOk = qx::cache::remove("list_author");

// Compte le nombre d'éléments du cache
long lCount = qx::cache::count();

// Récupère le coût actuel des éléments stockés dans le cache
long lCurrentCost = qx::cache::current_cost();

// Vérifie qu'un élément associé à la clé "comment" existe dans le cache
bool bExist = qx::cache::exist("comment");

// Récupère le 'comment' stocké dans le cache avec sa date-heure d'insertion
QDateTime dt;
bGetOk = qx::cache::get("comment", my_comment, dt);

// Vide le cache
qx::cache::clear();



Comment générer le schéma SQL (création et mise à jour des tables) en fonction des classes persistantes C++ définies dans le contexte QxOrm ?

Il est recommandé d'utiliser l'application QxEntityEditor pour gérer cette problématique.

La bibliothèque QxOrm ne fournit pas de mécanisme pour gérer automatiquement la création et mise à jour des tables dans la base de données.
En effet, la fonction qx::dao::create_table<T> doit être utilisée uniquement pour créer des prototypes.
Il est fortement recommandé d'utiliser un outil spécifique à chaque SGBD pour créer et maintenir les tables de la base de données (par exemple Navicat pour MySql, pgAdmin pour PostgreSQL, SQLite Manager pour SQLite, etc.).
De plus, un outil spécifique à chaque SGBD permet d'appliquer certaines optimisations (ajout d'index par exemple).

Cependant, il peut être intéressant pour certaines applications de ne pas avoir à gérer manuellement les tables de la base de données.
Dans ce cas, il est possible de créer une fonction C++ pour parcourir la liste des classes persistantes enregistrées dans le contexte QxOrm (en utilisant le moteur d'introspection de la bibliothèque) et ainsi créer un script SQL de génération et mise à jour des tables de la base de données.

La bibliothèque QxOrm fournit une fonction C++ créée uniquement à titre d'exemple : elle peut donc servir de base de travail pour créer sa propre fonction de génération de script SQL.
Cette fonction se trouve dans le fichier ./src/QxRegister/QxClassX.cpp et se nomme QString qx::QxClassX::dumpSqlSchema().
Elle génère un script SQL et le renvoie sous forme de QString : il est possible d'adapter cette fonction pour générer un fichier contenant le script SQL ou bien appliquer chaque instruction SQL directement au SGBD.

Voici un exemple d'implémentation proposé par dodobibi pour gérer une base PostgreSQL : cet exemple gère les évolutions futures de son application (ajout de colonnes dans une table existante, ajout d'index sur une colonne existante, etc.).
Au lancement de l'application, le numéro de version est indiqué de la façon suivante :

QApplication app(argc, argv);
app.setProperty("DomainVersion", 1); // Version incrementée à chaque compilation et diffusion de l'application

Une table de la base de données permet de stocker le numéro de version courant.
Une classe persistante C++ est mappée sur cette table de la façon suivante :

#ifndef _DATABASE_VERSION_H_
#define _DATABASE_VERSION_H_
 
class MY_DLL_EXPORT DatabaseVersion
{
public:
  QString name;
  long version;
};

QX_REGISTER_HPP_MY_APP(DatabaseVersion, qx::trait::no_base_class_defined, 0)

#endif // _DATABASE_VERSION_H_

#include "../include/precompiled.h"
#include "../include/DatabaseVersion.h"
#include <QxOrm_Impl.h>
 
QX_REGISTER_CPP_MY_APP(DatabaseVersion)

namespace qx {
template <> void register_class(QxClass<DatabaseVersion> & t)
{
  t.id(& DatabaseVersion::name, "name");
  t.data(& DatabaseVersion::version, "version");
}}

Avec la classe DatabaseVersion, il est possible de vérifier si la version de la base de données est à jour.
C'est le rôle de la fonction isDatabaseVersionOld() :

bool isDatabaseVersionOld()
{
  DatabaseVersion dbVersion;
  dbVersion.name = "MyAppName";
  QSqlError err = qx::dao::fetch_by_id(dbVersion);
  if (err.isValid()) { qAssert(false); return false; }
  return (dbVersion.version < qApp->property("DomainVersion").toInt());
}

Si au lancement de l'application, la fonction isDatabaseVersionOld() renvoie true, alors la mise à jour de la base de données est effectuée de la façon suivante :

void updateDatabaseVersion()
{
  try
  {
    int domainVersion = qApp->property("DomainVersion").toInt();

    // On se connecte avec un utilisateur de la base de données qui a les droits de modifications du schéma
    QSqlDatabase db = qx::QxSqlDatabase::getSingleton()->getDatabaseCloned();
    db.setUserName("MyAdminLogin");
    db.setPassword("MyAdminPassword");

    // On s'assure que la session démarre une transaction et lève une exception à la moindre erreur
    qx::QxSession session(db, true, true);

    // On "fetch" la version de la base de données avec un verrou pour éviter les modifications concurrentes !
    // Si plusieurs utilisateurs lancent l'application en même temps et qu'une mise à jour
    // est nécessaire, le premier fera la mise à jour, et les autres seront en attente
    DatabaseVersion dbVersion;
    session.fetchByQuery(qx_query("WHERE name='MyAppName' FOR UPDATE"), dbVersion);

    // Pour les autres utilisateurs, une fois le verrou levé, on vérifie si la mise à jour est toujours nécessaire
    if (dbVersion.version >= domainVersion) { return; }

    // On exécute chaque instruction SQL avec la variable "query"
    QSqlQuery query(db);

    // On récupère toutes les classes persistantes C++ enregistrées dans le contexte QxOrm
    qx::QxCollection<QString, qx::IxClass *> * pAllClasses = qx::QxClassX::getAllClasses();
    if (! pAllClasses) { qAssert(false); return; }

    // on récupère la liste des tables existantes dans la base (fonction de Qt)
    QStringList tables = db.tables();

    for (long k = 0; k < pAllClasses->count(); k++)
    {
      qx::IxClass * pClass = pAllClasses->getByIndex(k);
      if (! pClass) { continue; }

      // Filtre les classes non persistantes
      if (pClass->isKindOf("qx::service::IxParameter") || pClass->isKindOf("qx::service::IxService")) { continue; }

      // Filtre les classes à jour : si la version de pClass est <= à la version enregistrée dans la base, la mise à jour n'est pas nécessaire
      if (pClass->getVersion() <= dbVersion.version) { continue; }

      // On crée la table si elle n'existe pas, et on définit son propriétaire
      if (! tables.contains(pClass->getName()))
      {
        query.exec("CREATE TABLE " + pClass->getName() + " ( ) WITH (OIDS = FALSE);"
                   "ALTER TABLE " + pClass->getName() + " OWNER TO \"MyAdminLogin\";");
        session += query.lastError();
      }

      // On ajoute les colonnes à la table si elles n'existent pas
      qx::IxDataMemberX * pDataMemberX = pClass->getDataMemberX();
      for (long l = 0; (pDataMemberX && (l < pDataMemberX->count_WithDaoStrategy())); l++)
      {
        qx::IxDataMember * p = pDataMemberX->get_WithDaoStrategy(l);
        if (! p || (p->getVersion() <= dbVersion.version)) { continue; }

        query.exec("ALTER TABLE " + pClass->getName() + " ADD COLUMN " + p->getName() + " " + p->getSqlType() + ";");
        session += query.lastError();

        if (p->getIsPrimaryKey()) // PRIMARY KEY
        {
          query.exec("ALTER TABLE " + pClass->getName() + " ADD PRIMARY KEY (" + p->getName() + ");");
          session += query.lastError();
        }

        if (p->getAllPropertyBagKeys().contains("INDEX")) // INDEX
        {
          query.exec("CREATE INDEX " + pClass->getName() + "_" + p->getName() + "_idx" + 
                     " ON " + pClass->getName() + " USING " + p->getPropertyBag("INDEX").toString() + " (" + p->getName() + ");");
          session += query.lastError();
        }

        if (p->getNotNull()) // NOT NULL
        {
          query.exec("ALTER TABLE " + pClass->getName() + " ALTER COLUMN " + p->getName() + " SET NOT NULL;");
          session += query.lastError();
        }

        if (p->getAutoIncrement()) // AUTO INCREMENT
        {
          query.exec("CREATE SEQUENCE " + pClass->getName() + "_" + p->getName() + "_seq" + "; "
                     "ALTER TABLE " + pClass->getName() + "_" + p->getName() + "_seq" + " OWNER TO \"MyAdminLogin\"; "
                     "ALTER TABLE " + pClass->getName() + " ALTER COLUMN " + p->getName() + " " +
                     "SET DEFAULT nextval('" + pClass->getName() + "_" + p->getName() + "_seq" + "'::regclass);");
          session += query.lastError();
        }

        if (p->getDescription() != "") // DESCRIPTION
        {
          // $$ceci est un texte ne nécessitant pas de caractères d'échappement dans postgres grace aux doubles dolars$$
          query.exec("COMMENT ON COLUMN " + pClass->getName() + "." + p->getName() + " IS $$" + p->getDescription() + "$$ ;");
          session += query.lastError();
        }
      }
    }

    // On enregistre la version courante de la base de données
    dbVersion.version = domainVersion;
    session.save(dbVersion);

    // Fin du block "try" : la session est détruite => commit ou rollback automatique
    // De plus, un commit ou rollback sur la transaction lève automatiquement le verrou posé précédemment
  }
  catch (const qx::dao::sql_error & err)
  {
    QSqlError sqlError = err.get();
    qDebug() << sqlError.databaseText();
    qDebug() << sqlError.driverText();
    qDebug() << sqlError.number();
    qDebug() << sqlError.type();
  }
}

Remarque : le code précédent (tout comme la fonction qx::QxClassX::dumpSqlSchema()) peut être modifié pour s'adapter aux besoins spécifiques d'une application.
Par exemple, il pourrait être intéressant de créer par défaut une seconde table (en plus de la table DatabaseVersion) pour enregistrer la liste des classes persistantes enregistrées dans le contexte QxOrm : ainsi, au lieu d'utiliser la fonction proposée par Qt "db.tables()", il serait possible de récupérer toutes les tables mappées sur des classes persistantes avec des informations supplémentaires (numéro de version pour chaque table, nombre de colonnes enregistrées dans le contexte QxOrm, description de chaque table, etc.).


Comment associer un type SQL à une classe C++ ?

Chaque base de données propose des types SQL différents pour stocker l'information.
La bibliothèque QxOrm propose une association par défaut pour les classes C++ les plus fréquemment utilisées dans un programme.
Voici cette association par défaut :

"bool" <-> "SMALLINT"
"qx_bool" <-> "SMALLINT"
"short" <-> "SMALLINT"
"int" <-> "INTEGER"
"long" <-> "INTEGER"
"long long" <-> "INTEGER"
"float" <-> "FLOAT"
"double" <-> "FLOAT"
"long double" <-> "FLOAT"
"unsigned short" <-> "SMALLINT"
"unsigned int" <-> "INTEGER"
"unsigned long" <-> "INTEGER"
"unsigned long long" <-> "INTEGER"
"std::string" <-> "TEXT"
"std::wstring" <-> "TEXT"
"QString" <-> "TEXT"
"QVariant" <-> "TEXT"
"QUuid" <-> "TEXT"
"QDate" <-> "DATE"
"QTime" <-> "TIME"
"QDateTime" <-> "TIMESTAMP"
"QByteArray" <-> "BLOB"
"qx::QxDateNeutral" <-> "TEXT"
"qx::QxTimeNeutral" <-> "TEXT"
"qx::QxDateTimeNeutral" <-> "TEXT"

Si le type SQL proposé par défaut par la bibliothèque QxOrm ne correspond pas à la base de données utilisée, il peut facilement être modifié (de manière globale à toute l'application) en utilisant la collection suivante :

QHash<QString, QString> * lstSqlType = qx::QxClassX::getAllSqlTypeByClassName();
lstSqlType->insert("QString", "VARCHAR(255)");
lstSqlType->insert("std::string", "VARCHAR(255)");
// etc.

Pour modifier le type SQL de manière spécifique pour une colonne d'une table de la base de données, il faut définir le type SQL dans la fonction de mapping de QxOrm :

namespace qx {
template <> void register_class(QxClass<MyClass> & t)
{
  //...
  IxDataMember * p =  t.data(& MyClass::m_MyProperty, "my_property");
  p->setSqlType("VARCHAR(255)");
  //...
}}

Pour les classes non supportées par défaut par la bibliothèque QxOrm (voir cette Question-Réponse de la FAQ : Comment persister un type dont on ne possède pas le code source (classe provenant d'une bibliothèque tierce par exemple) ?), il est possible d'associer un type SQL par défaut en utilisant la macro suivante (en dehors de tout namespace) :

QX_REGISTER_TRAIT_GET_SQL_TYPE(MyClass, "my_sql_type")



Comment utiliser le module QxValidator pour valider automatiquement les données ?

Le module QxValidator de la bibliothèque QxOrm permet d'ajouter des contraintes sur les propriétés enregistrées dans le contexte QxOrm.
Ces contraintes sont définies dans la méthode de mapping : void qx::register_class<T>.
Si pour une instance de classe donnée, au moins une contrainte n'est pas respectée, alors l'instance est considérée comme invalide : l'objet ne peut alors pas être sauvegardé en base de données (INSERT ou UPDATE).

Il est également possible d'utiliser le module QxValidator pour valider les données au niveau de la couche présentation de l'application : si les données saisies par un utilisateur ne sont pas valides, un message d'erreur peut être signalé, il n'est alors pas nécessaire d'essayer d'enregistrer l'instance courante en base de données.
Les règles de validation n'ont pas besoin d'être dupliquées : elles peuvent être utilisées aussi bien par la couche présentation que par la couche d'accès aux données de l'application.

Voici la description de quelques classes du module QxValidator : Le module QxValidator gère automatiquement la notion d'héritage de classe : si des contraintes sont définies au niveau de la classe de base, alors elles seront automatiquement vérifiées pour chaque validation d'une classe dérivée.

Voici un exemple d'utilisation du module QxValidator avec une classe 'person' :

* fichier 'person.h' :
#ifndef _CLASS_PERSON_H_
#define _CLASS_PERSON_H_
 
class person
{

public:

   enum sex { male, female, unknown };

   long        _id;
   QString     _firstName;
   QString     _lastName;
   QDateTime   _birthDate;
   sex         _sex;

   person() : _id(0), _sex(unknown) { ; }
   person(long id) : _id(id), _sex(unknown) { ; }
   virtual ~person() { ; }

private:

   void isValid(qx::QxInvalidValueX & invalidValues);

};

QX_REGISTER_HPP_MY_EXE(person, qx::trait::no_base_class_defined, 0)

#endif // _CLASS_PERSON_H_

* fichier 'person.cpp' :
#include "../include/precompiled.h"

#include "../include/person.h"
#include "../include/global_validator.h"

#include <QxOrm_Impl.h>

QX_REGISTER_CPP_MY_EXE(person)

namespace qx {
template <> void register_class(QxClass<person> & t)
{
   t.id(& person::_id, "id");

   t.data(& person::_firstName, "firstName");
   t.data(& person::_lastName, "lastName");
   t.data(& person::_birthDate, "birthDate");
   t.data(& person::_sex, "sex");

   QxValidatorX<person> * pAllValidator = t.getAllValidator();
   pAllValidator->add_NotEmpty("firstName");
   pAllValidator->add_NotEmpty("lastName", "a person must have a lastname");
   pAllValidator->add_CustomValidator(& person::isValid);
   pAllValidator->add_CustomValidator_QVariant(& validateFirstName, "firstName");
   pAllValidator->add_CustomValidator_DataType<QDateTime>(& validateDateTime, "birthDate");
}}

void person::isValid(qx::QxInvalidValueX & invalidValues)
{
   // Cette méthode est appelée automatiquement par le module 'QxValidator' :
   // - avant d'insérer ou mettre à jour une instance de type 'person' par les fonctions du namespace 'qx::dao' ;
   // - en utilisant la fonction 'qx::validate()' avec pour paramètre une instance de type 'person'.

   // L'enregistrement de la méthode 'person::isValid()' est effectué dans la fonction de mapping :
   // pAllValidator->add_CustomValidator(& person::isValid);

   // Dans cette méthode, il est possible de vérifier n'importe quelle valeur de l'instance courante
   // Si une propriété est non valide, il suffit d'insérer un élément dans la collection 'invalidValues'

   // Remarque : cette méthode est déclarée 'private' pour forcer l'utilisateur à utiliser la fonction 'qx::validate()'
   // Mais ce n'est pas une obligation : cette méthode peut être déclarée 'public' ou 'protected'

   // Par exemple, si on souhaite vérifier la propriété '_sex' d'une personne :
   if ((_sex != male) && (_sex != female))
   { invalidValues.insert("le sexe de la personne doit être défini : masculin ou féminin"); }
}

* fichier 'global_validator.h' :
// Les fonctions suivantes ('validateFirstName()' et 'validateDateTime()') sont globales (non liées à une classe)
// Elles peuvent ainsi être utilisées par plusieurs classes pour valider une propriété (par exemple : valider la saisie d'une adresse IP).
// Ces fonctions seront appelées automatiquement par le module 'QxValidator' :
// - avant d'insérer ou mettre à jour une instance de classe par les fonctions du namespace 'qx::dao' ;
// - en utilisant la fonction 'qx::validate()'.
 
void validateFirstName(const QVariant & value, const qx::IxValidator * validator, qx::QxInvalidValueX & invalidValues)
{
   // Ici, on peut tester la valeur d'une propriété (convertie en type QVariant)
   // Si la valeur est invalide, il suffit d'insérer un message à la collection 'invalidValues'

   // Par exemple, si la valeur ne doit jamais être égale à "admin" :
   if (value.toString() == "admin")
   { invalidValues.insert("la valeur ne peut pas être égale à 'admin'"); }
}

void validateDateTime(const QDateTime & value, const qx::IxValidator * validator, qx::QxInvalidValueX & invalidValues)
{
   // Ici, on peut tester la valeur d'une propriété (en conservant son vrai type, ici il s'agit de tester une date-heure de type 'QDateTime')
   // Si la valeur est invalide, il suffit d'insérer un message à la collection 'invalidValues'

   // Par exemple, si la date-heure doit forcément être renseignée :
   if (! value.isValid())
   { invalidValues.insert("la date-heure doit être renseignée et doit être valide"); }
}

* fichier 'main.cpp' :
person personValidate;
personValidate._lastName = "admin";
qx::QxInvalidValueX invalidValues = qx::validate(personValidate);
QString sInvalidValues = invalidValues.text();
qDebug("[QxOrm] test 'QxValidator' module :\n%s", qPrintable(sInvalidValues));

A l'exécution de ce bout de code, l'instance 'personValidate' est non valide : la collection 'invalidValues' contient quatre éléments :
- "la valeur de la propriété 'firstName' ne peut pas être vide" ;
- "le sexe de la personne doit être défini : masculin ou féminin" ;
- "la valeur ne peut pas être égale à 'admin'" ;
- "la date-heure doit être renseignée et doit être valide".

Le module QxValidator fournit plusieurs validateurs pour effectuer des vérifications basiques :
  • add_NotNull() : vérifie que la valeur n'est pas nulle ;
  • add_NotEmpty() : vérifie que la chaîne de caractères n'est pas vide ;
  • add_MinValue() : vérifie que la valeur numérique n'est pas inférieure au paramètre ;
  • add_MaxValue() : vérifie que la valeur numérique n'est pas supérieure au paramètre ;
  • add_Range() : vérifie que la valeur numérique est comprise entre les deux paramètres ;
  • add_MinDecimal() : vérifie que la valeur décimale n'est pas inférieure au paramètre ;
  • add_MaxDecimal() : vérifie que la valeur décimale n'est pas supérieure au paramètre ;
  • add_RangeDecimal() : vérifie que la valeur décimale est comprise entre les deux paramètres ;
  • add_MinLength() : vérifie que la chaîne de caractères a une taille minimale ;
  • add_MaxLength() : vérifie que la chaîne de caractères ne dépasse pas un certain nombre de caractères ;
  • add_Size() : vérifie que la taille de la chaîne de caractères est comprise entre les deux paramètres ;
  • add_DatePast() : vérifie que la date-heure est dans le passé ;
  • add_DateFuture() : vérifie que la date-heure est dans le futur ;
  • add_RegExp() : vérifie que la chaîne de caractères est compatible avec l'expression régulière passée en paramètre ;
  • add_EMail() : vérifie que la chaîne de caractères correspond à un e-mail.
Comme dans l'exemple de la classe 'person', il est possible de définir également des validateurs personnalisés : ce sont des fonctions ou méthodes de classe qui seront appelées automatiquement par le module QxValidator pour valider une propriété ou une instance de classe.
Il existe trois types de validateurs personnalisés :
  • add_CustomValidator() : méthode de classe, la signature de la méthode doit être "void my_class::my_method(qx::QxInvalidValueX &)" ;
  • add_CustomValidator_QVariant() : fonction globale avec type QVariant (propriété convertie en QVariant), la signature de la fonction doit être "void my_validator(const QVariant &, const qx::IxValidator *, qx::QxInvalidValueX &)" ;
  • add_CustomValidator_DataType() : fonction globale avec le type réel de la propriété, la signature de la fonction doit être "void my_validator(const T &, const qx::IxValidator *, qx::QxInvalidValueX &)" ;
Remarque : à chaque validateur peut être associé un groupe (paramètre optionnel pour chaque méthode add_XXX() de la classe qx::IxValidatorX).
Il est ainsi possible de créer des groupes de validation suivant le contexte d'exécution : par exemple, valider la saisie d'une personne sur une IHM A ne nécessite peut-être pas les mêmes vérifications que valider une personne sur une IHM B.
Pour exécuter la validation d'une instance pour un groupe donné (par exemple "myGroup"), il faut appeler la fonction suivante : "qx::QxInvalidValueX invalidValues = qx::validate(personValidate, "myGroup");".

Autre remarque : le module QxValidator définit des messages par défaut lorsqu'une contrainte n'est pas vérifiée.
Il est possible de redéfinir ces messages par défaut en modifiant la collection suivante : "QHash * lstMessage = QxClassX::getAllValidatorMessage();".
Par exemple : "lstMessage->insert("min_value", "la valeur '%NAME%' doit être inférieure ou égale à '%CONSTRAINT%'");".
Les champs %NAME% et %CONSTRAINT% seront automatiquement remplacés par les valeurs correspondantes.
Pour modifier le message pour un validateur donné (et non de manière globale), il faut utiliser le paramètre optionnel disponible pour les méthodes add_XXX() de la classe qx::IxValidatorX.


Comment utiliser l'interface qx::IxPersistable ?

L'interface qx::IxPersistable (ou classe abstraite) dispose uniquement de méthodes virtuelles pures.
Elle permet d'avoir une classe de base commune pour appeler les fonctions de persistance sans connaître le type réel de l'instance courante (notion de polymorphisme).
La bibliothèque QxOrm n'impose pas de travailler avec une classe de base pour enregistrer un type persistant dans le contexte QxOrm, cependant il est parfois utile de disposer d'une interface afin d'écrire des algorithmes génériques.

La classe qx::IxPersistable met à disposition les méthodes virtuelles suivantes (pour plus d'informations sur ces méthodes, rendez-vous sur la documentation en ligne de la bibliothèque QxOrm) :

virtual long qxCount(const qx::QxSqlQuery & query = qx::QxSqlQuery(), QSqlDatabase * pDatabase = NULL);
virtual QSqlError qxFetchById(const QVariant & id = QVariant(), const QStringList & columns = QStringList(), const QStringList & relation = QStringList(), QSqlDatabase * pDatabase = NULL);
virtual QSqlError qxFetchAll(qx::IxCollection & list, const QStringList & columns = QStringList(), const QStringList & relation = QStringList(), QSqlDatabase * pDatabase = NULL);
virtual QSqlError qxFetchByQuery(const qx::QxSqlQuery & query, qx::IxCollection & list, const QStringList & columns = QStringList(), const QStringList & relation = QStringList(), QSqlDatabase * pDatabase = NULL);
virtual QSqlError qxInsert(const QStringList & relation = QStringList(), QSqlDatabase * pDatabase = NULL);
virtual QSqlError qxUpdate(const qx::QxSqlQuery & query = qx::QxSqlQuery(), const QStringList & columns = QStringList(), const QStringList & relation = QStringList(), QSqlDatabase * pDatabase = NULL);
virtual QSqlError qxSave(const QStringList & relation = QStringList(), QSqlDatabase * pDatabase = NULL);
virtual QSqlError qxDeleteById(const QVariant & id = QVariant(), QSqlDatabase * pDatabase = NULL);
virtual QSqlError qxDeleteAll(QSqlDatabase * pDatabase = NULL);
virtual QSqlError qxDeleteByQuery(const qx::QxSqlQuery & query, QSqlDatabase * pDatabase = NULL);
virtual QSqlError qxDestroyById(const QVariant & id = QVariant(), QSqlDatabase * pDatabase = NULL);
virtual QSqlError qxDestroyAll(QSqlDatabase * pDatabase = NULL);
virtual QSqlError qxDestroyByQuery(const qx::QxSqlQuery & query, QSqlDatabase * pDatabase = NULL);
virtual qx_bool qxExist(const QVariant & id = QVariant(), QSqlDatabase * pDatabase = NULL);
virtual qx::QxInvalidValueX qxValidate(const QStringList & groups = QStringList());
virtual qx::IxPersistableCollection_ptr qxNewPersistableCollection() const;
virtual qx::IxClass * qxClass() const;

Par exemple, à partir d'une liste de pointeurs de type qx::IxPersistable, il est possible d'enregistrer les éléments dans plusieurs tables différentes de la base de données de la façon suivante :

QList<qx::IxPersistable *> lst = ...;
foreach(qx::IxPersistable * p, lst)
{
   QSqlError daoError = p->qxSave();
   if (daoError.isValid()) { /* an error occured */ }
   // etc...
}

Pour implémenter l'interface qx::IxPersistable, il faut :
  • faire hériter la classe persistante du type qx::IxPersistable ;
  • dans la définition de la classe (myClass.h par exemple), ajouter la macro QX_PERSISTABLE_HPP(myClass) ;
  • dans l'implémentation de la classe (myClass.cpp par exemple), ajouter la macro QX_PERSISTABLE_CPP(myClass).
Par exemple, implémenter l'interface qx::IxPersistable pour la classe author du tutoriel qxBlog revient à écrire (les modifications par rapport au code du tutoriel apparaissent en gras) :

#ifndef _QX_BLOG_AUTHOR_H_
#define _QX_BLOG_AUTHOR_H_

class blog;

class QX_BLOG_DLL_EXPORT author : public qx::IxPersistable
{
   QX_PERSISTABLE_HPP(author)
public:
// -- typedef
   typedef boost::shared_ptr<blog> blog_ptr;
   typedef std::vector<blog_ptr> list_blog;
// -- enum
   enum enum_sex { male, female, unknown };
// -- propriétés
   QString     m_id;
   QString     m_name;
   QDate       m_birthdate;
   enum_sex    m_sex;
   list_blog   m_blogX;
// -- constructeur, destructeur virtuel
   author() : m_id(0), m_sex(unknown) { ; }
   virtual ~author() { ; }
// -- méthodes
   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 <QxOrm_Impl.h>

QX_REGISTER_CPP_QX_BLOG(author)
QX_PERSISTABLE_CPP(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());
}

Remarque : le projet de test ./test/qxDllSample/dll1/ met à disposition une sorte de 'super classe de base' : la classe qx::QxPersistable implémente l'interface qx::IxPersistable et hérite de QObject.
Le mécanisme SIGNAL-SLOT de Qt peut donc être utilisé avec cette classe, ce qui peut être intéressant par exemple pour la notion de déclencheurs (ou trigger).
La classe qx::QxPersistable met également à disposition des méthodes virtuelles qu'il est possible de surcharger pour gérer notamment la notion de validation des données avec le module QxValidator.
La classe qx::QxPersistable ne fait pas partie de la distribution de QxOrm, mais il est possible de la copier-coller dans un projet afin de profiter de ses fonctionnalités :


Comment utiliser le moteur de relations pour récupérer des données associées à plusieurs tables ?

La bibliothèque QxOrm supporte quatre types de relations pour lier les classes C++ enregistrées dans le contexte QxOrm : one-to-one, one-to-many, many-to-one et many-to-many.
Pour plus de détails sur la définition de ces relations, il est conseillé de lire le tutoriel qxBlog.
Nous allons détailler dans cette Q&R les différentes méthodes de récupération des données (module QxDao, fonctions du namespace qx::dao) : Le premier paramètre des fonctions fetch_by_id_with_relation, fetch_all_with_relation et fetch_by_query_with_relation correspond à la liste des relations à requêter.
Cette liste de relations peut contenir les éléments suivants :
  • identifiant d'une relation : chaque relation possède une clé définie au niveau de la fonction de paramétrage qx::register_class<T> ;
  • le mot-clé "*" signifie "récupérer toutes les relations définies dans la fonction de paramétrage qx::register_class<T> sur un niveau" ;
  • le mot-clé "->" signifie jointure de type "LEFT OUTER JOIN" (jointure par défaut de la bibliothèque QxOrm) ;
  • le mot-clé ">>" signifie jointure de type "INNER JOIN" entre deux tables.
Remarque : en utilisant le mot-clé "*" pour indiquer "toutes les relations sur un niveau", les appels suivants sont équivalents :
  • qx::dao::fetch_by_id_with_relation("*", ...) == qx::dao::fetch_by_id_with_all_relation(...) ;
  • qx::dao::fetch_by_query_with_relation("*", ...) == qx::dao::fetch_by_query_with_all_relation(...) ;
  • qx::dao::fetch_all_with_relation("*", ...) == qx::dao::fetch_all_with_all_relation(...).

Exemple : à partir du tutoriel qxBlog, il est possible de récupérer les données suivantes avec une seule requête :

1- récupérer un blog et son author ;
2- pour l'author valorisé, récupérer tous les blog qu'il a écrit ;
3- pour chaque blog que l'author a écrit, récupérer tous les comment associés.

blog_ptr my_blog = blog_ptr(new blog(10));
QSqlError daoError = qx::dao::fetch_by_id_with_relation("author_id->list_blog->list_comment", my_blog);

Ce qui génère la requête SQL suivante :
SELECT blog.blog_id AS blog_blog_id_0, blog.blog_text AS blog_blog_text_0, blog.date_creation AS blog_date_creation_0, blog.author_id AS blog_author_id_0, 
       author_1.author_id AS author_1_author_id_0, author_1.name AS author_1_name_0, author_1.birthdate AS author_1_birthdate_0, author_1.sex AS author_1_sex_0, 
       blog_2.blog_id AS blog_2_blog_id_0, blog_2.author_id AS blog_2_author_id_0, blog_2.blog_text AS blog_2_blog_text_0, blog_2.date_creation AS blog_2_date_creation_0, 
       comment_4.comment_id AS comment_4_comment_id_0, comment_4.blog_id AS comment_4_blog_id_0, comment_4.comment_text AS comment_4_comment_text_0, comment_4.date_creation AS comment_4_date_creation_0 
FROM blog 
LEFT OUTER JOIN author author_1 ON author_1.author_id = blog.author_id 
LEFT OUTER JOIN blog blog_2 ON blog_2.author_id = author_1.author_id 
LEFT OUTER JOIN comment comment_4 ON comment_4.blog_id = blog_2.blog_id 
WHERE blog.blog_id = :blog_id


Autre exemple : il est également possible de créer une liste de relations à récupérer, comme ceci par exemple :

blog_ptr my_blog = blog_ptr(new blog(10));
QStringList relation;
relation << "author_id->list_blog->list_comment";
relation << "author_id->list_blog->list_category";
relation << "list_comment";
relation << "list_category";
QSqlError daoError = qx::dao::fetch_by_id_with_relation(relation, my_blog);

Ce qui génère la requête SQL suivante :
SELECT blog.blog_id AS blog_blog_id_0, blog.blog_text AS blog_blog_text_0, blog.date_creation AS blog_date_creation_0, blog.author_id AS blog_author_id_0, 
       author_1.author_id AS author_1_author_id_0, author_1.name AS author_1_name_0, author_1.birthdate AS author_1_birthdate_0, author_1.sex AS author_1_sex_0, 
       blog_2.blog_id AS blog_2_blog_id_0, blog_2.author_id AS blog_2_author_id_0, blog_2.blog_text AS blog_2_blog_text_0, blog_2.date_creation AS blog_2_date_creation_0, 
       category_5.category_id AS category_5_category_id_0, category_5.name AS category_5_name_0, category_5.description AS category_5_description_0, 
       comment_6.comment_id AS comment_6_comment_id_0, comment_6.blog_id AS comment_6_blog_id_0, comment_6.comment_text AS comment_6_comment_text_0, comment_6.date_creation AS comment_6_date_creation_0, 
       category_7.category_id AS category_7_category_id_0, category_7.name AS category_7_name_0, category_7.description AS category_7_description_0 
FROM blog 
LEFT OUTER JOIN author author_1 ON author_1.author_id = blog.author_id 
LEFT OUTER JOIN blog blog_2 ON blog_2.author_id = author_1.author_id 
LEFT OUTER JOIN category_blog category_blog_5 ON blog_2.blog_id = category_blog_5.blog_id 
LEFT OUTER JOIN category category_5 ON category_blog_5.category_id = category_5.category_id 
LEFT OUTER JOIN comment comment_6 ON comment_6.blog_id = blog.blog_id 
LEFT OUTER JOIN category_blog category_blog_7 ON blog.blog_id = category_blog_7.blog_id 
LEFT OUTER JOIN category category_7 ON category_blog_7.category_id = category_7.category_id 
WHERE blog.blog_id = :blog_id


Autre exemple : pour récupérer toutes les relations pour un niveau donné, il faut utiliser le mot-clé "*".
Pour récupérer toutes les données de toutes les relations sur trois niveaux, il faut écrire :

blog_ptr my_blog = blog_ptr(new blog(10));
QSqlError daoError = qx::dao::fetch_by_id_with_relation("*->*->*", my_blog);

Ce qui génère la requête SQL suivante :
SELECT blog.blog_id AS blog_blog_id_0, blog.blog_text AS blog_blog_text_0, blog.date_creation AS blog_date_creation_0, blog.author_id AS blog_author_id_0, 
       author_1.author_id AS author_1_author_id_0, author_1.name AS author_1_name_0, author_1.birthdate AS author_1_birthdate_0, author_1.sex AS author_1_sex_0, 
       blog_2.blog_id AS blog_2_blog_id_0, blog_2.author_id AS blog_2_author_id_0, blog_2.blog_text AS blog_2_blog_text_0, blog_2.date_creation AS blog_2_date_creation_0, blog_2.author_id AS blog_2_author_id_0_2, 
       author_3.author_id AS author_3_author_id_0, author_3.name AS author_3_name_0, author_3.birthdate AS author_3_birthdate_0, author_3.sex AS author_3_sex_0, 
       comment_4.comment_id AS comment_4_comment_id_0, comment_4.blog_id AS comment_4_blog_id_0, comment_4.comment_text AS comment_4_comment_text_0, comment_4.date_creation AS comment_4_date_creation_0, 
       category_5.category_id AS category_5_category_id_0, category_5.name AS category_5_name_0, category_5.description AS category_5_description_0, 
       comment_6.comment_id AS comment_6_comment_id_0, comment_6.blog_id AS comment_6_blog_id_0, comment_6.comment_text AS comment_6_comment_text_0, comment_6.date_creation AS comment_6_date_creation_0, comment_6.blog_id AS comment_6_blog_id_0_6, 
       blog_7.blog_id AS blog_7_blog_id_0, blog_7.blog_text AS blog_7_blog_text_0, blog_7.date_creation AS blog_7_date_creation_0, blog_7.author_id AS blog_7_author_id_0_7, 
       author_8.author_id AS author_8_author_id_0, author_8.name AS author_8_name_0, author_8.birthdate AS author_8_birthdate_0, author_8.sex AS author_8_sex_0, 
       comment_9.comment_id AS comment_9_comment_id_0, comment_9.blog_id AS comment_9_blog_id_0, comment_9.comment_text AS comment_9_comment_text_0, comment_9.date_creation AS comment_9_date_creation_0, 
       category_10.category_id AS category_10_category_id_0, category_10.name AS category_10_name_0, category_10.description AS category_10_description_0, 
       category_11.category_id AS category_11_category_id_0, category_11.name AS category_11_name_0, category_11.description AS category_11_description_0, 
       blog_12.blog_id AS blog_12_blog_id_0, blog_12.blog_text AS blog_12_blog_text_0, blog_12.date_creation AS blog_12_date_creation_0, blog_12.author_id AS blog_12_author_id_0_12, 
       author_13.author_id AS author_13_author_id_0, author_13.name AS author_13_name_0, author_13.birthdate AS author_13_birthdate_0, author_13.sex AS author_13_sex_0, 
       comment_14.comment_id AS comment_14_comment_id_0, comment_14.blog_id AS comment_14_blog_id_0, comment_14.comment_text AS comment_14_comment_text_0, comment_14.date_creation AS comment_14_date_creation_0, 
       category_15.category_id AS category_15_category_id_0, category_15.name AS category_15_name_0, category_15.description AS category_15_description_0 
FROM blog 
LEFT OUTER JOIN author author_1 ON author_1.author_id = blog.author_id 
LEFT OUTER JOIN blog blog_2 ON blog_2.author_id = author_1.author_id 
LEFT OUTER JOIN author author_3 ON author_3.author_id = blog_2.author_id 
LEFT OUTER JOIN comment comment_4 ON comment_4.blog_id = blog_2.blog_id 
LEFT OUTER JOIN category_blog category_blog_5 ON blog_2.blog_id = category_blog_5.blog_id 
LEFT OUTER JOIN category category_5 ON category_blog_5.category_id = category_5.category_id 
LEFT OUTER JOIN comment comment_6 ON comment_6.blog_id = blog.blog_id 
LEFT OUTER JOIN blog blog_7 ON blog_7.blog_id = comment_6.blog_id 
LEFT OUTER JOIN author author_8 ON author_8.author_id = blog_7.author_id 
LEFT OUTER JOIN comment comment_9 ON comment_9.blog_id = blog_7.blog_id 
LEFT OUTER JOIN category_blog category_blog_10 ON blog_7.blog_id = category_blog_10.blog_id 
LEFT OUTER JOIN category category_10 ON category_blog_10.category_id = category_10.category_id 
LEFT OUTER JOIN category_blog category_blog_11 ON blog.blog_id = category_blog_11.blog_id 
LEFT OUTER JOIN category category_11 ON category_blog_11.category_id = category_11.category_id 
LEFT OUTER JOIN category_blog category_blog_12 ON category_11.category_id = category_blog_12.category_id 
LEFT OUTER JOIN blog blog_12 ON category_blog_12.blog_id = blog_12.blog_id 
LEFT OUTER JOIN author author_13 ON author_13.author_id = blog_12.author_id 
LEFT OUTER JOIN comment comment_14 ON comment_14.blog_id = blog_12.blog_id 
LEFT OUTER JOIN category_blog category_blog_15 ON blog_12.blog_id = category_blog_15.blog_id 
LEFT OUTER JOIN category category_15 ON category_blog_15.category_id = category_15.category_id 
WHERE blog.blog_id = :blog_id



Comment appeler une procédure stockée ou une requête SQL personnalisée ?

La bibliothèque QxOrm fournit deux fonctions pour appeler une procédure stockée ou une requête SQL personnalisée : Le premier paramètre de ces deux fonctions, de type qx::QxSqlQuery (ou qx_query), correspond à la procédure stockée ou à la requête SQL personnalisée.
Pour plus d'informations sur la classe qx::QxSqlQuery, rendez-vous sur cette Q&R de la FAQ de QxOrm : Comment construire une requête pour interroger la base de données sans écrire de SQL avec la classe qx::QxSqlQuery ?

La fonction qx::dao::execute_query<T>() est une fonction template : le type T doit être enregistré dans le contexte QxOrm (fonction qx::register_class<T>).
Toutes les données renvoyées par la procédure stockée ou la requête SQL personnalisée qui pourront être associées aux membres des classes C++ (de type T) seront valorisées automatiquement.
Une recherche automatique est effectuée sur le nom des champs associés aux données.
Voici un exemple d'utilisation (disponible dans le projet qxBlog du package QxOrm) :

// Call a custom SQL query or a stored procedure and fetch automatically properties (with a collection of items)
qx_query testStoredProcBis("SELECT * FROM author");
daoError = qx::dao::execute_query(testStoredProcBis, authorX);
qAssert(! daoError.isValid()); qAssert(authorX.count() > 0);
qx::dump(authorX);


La fonction qx::dao::call_query() n'est pas une fonction template : les résultats de la requête doivent être parcourus manuellement sur la classe qx::QxSqlQuery (ou qx_query).
Pour récupérer un paramètre de sortie (qui doit être passé à la requête en tant que QSql::Out ou QSql::InOut), il suffit d'utiliser la méthode : QVariant qx::QxSqlQuery::boundValue(const QString & sKey) const;.

Pour parcourir la liste des résultats de la requête, il faut utiliser les méthodes suivantes :
* long qx::QxSqlQuery::getSqlResultRowCount() const;
* long qx::QxSqlQuery::getSqlResultColumnCount() const;
* QVariant qx::QxSqlQuery::getSqlResultAt(long row, long column) const;
* QVariant qx::QxSqlQuery::getSqlResultAt(long row, const QString & column) const;
* QVector qx::QxSqlQuery::getSqlResultAllColumns() const;
* void qx::QxSqlQuery::dumpSqlResult();

Voici un exemple d'utilisation avec la fonction qx::dao::call_query() :

qx_query query("CALL MyStoredProc(:param1, :param2)");
query.bind(":param1", "myValue1");
query.bind(":param2", 5024, QSql::InOut);
QSqlError daoError = qx::dao::call_query(query);
QVariant vNewValue = query.boundValue(":param2");
query.dumpSqlResult();



Comment utiliser la classe qx::QxDaoAsync pour appeler des requêtes de manière asynchrone (multi-thread) ?

Il peut être parfois intéressant d'exécuter certaines requêtes à la base de données de manière asynchrone (multi-thread), par exemple pour éviter de bloquer une IHM si une requête est trop longue à s'exécuter.
Pour simplifier les requêtes asynchrones, la bibliothèque QxOrm fournit la classe qx::QxDaoAsync.
Cette classe exécute une requête dans un thread dédié et renvoie un SIGNAL queryFinished() lorsque la requête est terminée.
Pour utiliser la classe qx::QxDaoAsync, il suffit de :
  • vérifier que la requête fait appel à une classe qui implémente l'interface qx::IxPersistable ;
  • créer une instance de type qx::QxDaoAsync (par exemple, une propriété membre d'une classe dérivant du type QWidget) ;
  • connecter un SLOT au SIGNAL qx::QxDaoAsync::queryFinished() (par exemple, un SLOT défini dans une classe dérivant du type QWidget) ;
  • exécuter une requête à la base de données en utilisant l'une des méthodes commençant par async : qx::QxDaoAsync::asyncXXXX().
Voici un exemple d'utilisation avec une classe nommée MyWidget :

class MyWidget : public QWidget
{ Q_OBJECT

   //...
   qx::QxDaoAsync m_daoAsync;
   //...
Q_SLOTS:
   void onQueryFinished(const QSqlError & daoError, qx::dao::detail::QxDaoAsyncParams_ptr pDaoParams);
   //...

};

Et voici l'implémentation de la classe MyWidget :

MyWidget::MyWidget() : QObject()
{
   //...
   QObject::connect((& m_daoAsync), SIGNAL(queryFinished(const QSqlError &, qx::dao::detail::QxDaoAsyncParams_ptr)), 
                    this, SLOT(onQueryFinished(const QSqlError &, qx::dao::detail::QxDaoAsyncParams_ptr)));
   //...
}

void MyWidget::onQueryFinished(const QSqlError & daoError, qx::dao::detail::QxDaoAsyncParams_ptr pDaoParams)
{
   if (! pDaoParams) { return; }
   qx::QxSqlQuery query = pDaoParams->query;
   if (! daoError.isValid()) { ; }
   // If the async query is associated to a simple object, just use 'pDaoParams->pInstance' method
   qx::IxPersistable_ptr ptr = pDaoParams->pInstance;
   // If the async query is associated to a list of objects, just use 'pDaoParams->pListOfInstances' method
   qx::IxPersistableCollection_ptr lst = pDaoParams->pListOfInstances;
   //...
}



Comment utiliser le module QxModelView pour travailler avec le moteur model/view de Qt (Qt widgets et vues QML) ?

Le module QxModelView permet d'utiliser le moteur model/view de Qt avec toutes les classes enregistrées dans le contexte QxOrm :
  • Qt widgets : utilisation de QTableView ou QListView par exemple pour afficher/modifier le contenu d'une table de base de données ;
  • QML : toute propriété enregistrée dans le contexte QxOrm est accessible en QML : le module QxModelView permet ainsi de faciliter l'intéraction entre QML et les bases de données.
L'interface qx::IxModel propose une base commune pour tous les modèles liés aux classes persistantes déclarées dans le contexte QxOrm. Les méthodes de cette classe préfixées par 'qx' appellent les fonctions du namespace 'qx::dao' et communiquent donc directement avec la base de données.

Le projet de test qxBlogModelView présent dans le dossier ./test/ du package QxOrm montre comment créer rapidement un modèle et l'associer au moteur model/view de Qt (d'abord dans un widget Qt, puis dans une vue QML).

1- Exemple de création d'un modèle pour afficher/modifier les données de la table 'author' (voir le tutoriel qxBlog pour la définition de la classe 'author') dans un QTableView :

// Create a model and fetch all data from database
qx::IxModel * pModel = new qx::QxModel<author>();
pModel->qxFetchAll();

// Associate the model to a QTableView and display it
QTableView tableView;
tableView.setModel(pModel);
tableView.show();

Ce qui donne le résultat suivant à l'exécution :

qx_model_view_01


2- Voici un autre exemple en QML (en Qt5, le module QxModelView étant également compatible avec Qt4) :

// Create a model and fetch all data from database
qx::IxModel * pModel = new qx::QxModel<author>();
pModel->qxFetchAll();

// Associate the model to a QML view and display it
QQuickView qmlView;
qmlView.rootContext()->setContextProperty("myModel", pModel);
qmlView.setSource(QUrl("qrc:/documents/main.qml"));
qmlView.show();

Et voici le contenu du fichier 'main.qml' :

import QtQuick 2.1
import QtQuick.Controls 1.0

Item {
   width: 400
   height: 300
   Row {
      height: 20
      spacing: 20
      Button {
         text: "Clear"
         onClicked: myModel.clear()
      }
      Button {
         text: "Fetch All"
         onClicked: myModel.qxFetchAll_()
      }
      Button {
         text: "Save"
         onClicked: myModel.qxSave_()
      }
   }
   ListView {
      y: 30
      height: 270
      model: myModel
      delegate: Row {
         height: 20
         spacing: 10
         Text { text: "id: " + author_id }
         TextField {
            text: name
            onTextChanged: name = text
         }
      }
   }
}

Ce qui donne le résultat suivant à l'exécution :

qx_model_view_02

Comme on peut le constater dans le fichier 'main.qml', les propriétés 'author_id' et 'name' du modèle 'author' (variable myModel) sont accessibles automatiquement en lecture/écriture (car elles ont été enregistrées dans le contexte QxOrm).
De plus, l'interface qx::IxModel propose une liste de méthodes accessibles en QML (utilisation de Q_INVOKABLE) pour communiquer directement avec la base de données : ainsi, le bouton 'Save' de l'écran ci-dessus enregistre le modèle en base de données depuis QML.

Remarque : un plugin de QxEntityEditor permet de générer automatiquement le code des modèles pour la gestion des relations. Il est ainsi possible de travailler avec des modèles imbriqués.




QxOrm © 2020 Lionel Marty - contact@qxorm.com