QxOrm Windows Linux Macintosh C++

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

QxOrm >> Manuel d'utilisation de la bibliothèque QxOrm
Version courante :  QxOrm 1.4.9 - documentation en ligne de la bibliothèque QxOrm - GitHub
QxEntityEditor 1.2.7
Version française du site Web site english version
Sélection du manuel : Manuel QxOrm Manuel QxEntityEditor


Manuel d'utilisation de la bibliothèque QxOrm - Table des matières

  1. Introduction
    1. Bibliothèque QxOrm
    2. Aperçu rapide de l'application QxEntityEditor
    3. Convention d'écriture C++ utilisée par la bibliothèque QxOrm
  2. Installation
    1. Dépendance à Qt
    2. Dépendance à boost (optionnel)
    3. Fichier de configuration QxOrm.pri (ou QxOrm.cmake)
    4. Compiler la bibliothèque QxOrm (avec qmake ou CMake)
    5. Pilotes SQL fournis par Qt (drivers)
  3. Persistance - Object Relational Mapping (ORM)
    1. Définir une classe dans le contexte QxOrm (mapping)
      1. Clé primaire autre que le type par défaut "long"
      2. Clé primaire sur plusieurs colonnes (composite key)
      3. Données membres public/protected/private
      4. Espace de nom (namespace)
      5. Types C++ supportés par QxOrm
      6. Définir une donnée membre transient
    2. Connexion à la base de données
    3. Sauvegarder une instance C++ en base de données (insert/update)
    4. Supprimer une instance C++ de la base de données (delete)
      1. Suppression logique (soft delete)
    5. Récupérer une instance C++ de la base de données (fetch)
    6. Requêtes SQL
      1. Utilisation de la classe qx::QxSqlQuery (ou son alias qx_query)
      2. Appel de procédure stockée ou requête SQL personnalisée
    7. Transactions (commit, rollback, session)
    8. Moteur de relations
      1. one-to-many (1-n)
      2. many-to-one (n-1)
      3. many-to-many (n-n)
      4. one-to-one (1-1)
      5. Requête SQL avec relations
      6. Sélectionner les colonnes des relations à récupérer et définition des alias SQL
      7. Ajout SQL dans les clauses LEFT OUTER JOIN / INNER JOIN
    9. Collections supportées par QxOrm
      1. Collections de Qt
      2. Collections de boost
      3. Collections fournies par l'espace de nom standard std
      4. qx::QxCollection
    10. Pointeurs intelligents supportés par QxOrm (smart-pointers)
      1. Pointeurs intelligents de Qt
      2. Pointeurs intelligents de boost
      3. Pointeurs intelligents fournis par l'espace de nom standard std
      4. qx::dao::ptr
    11. Déclencheurs (triggers)
    12. Validation d'une instance C++ (validators)
    13. Gérer la valeur NULL de la base de données
      1. boost::optional
      2. QVariant
    14. Héritage et polymorphisme
    15. Interface qx::IxPersistable (classe abstraite)
    16. Utiliser le pattern C++ PIMPL (Private Implementation idiom ou d-pointer)
    17. Persister des types personnalisés
    18. Générer le schéma DDL SQL de la base de données
    19. Associer un type SQL à une classe C++
    20. Effectuer des requêtes asynchrones à la base de données
    21. Gestion du cache pour sauvegarder des instances C++ (module QxCache)
    22. Travailler avec plusieurs bases de données
    23. Déclarer une classe abstraite dans le contexte QxOrm
    24. Déclarer automatiquement les méta-propriétés de Qt (macro Q_PROPERTY)
  4. Sérialisation
    1. N° version pour assurer une compatibilité ascendante
    2. Moteur QDataStream de Qt
    3. Moteur JSON de Qt
    4. Moteur XML de boost::serialization
    5. Moteur binaire de boost::serialization
    6. Autres types de sérialisation proposés par boost
    7. Cloner une instance C++
    8. Afficher le détail d'une instance C++ (dump au format XML ou JSON)
  5. Introspection - Réflexion
    1. Obtenir dynamiquement la valeur d'une donnée membre
    2. Valoriser dynamiquement une donnée membre
    3. Appeler dynamiquement une fonction
    4. Créer une instance C++ dynamiquement
    5. Parcourir la liste des classes/propriétés enregistrées dans le contexte QxOrm
  6. Services : transférer la couche de données persistante sur le réseau (module QxService)
    1. Paramètres d'entrée/sortie d'un service (requête/réponse)
    2. Définir les fonctions publiées par un service
    3. Liste des options disponibles côté serveur
    4. Paramétrage de la connexion côté client
    5. Gestion de l'authentification dans un service
    6. Requêtes client/serveur asynchrones
  7. Moteur modèle/vue (module QxModelView)
    1. Définir un modèle "simple" (sans relation)
    2. Modèles avec relations (notion de modèles imbriqués)
    3. Intéraction avec les vues QML
    4. Intéraction avec les vues QtWidget
    5. Connexion d'un modèle au module QxService
  8. QxOrm et MongoDB (C++ ODM Object Document Mapper)
    1. Pré-requis : driver libmongoc et libbson
    2. Paramétrage du fichier QxOrm.pri (ou QxOrm.cmake)
    3. Connexion à la base de données MongoDB
    4. Définition d'une classe persistante MongoDB (Collection) dans le contexte QxOrm (mapping)
      1. Gestion des clés primaires ObjectId
    5. Insérer une instance C++ (Document) dans la base de données MongoDB (INSERT)
      1. Insérer une liste d'instances C++ (plusieurs Documents) dans la base de données MongoDB (INSERT)
    6. Mettre à jour une instance C++ (Document) dans la base de données MongoDB (UPDATE)
      1. Mettre à jour une liste d'instances C++ (plusieurs Documents) dans la base de données MongoDB (UPDATE)
    7. Supprimer une instance C++ (Document) de la base de données MongoDB (DELETE)
      1. Supprimer une liste d'instances C++ (plusieurs Documents) de la base de données MongoDB (DELETE)
    8. Récupérer une instance C++ (Document) de la base de données MongoDB (FETCH)
      1. Récupérer une liste d'instances C++ (plusieurs Documents) de la base de données MongoDB (FETCH)
    9. Requêtes JSON
      1. Utilisation de la classe qx::QxSqlQuery (ou son alias qx_query)
      2. Utiliser le moteur d'aggregation MongoDB
      3. Ajouter des propriétés à la requête de type : 'sort', 'limit', 'skip', etc...
      4. Exécuter une requête personnalisée
    10. Moteur de relations (nécessite une version MongoDB 3.6 ou +)
      1. Relations : Embedded vs Referenced
    11. Création automatique des index
  9. Serveur web HTTP/HTTPS (module QxHttpServer)
    1. Hello World !
    2. Paramétrage du serveur web HTTP
      1. Connexions sécurisées SSL/TLS
    3. Routage des URL (définir les endpoints / dispatcher)
      1. Routage dynamique des URL
    4. Récupérer les paramètres de la requête HTTP
    5. Générer la réponse HTTP
    6. Sessions (stockage par client côté serveur)
    7. Cookies
    8. Gestion des fichiers statiques
    9. Encodage de transfert en bloc (chunked responses)
    10. Requêtes par les API JSON (module QxRestApi)
    11. WebSocket
    12. Performance (testé avec Apache Benchmark)
      1. Améliorer les performances avec epoll dispatcher sous Linux
  10. API REST JSON (module QxRestApi)
    1. Principe de fonctionnement
      1. Cas d'utilisation
    2. Projet de test qxBlogRestApi (QML et serveur web HTTP)
    3. Récupération de données (fetch/count/exist)
      1. fetch_all
      2. fetch_by_id
      3. fetch_by_query
      4. count
      5. exist
    4. Ajout de données (insert)
    5. Mise à jour de données (update)
    6. Sauvegarde de données (save)
    7. Suppression de données (delete)
      1. delete_all / destroy_all
      2. delete_by_query / destroy_by_query
      3. delete_by_id / destroy_by_id
    8. Validation de données (validate)
    9. Appel RAW SQL ou procédure stockée
    10. Appel fonctions natives C++
    11. Meta-data (structure des classes C++ enregistrées dans le contexte QxOrm)
    12. Envoyer une liste de requêtes JSON
qt_ambassador
QxOrm library has been accepted into the Qt Ambassador Program


Introduction

L'objectif de ce manuel utilisateur est de présenter de manière structurée l'ensemble des fonctionnalités proposées par la bibliothèque QxOrm. Ce manuel est destiné aux développeurs et architectes logiciel qui souhaitent gérer une couche de données persistante en C++/Qt. Des compétences techniques en C++ et base de données sont requises pour la bonne compréhension de ce document.

Remarque : la plupart des fonctionnalités présentées dans ce manuel peuvent être définies rapidement et facilement avec l'application QxEntityEditor (l'éditeur graphique de la bibliothèque QxOrm). Une documentation dédiée à l'application QxEntityEditor est également disponible.

Autre remarque : ce manuel est basé en grande partie sur l'ancienne FAQ du site QxOrm, toujours accessible en cliquant ici.

Bibliothè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 : support des bases de données SQLite, MySQL, PostgreSQL, Oracle, MS SQL Server, MongoDB (gestion des relations 1-1, 1-n, n-1 et n-n) ;
  • Sérialisation des données (flux JSON, binaire et XML) ;
  • Réflexion (ou introspection) pour accéder dynamiquement aux classes, attributs et invoquer des méthodes ;
  • Serveur web HTTP : serveur web compatible HTTP 1.1 autonome, performant, multi-plateforme et simple d'utilisation ;
  • API JSON : interopérabilité avec d'autres technologies que C++/Qt (web services REST, applications QML, langages de script).
QxOrm est dépendant des excellentes bibliothèques Qt (compatible à partir de la version 4.5.0) et boost (compatible à partir de la version 1.38, par défaut seuls les fichiers d'en-tête *.hpp sont nécessaires).
La bibliothèque QxOrm a été retenue pour faire partie du programme Qt Ambassador.

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.

Aperçu rapide de l'application 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.
Un manuel utilisateur dédié à l'application QxEntityEditor est disponible.


Convention d'écriture C++ utilisée par la bibliothèque QxOrm

La bibliothèque QxOrm utilise les conventions d'écriture de code C++ suivantes :
  • toutes les classes, fonctions, variables, etc... sont définies dans l'espace de nom (namespace) qx ;
  • les macro de QxOrm sont écrites sous la forme QX_... ;
  • les classes abstraites (ou interfaces) ont le préfixe Ix (par exemple : IxFactory est une interface pour la création d'instances) ;
  • les autres classes ont le préfixe Qx (par exemple : QxDataMember) ;
  • les collections d'objets ont pour suffixe X (par exemple : QxDataMemberX est une collection de QxDataMember) ;
  • les fonctions pour communiquer avec les bases de données se trouvent sous le namespace qx::dao (par exemple : qx::dao::fetch_by_id()) ;
  • les fonctions pour la serialization des données se trouvent sous le namespace qx::serialization (par exemple : qx::serialization::xml::to_file()) ;
  • le moteur de reflection (ou introspection) est accessible depuis la classe qx::QxClassX (par exemple qx::QxClassX::invoke() pour invoquer une méthode de classe) ;
  • les classes de traits se trouvent sous le namespace qx::trait (par exemple : qx::trait::is_smart_ptr<T>).

Installation

La bibliothèque QxOrm est multi-plateforme et peut être installée sur tous types d'environnement : Windows, Linux (Unix), Mac OS X, Android, iOS, Windows Phone, etc...
Un tutoriel complet (avec captures d'écran) pour installer un environnement de développement avec QxOrm sous Windows est disponible en cliquant ici.

L'objectif de ce chapitre est de présenter rapidement les différentes étapes à suivre pour installer QxOrm sur tous types d'environnement :

Dépendance à Qt

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/

Remarque : par défaut, la bibliothèque QxOrm dépend uniquement des modules QtCore et QtSql. Il est possible d'activer des fonctionnalités supplémentaires grâce au fichier de configuration QxOrm.pri (ou QxOrm.cmake) : ces nouvelles fonctionnalités peuvent alors ajouter des dépendances à QxOrm.

Dépendance à boost (optionnel)

Par défaut, la bibliothèque QxOrm dépend uniquement de Qt (QtCore et QtSql). L'installation de boost est optionnelle et non requise avec la configuration par défaut.
Remarque : QxOrm propose 2 niveaux de dépendance à boost en option :
  • une dépendance uniquement aux fichiers d'en-têtes de boost (*.hpp) : option de compilation _QX_ENABLE_BOOST ;
  • une dépendance au module boost serialization : option de compilation _QX_ENABLE_BOOST_SERIALIZATION.

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, type_traits, multi_index_container, unordered_container, any, tuple, foreach, function. Toutes ces fonctionnalités sont header only, la dépendance au module serialization est optionnelle.
Il est conseillé d'installer et d'utiliser la dernière version de boost disponible à l'adresse suivante : http://www.boost.org/

Remarque importante : avec l'option de compilation _QX_ENABLE_BOOST, la bibliothèque QxOrm dépend uniquement des fichiers d'en-tête *.hpp de boost (utilisation des bibliothèques header only uniquement). L'installation de boost est donc très simple puisqu'il suffit de dézipper le package boost (pour disposer des fichiers d'en-tête *.hpp).

Fichier de configuration QxOrm.pri (ou QxOrm.cmake)

Le fichier de configuration QxOrm.pri (ou QxOrm.cmake) est divisé en plusieurs sections (chacune étant commentée) et regroupe les différents paramétrages et options de compilation disponibles. Il est fortement recommandé de lire attentivement le fichier de configuration QxOrm.pri avant de compiler la bibliothèque QxOrm. Il est possible de conserver le paramétrage par défaut, seule la variable QX_BOOST_INCLUDE_PATH est nécessaire si votre projet utilise le framework boost : cette variable indique où trouver les fichiers d'en-tête *.hpp de la bibliothèque boost :

   isEmpty(QX_BOOST_INCLUDE_PATH) { QX_BOOST_INCLUDE_PATH = $$quote(D:/Dvlp/_Libs/Boost/1_57/include) }   

Si vous ne souhaitez pas modifier le fichier de configuration QxOrm.pri, il est possible de définir une variable d'environnement nommée BOOST_INCLUDE : cette variable d'environnement sera alors utilisée automatiquement pour valoriser QX_BOOST_INCLUDE_PATH (lire le fichier QxOrm.pri pour plus d'informations).

Voici une liste non exhaustive des différentes options de compilation disponibles (lire le fichier de configuration QxOrm.pri pour plus de détails), aucune n'étant activée par défaut :
  • _QX_ENABLE_BOOST : ajoute une dépendance aux fichiers d'en-têtes de boost (*.hpp), support des classes boost::shared_ptr, boost::optional, boost::container, etc... ;
  • _QX_ENABLE_BOOST_SERIALIZATION : active les fonctionnalités de sérialisation avec le module boost::serialization. Cette option nécessite la compilation du binaire boost::serialization et ajoute donc une dépendance à QxOrm ;

  • _QX_ENABLE_QT_GUI : support de la sérialisation des types du module QtGui : QBrush, QColor, QFont, QImage, QMatrix, QPicture, QPixmap, QRegion. Cette option ajoute une dépendance à QxOrm (QtGui) ;
  • _QX_ENABLE_QT_NETWORK : active le module QxService pour transférer la couche de données persistante sur le réseau (application client/serveur). Cette option ajoute une dépendance à QxOrm (QtNetwork) ;
  • _QX_NO_PRECOMPILED_HEADER : désactive l'utilisation d'un en-tête précompilé (permet de réduire les temps de compilation d'un projet) : cette option est nécessaire pour contourner un bug des versions récentes de MinGW, pour tous les autres compilateurs il est recommandé de travailler avec un precompiled header ;
  • _QX_NO_RTTI : permet de compiler QxOrm et les projets dépendants sans les informations de type C++ RTTI ;
  • _QX_STATIC_BUILD : permet de compiler la bibliothèque QxOrm en mode statique ;
  • _QX_UNITY_BUILD : réduit les temps de compilation de la bibliothèque QxOrm en utilisant le concept unity build : un seul fichier source all.cpp à compiler. Il est recommandé d'activer cette option avec CMake (car ne supporte pas nativement les en-têtes précompilés) ;
  • _QX_ENABLE_MONGODB : support de la base de données MongoDB, la bibliothèque QxOrm devient ainsi un ODM (Object Document Mapper).

Remarque : le fichier de configuration QxOrm.pri (ou QxOrm.cmake) devra être inclus dans tous les projets dépendants de la bibliothèque QxOrm en ajoutant la ligne suivante dans le fichier *.pro du projet :

   include(my_path_to_QxOrm_library/QxOrm.pri)   

Autre remarque : à la place de qmake, il est possible d'utiliser l'outil de compilation CMake pour configurer et construire la bibliothèque QxOrm. CMake propose un outil graphique afin de visualiser et paramétrer les différentes options disponibles :

QxOrm and CMake

Compiler la bibliothèque QxOrm (avec qmake ou CMake)

QxOrm utilise le processus qmake de la bibliothèque Qt pour générer les makefile et compiler le projet (il est également possible d'utiliser l'outil de compilation CMake, un fichier CMakeLists.txt étant fourni avec la bibliothèque QxOrm).
qmake est multi-plateforme et fonctionne parfaitement sous Windows, Linux (Unix) et Mac OS X.
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 Microsoft Visual C++.
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 OS X (merci à Dominique Billet pour l'écriture des scripts).

Pilotes SQL fournis par Qt (drivers)

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.
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.
Remarque : pour se connecter à une base de données Microsoft SQL Server, il est nécessaire d'utiliser le pilote ODBC (plugin QODBC).

Autre remarque : la bibliothèque QxOrm supporte également la base de données MongoDB (C++ ODM Object Document Mapper).

Persistance - Object Relational Mapping (ORM)

La bibliothèque QxOrm fournit un moteur de persistance des données basé sur le module QtSql de Qt. Ce moteur de persistance utilise la technique de programmation : Object Relational Mapping (ORM).

Définition du site Wikipedia : un mapping objet-relationnel (en anglais object-relational mapping ou ORM) est une technique de programmation informatique qui crée l'illusion d'une base de données orientée objet à partir d'une base de données relationnelle en définissant des correspondances entre cette base de données et les objets du langage utilisé. On pourrait le désigner par « correspondance entre monde objet et monde relationnel ». Le mapping objet-relationnel consiste à associer une ou plusieurs classes avec une table, et chaque attribut de la classe avec un champ de la table. Les frameworks de mapping objet-relationnel permettent d'éliminer la duplication de code dans les opérations CRUD.

Pour effectuer cette correspondance entre le monde objet et le monde relationnel, ainsi pour que proposer l'ensemble de ses fonctionnalités, la bibliothèque QxOrm impose l'enregistrement de classes C++ dans le contexte QxOrm. Nous allons donc débuter ce chapitre de la façon suivante : comment enregistrer une classe C++ dans le contexte QxOrm ?

Remarque : la bibliothèque QxOrm supporte également la base de données MongoDB (C++ ODM Object Document Mapper).

Définir une classe dans le contexte QxOrm (mapping)

Toutes les classes C++ peuvent être enregistrées dans le contexte QxOrm : il n'y a pas besoin de dériver d'un super objet, et vous pouvez écrire vos méthodes de classes et accesseurs sans aucune contrainte. Enregistrer une classe C++ dans le contexte QxOrm signifie :
  • dans le fichier en-tête *.h contenant la définition de la classe : utilisation de la macro QX_REGISTER_HPP(class_name, base_class, class_version) ;
  • dans le fichier source *.cpp contenant l'implémentation de la classe : utilisation de la macro QX_REGISTER_CPP(class_name) ;
  • dans le fichier source *.cpp contenant l'implémentation de la classe : spécialisation de la fonction template : void qx::register_class<T>(qx::QxClass<T> & t).
Par exemple, voici comment déclarer une classe person avec 4 propriétés enregistrées dans le contexte QxOrm : id, firstName, lastName, birthDate :

* Fichier person.h :
#ifndef _PERSON_H_
#define _PERSON_H_

class person
{
public:
   long id;
   QString firstName;
   QString lastName;
   QDateTime birthDate;

   person() : id(0) { ; }
   virtual ~person() { ; }
};

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

/* This macro is necessary to register 'person' class in QxOrm context */
/* param 1 : the current class to register => 'person' */
/* param 2 : the base class, if no base class, use the qx trait => 'qx::trait::no_base_class_defined' */
/* param 3 : the class version used by serialization engine to provide 'ascendant compatibility' */

#endif // _PERSON_H_

* Fichier person.cpp :
#include "precompiled.h"   // Precompiled-header with '#include <QxOrm.h>' and '#include "export.h"'
#include "person.h"          // Class definition 'person'
#include <QxOrm_Impl.h>     // Automatic memory leak detection and boost serialization export macro

QX_REGISTER_CPP_MY_TEST_EXE(person)   // This macro is necessary to register 'person' class in QxOrm context

namespace qx {
template <> void register_class(QxClass<person> & t)
{
  t.setName("t_person");               // 'person' C++ class is mapped to 't_person' database table

  t.id(& person::id, "id");               // Register 'person::id' <=> primary key in your database
  t.data(& person::firstName, "first_name");      // Register 'person::firstName' property mapped to 'first_name' database column name
  t.data(& person::lastName, "last_name");  // Register 'person::lastName' property mapped to 'last_name' database column name
  t.data(& person::birthDate, "birth_date");  // Register 'person::birthDate' property mapped to 'birth_date' database column name
}}


Remarque : les méthodes qx::QxClass<T>::id() et qx::QxClass<T>::data() retournent une instance de type : qx::IxDataMember (classe de base pour l'enregistrement des données membre). Grâce à cette instance, il est possible de personnaliser le comportement par défaut proposé par la classe qx::IxDataMember, comme par exemple dans le chapitre : Définir une donnée membre transient.

Autre remarque : il est également possible d'enregistrer des méthodes de classe dans le contexte QxOrm (gestion des méthodes static et non static) avec les méthodes qx::QxClass<T>::fct_0(), qx::QxClass<T>::fct_1(), etc... Cette fonctionnalité fait partie du moteur d'introspection de la bibliothèque QxOrm, plus de détails dans le chapitre : Appeler dynamiquement une fonction.

Clé primaire autre que le type par défaut "long"

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<T> 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_


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 (ou std::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());
}


Données membres public/protected/private

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


Espace de nom (namespace)

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 "_".

Types C++ supportés par QxOrm

La bibliothèque QxOrm supporte la plupart des types primitifs du standard C++ et du framework Qt (numériques, booléens, chaines de caractères, date/heure, collections, pointeurs et pointeurs intelligents, etc...). Voici un exemple présentant une liste (non exhaustive) de types C++ supportés ainsi que l'association par défaut du type de base de données (format SQLite) :

"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"

Remarque : il est également possible de persister un type non géré par défaut par la bibliothèque QxOrm. Rendez-vous au chapitre Persister des types personnalisés pour plus de détails sur cette fonctionnalité.

Autre remarque : concernant l'association d'un type C++ avec le type de base de données associé, rendez-vous au chapitre Associer un type SQL à une classe C++ pour plus de détails.

Définir une donnée membre transient

Une donnée membre transient n'est pas associée à une colonne d'une table de la base de données. Le module QxDao ignore donc cette propriété pour toutes les requêtes à la base de données.

A quoi sert l'enregistrement d'une donnée membre transient dans le contexte QxOrm ?
Enregistrer une donnée membre transient dans le contexte QxOrm permet de disposer des autres fonctionnalités de la bibliothèque QxOrm sur cette propriété, comme par exemple : sérialisation, introspection, etc...

La méthode qx::QxClass<T>::data() dispose d'un paramètre optionnel nommé : bool bDao (par défaut, valeur à true). Par exemple, ajoutons une propriété transient nommée age à la classe person (cette propriété n'a pas besoin d'être stockée en base de données puisque nous disposons déjà de la propriété birthDate) :

namespace qx {
template <> void register_class(QxClass<person> & t)
{
  t.id(& person::id, "id");
  t.data(& person::firstName, "first_name";);
  t.data(& person::lastName, "last_name");
  t.data(& person::birthDate, "birth_date");
  t.data(& person::age, "age", 0, true, false);
}}

Voici une autre façon de définir une propriété transient en récupérant l'instance de type qx::IxDataMember :

namespace qx {
template <> void register_class(QxClass<person> & t)
{
  t.id(& person::id, "id");
  t.data(& person::firstName, "first_name";);
  t.data(& person::lastName, "last_name");
  t.data(& person::birthDate, "birth_date");

  IxDataMember * pDataMember = t.data(& person::age, "age");
  pDataMember->setDao(false);
}}


Connexion à la base de données

La connexion à la base de données peut être paramétrée avec la classe singleton : qx::QxSqlDatabase.
Voici un exemple de paramétrage à une base de données SQLite nommée test_qxorm.db :

   // Init parameters to connect to database
   qx::QxSqlDatabase::getSingleton()->setDriverName("QSQLITE");
   qx::QxSqlDatabase::getSingleton()->setDatabaseName("./test_qxorm.db");
   qx::QxSqlDatabase::getSingleton()->setHostName("localhost");
   qx::QxSqlDatabase::getSingleton()->setUserName("root");
   qx::QxSqlDatabase::getSingleton()->setPassword("");

Une fois les paramètres de connexion renseignés dans la classe singleton qx::QxSqlDatabase, toutes les opérations avec la base de données effectuées par la bibliothèque QxOrm utiliserons ces paramètres. Pour plus d'informations sur les paramètres de connexion à renseigner, il est recommandé de lire la documentation de la classe QSqlDatabase du framework Qt.

Remarque : la classe qx::QxSqlDatabase gère automatiquement les appels à la base de données dans différents threads (multi-threading).

Autre remarque : il est possible de gérer son propre pool de connexions à la base de données, et de travailler également avec plusieurs bases de données distinctes : rendez-vous dans le chapitre Travailler avec plusieurs bases de données pour plus d'informations sur cette fonctionnalité.

Autre remarque : suivant le pilote SQL renseigné dans les paramètres de connexion, la bibliothèque QxOrm associe automatiquement un générateur SQL. Ce générateur SQL permet de gérer les spécificités propres à chaque type de base de données. Tous les générateurs SQL héritent de la classe de base : qx::dao::detail::IxSqlGenerator :
   qx::dao::detail::IxSqlGenerator_ptr pSqlGenerator;
   pSqlGenerator.reset(new qx::dao::detail::QxSqlGenerator_MSSQLServer());   
   qx::QxSqlDatabase::getSingleton()->setSqlGenerator(pSqlGenerator);   


Sauvegarder une instance C++ en base de données (insert/update)

Toutes les fonctions liées à la base de données sont disponibles dans l'espace de nom qx::dao.

Pour sauvegarder une instance C++ (ou une liste d'instances C++) en base de données, la bibliothèque QxOrm fournit les fonctions suivantes :
  • qx::dao::insert : insère une instance (ou une liste d'instances) en base de données ;
  • qx::dao::insert_with_relation : insère une instance (ou une liste d'instances) + ses relations en base de données ;
  • qx::dao::insert_with_all_relation : insère une instance (ou une liste d'instances) + toutes ses relations en base de données ;

  • qx::dao::update : met à jour une instance (ou une liste d'instances) en base de données ;
  • qx::dao::update_with_relation : met à jour une instance (ou une liste d'instances) + ses relations en base de données ;
  • qx::dao::update_with_all_relation : met à jour une instance (ou une liste d'instances) + toutes ses relations en base de données ;
  • qx::dao::update_by_query : met à jour une instance (ou une liste d'instances) en base de données en filtrant avec une requête SQL ;
  • qx::dao::update_by_query_with_relation : met à jour une instance (ou une liste d'instances) + ses relations en base de données en filtrant avec une requête SQL ;
  • qx::dao::update_by_query_with_all_relation : met à jour une instance (ou une liste d'instances) + toutes ses relations en base de données en filtrant avec une requête SQL ;
  • qx::dao::update_optimized : met à jour uniquement les champs modifiés d'une instance (ou d'une liste d'instances) en base de données en utilisant le pattern is dirty et les fonctionnalités de la classe qx::dao::ptr ;
  • qx::dao::update_optimized_by_query : met à jour uniquement les champs modifiés d'une instance (ou d'une liste d'instances) en base de données en utilisant le pattern is dirty et les fonctionnalités de la classe qx::dao::ptr et en filtrant avec une requête SQL ;

  • qx::dao::save : insère (si l'élément n'existe pas en base de données) ou met à jour (si l'élément existe déjà en base de données) ;
  • qx::dao::save_with_relation : insère (si l'élément n'existe pas en base de données) ou met à jour (si l'élément existe déjà en base de données) + ses relations ;
  • qx::dao::save_with_all_relation : insère (si l'élément n'existe pas en base de données) ou met à jour (si l'élément existe déjà en base de données) + toutes ses relations ;
  • qx::dao::save_with_relation_recursive : insère (si l'élément n'existe pas en base de données) ou met à jour (si l'élément existe déjà en base de données) + toutes les relations sur tous les niveaux : utile pour sauvegarder en 1 commande une structure en arbre par exemple.

Par exemple :
   // Create 3 drugs instances
   // It is possible to use 'boost' and 'Qt' smart pointer : 'boost::shared_ptr', 'QSharedPointer', etc...
   typedef boost::shared_ptr<drug> drug_ptr;
   drug_ptr d1; d1.reset(new drug()); d1->name = "name1"; d1->description = "desc1";
   drug_ptr d2; d2.reset(new drug()); d2->name = "name2"; d2->description = "desc2";
   drug_ptr d3; d3.reset(new drug()); d3->name = "name3"; d3->description = "desc3";

   // Insert some drugs into a container
   // It is possible to use many containers from 'std', 'boost', 'Qt' and 'qx::QxCollection<Key, Value>'
   typedef std::vector<drug_ptr> type_lst_drug;
   type_lst_drug lst_drug;
   lst_drug.push_back(d1);
   lst_drug.push_back(d2);
   lst_drug.push_back(d3);

   // Insert drugs from container to database
   // 'id' property of 'd1', 'd2' and 'd3' are auto-updated
   QSqlError daoError = qx::dao::insert(lst_drug);

   // Modify and update the second drug into database
   d2->name = "name2 modified";
   d2->description = "desc2 modified";
   daoError = qx::dao::update(d2);


Remarque : toutes les fonctions de l'espace de nom qx::dao sont flexibles au niveau des paramètres, elles peuvent accepter : une instance, une liste d'instances, un pointeur, un pointeur intelligent, une liste de pointeurs, une liste de pointeurs intelligents, etc... Par exemple :
  • my_entity t;     /* ... */     qx::dao::insert(t);
  • my_entity * t;     /* ... */     qx::dao::insert(t);
  • std::shared_ptr<my_entity> t;     /* ... */     qx::dao::insert(t);
  • QList<my_entity> lst;     /* ... */     qx::dao::insert(lst);
  • QList<std::shared_ptr<my_entity> > lst;     /* ... */     qx::dao::insert(lst);
Pour connaitre la liste des collections supportées, rendez-vous dans le chapitre : Collections supportées par QxOrm.
Pour connaitre la liste des pointeurs intelligents supportés, rendez-vous dans le chapitre : Pointeurs intelligents supportés par QxOrm (smart-pointers).


Supprimer une instance C++ de la base de données (delete)

Toutes les fonctions liées à la base de données sont disponibles dans l'espace de nom qx::dao.

Pour supprimer une instance C++ (ou une liste d'instances C++) en base de données, la bibliothèque QxOrm fournit les fonctions suivantes :
Par exemple :
   // Create a drug instance with id '18'
   drug d; d.setId(18);

   // Delete the drug with id '18' from database
   QSqlError daoError = qx::dao::delete_by_id(d);

   // Delete all drugs from database
   daoError = qx::dao::delete_all<drug>();


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.

Récupérer une instance C++ de la base de données (fetch)

Toutes les fonctions liées à la base de données sont disponibles dans l'espace de nom qx::dao.

Pour valoriser automatiquement les propriétés d'une instance C++ (ou d'une liste d'instances C++) en fonction des données d'une table (ou plusieurs tables si des relations sont définies) de la base de données, la bibliothèque QxOrm fournit les fonctions suivantes :
Par exemple :
   // Fetch drug with id '3' into a new variable
   drug_ptr d; d.reset(new drug());
   d->id = 3;
   QSqlError daoError = qx::dao::fetch_by_id(d);


Requêtes SQL

La bibliothèque QxOrm fournit plusieurs outils pour effectuer des requêtes à la base de données : Remarque : QxOrm étant basé sur le module QtSql de Qt, il est toujours possible de requêter la base de données en utilisant la classe QSqlQuery de Qt si les fonctionnalités proposées par QxOrm ne sont pas suffisantes.

Utilisation de la classe qx::QxSqlQuery (ou son alias qx_query)

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).

Appel de procédure stockée ou 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 son alias 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 ce chapitre du manuel utilisateur : Utilisation de la classe qx::QxSqlQuery (ou son alias qx_query).

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();


Transactions (commit, rollback, session)

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 connexion 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 connexion 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 connexion à 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 connexion (provenant d'un pool de connexions 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 connexion 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)


Moteur de relations

La bibliothèque QxOrm fournit un puissant moteur de relations permettant de définir facilement : Remarque : un tutoriel complet sur les relations basé sur le projet de test qxBlog (dont les sources sont présentes dans le package QxOrm) est disponible.

one-to-many (1-n)

Une relation one-to-many (1-n) est définie par la méthode : qx::QxClass<T>::relationOneToMany(). Cette méthode renvoie une instance de la classe qx::IxSqlRelation (classe de base pour toutes les relations) et nécessite 3 paramètres :
  • V U::* pData : référence vers la donnée membre de la classe ;
  • const QString & sKey : clé unique associée à la relation ;
  • const QString & sForeignKey : clé étrangère définie dans la classe/table liée.

Par exemple : prenons l'exemple d'un author (une personne) qui peut rédiger plusieurs blog : nous allons ainsi montrer comment définir une relation de type one-to-many.
Au niveau base de données, voici les deux tables qui correspondent :

qxBlog.table.author

Fichier author.h :
#ifndef _QX_BLOG_AUTHOR_H_
#define _QX_BLOG_AUTHOR_H_

class blog;

class QX_BLOG_DLL_EXPORT author
{
public:
// -- typedef
   typedef boost::shared_ptr<blog> blog_ptr;
   typedef std::vector<blog_ptr> list_blog;
// -- enum
   enum enum_sex { male, female, unknown };
// -- 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_

Fichier author.cpp :
#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_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());
}


many-to-one (n-1)

Une relation many-to-one (n-1) est définie par la méthode : qx::QxClass<T>::relationManyToOne(). Cette méthode renvoie une instance de la classe qx::IxSqlRelation (classe de base pour toutes les relations) et nécessite 2 paramètres :
  • V U::* pData : référence vers la donnée membre de la classe ;
  • const QString & sKey : clé unique associée à la relation (correspond à une colonne de la table dans la base de données).

Par exemple : un comment est associé à un blog et un blog peut contenir plusieurs comment : nous allons ainsi montrer comment définir une relation de type many-to-one.
Au niveau base de données, voici les deux tables qui correspondent :

qxBlog.table.comment

Fichier comment.h :
#ifndef _QX_BLOG_COMMENT_H_
#define _QX_BLOG_COMMENT_H_

class blog;

class QX_BLOG_DLL_EXPORT comment
{
public:
// -- typedef
   typedef boost::shared_ptr<blog> blog_ptr;
// -- propriétés
   long        m_id;
   QString     m_text;
   QDateTime   m_dt_create;
   blog_ptr    m_blog;
// -- constructeur, destructeur virtuel
   comment() : m_id(0) { ; }
   virtual ~comment() { ; }
};

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

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

#endif // _QX_BLOG_COMMENT_H_

Fichier comment.cpp :
#include "../include/precompiled.h"

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

#include <QxOrm_Impl.h>

QX_REGISTER_CPP_QX_BLOG(comment)

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

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

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


many-to-many (n-n)

Une relation many-to-many (n-n) est définie par la méthode : qx::QxClass<T>::relationManyToMany(). Cette méthode renvoie une instance de la classe qx::IxSqlRelation (classe de base pour toutes les relations) et nécessite 5 paramètres :
  • V U::* pData : référence vers la donnée membre de la classe ;
  • const QString & sKey : clé unique associée à la relation ;
  • const QString & sExtraTable : nom de la table supplémentaire permettant de stocker les id de chaque côté des relations ;
  • const QString & sForeignKeyOwner : clé étrangère définie dans la table supplémentaire pour représenter la classe/table courante ;
  • const QString & sForeignKeyDataType : clé étrangère définie dans la table supplémentaire pour représenter la classe/table associée à la relation.

Par exemple : une category référence plusieurs blog et un blog peut appartenir à plusieurs category : nous allons ainsi montrer comment définir une relation de type many-to-many. Ce type de relation implique une table supplémentaire dans la base de données pour stocker la liste des id de chaque côté des relations.
Au niveau base de données, voici les trois tables qui correspondent :

qxBlog.table.category

Fichier category.h :
#ifndef _QX_BLOG_CATEGORY_H_
#define _QX_BLOG_CATEGORY_H_

class blog;

class QX_BLOG_DLL_EXPORT category
{
public:
// -- typedef
   typedef boost::shared_ptr<blog> blog_ptr;
   typedef qx::QxCollection<long, blog_ptr> list_blog;
// -- propriétés
   long        m_id;
   QString     m_name;
   QString     m_desc;
   list_blog   m_blogX;
// -- constructeur, destructeur virtuel
   category() : m_id(0) { ; }
   virtual ~category() { ; }
};

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

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

#endif // _QX_BLOG_CATEGORY_H_

Fichier category.cpp :
#include "../include/precompiled.h"

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

#include <QxOrm_Impl.h>

QX_REGISTER_CPP_QX_BLOG(category)

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

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

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


one-to-one (1-1)

Une relation one-to-one (1-1) permet de représenter 2 entités distinctes qui partagent le même identifiant en base de données. Une relation one-to-one (1-1) est définie par la méthode : qx::QxClass<T>::relationOneToOne(). Cette méthode renvoie une instance de la classe qx::IxSqlRelation (classe de base pour toutes les relations) et nécessite 2 paramètres :
  • V U::* pData : référence vers la donnée membre de la classe ;
  • const QString & sKey : clé unique associée à la relation.

Par exemple : prenons l'exemple d'une table person et d'une autre table author : un author est également une person, les 2 tables pourraient partager le même identifiant en base de données. Au niveau base de données, voici les 2 tables qui correspondent (person_id == author_id) :

qxBlog.table.person


Requête SQL avec relations

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

Sélectionner les colonnes des relations à récupérer et définition des alias SQL

Il est parfois nécessaire de ne pas requêter toutes les colonnes d'une table par soucis d'optimisation : en effet, sélectionner les colonnes réellement utilisées par un traitement permet de limiter les flux réseau entre la base de données et l'application C++, ce qui améliore les performances.

Concernant les relations, la bibliothèque QxOrm fournit une syntaxe spécifique pour sélectionner les colonnes à récupérer, sous la forme : my_relation { col_1, col_2, etc... }. Si cette syntaxe n'est pas utilisée, par défaut, QxOrm récupère toutes les colonnes.

Par exemple : imaginons la requête suivante qui permet de récupérer :
  • uniquement la colonne blog_text de la table blog ;
  • uniquement les colonnes name et birthdate de la table author ;
  • uniquement la colonne comment_text de la table comment.
   // Fetch relations defining columns to fetch with syntax { col_1, col_2, etc... }
   list_blog lstBlogComplexRelation;
   QStringList relations = QStringList() << "{ blog_text }" << "author_id { name, birthdate }" << "list_comment { comment_text }";
   QSqlError daoError = qx::dao::fetch_all_with_relation(relations, lstBlogComplexRelation);

   qx::dump(lstBlogComplexRelation);
   qAssert(lstBlogComplexRelation.size() > 0);
   qAssert(lstBlogComplexRelation[0]->m_text != ""); // Fetched
   qAssert(lstBlogComplexRelation[0]->m_dt_creation.isNull()); // Not fetched
   qAssert(lstBlogComplexRelation[0]->m_author->m_sex == author::unknown); // Not fetched
   qAssert(lstBlogComplexRelation[0]->m_author->m_name != ""); // Fetched
   qAssert(lstBlogComplexRelation[0]->m_commentX.size() > 0);
   qAssert(lstBlogComplexRelation[0]->m_commentX[0]->m_dt_create.isNull()); // Not fetched
   qAssert(lstBlogComplexRelation[0]->m_commentX[0]->m_text != ""); // Fetched

Remarque : une autre syntaxe est disponible afin de renseigner les colonnes à ne pas récupérer : my_relation -{ col_1, col_2, etc... }.

Autre remarque : il est également possible de définir un alias par relation à utiliser dans la requête SQL. Ceci est utile pour l'écriture des conditions dans la clause WHERE. Un alias SQL peut être défini entre les caractères < >.

Exemple : voici un exemple de fetch avec relations en définissant des alias SQL par relation :

list_blog lstBlogComplexRelation3;
QStringList relations;
relations << "<blog_alias> { blog_text }";
relations << "author_id <author_alias> { name, birthdate }";
relations << "list_comment <list_comment_alias> { comment_text } -> blog_id <blog_alias_2> -> * <..._my_alias_suffix>";
QSqlError daoError = qx::dao::fetch_all_with_relation(relations, lstBlogComplexRelation3);
qx::dump(lstBlogComplexRelation3);


Ce qui génère la requête SQL suivante :
SELECT blog_alias.blog_id AS blog_alias_blog_id_0, blog_alias.blog_text AS blog_alias_blog_text_0, blog_alias.author_id AS blog_alias_author_id_0, author_alias.author_id AS author_alias_author_id_0, author_alias.name AS author_alias_name_0, author_alias.birthdate AS author_alias_birthdate_0, list_comment_alias.comment_id AS list_comment_alias_comment_id_0, list_comment_alias.blog_id AS list_comment_alias_blog_id_0, list_comment_alias.comment_text AS list_comment_alias_comment_text_0, list_comment_alias.blog_id AS list_comment_alias_blog_id_0_2, blog_alias_2.blog_id AS blog_alias_2_blog_id_0, blog_alias_2.blog_text AS blog_alias_2_blog_text_0, blog_alias_2.date_creation AS blog_alias_2_date_creation_0, blog_alias_2.author_id AS blog_alias_2_author_id_0_3, author_my_alias_suffix.author_id AS author_my_alias_suffix_author_id_0, author_my_alias_suffix.name AS author_my_alias_suffix_name_0, author_my_alias_suffix.birthdate AS author_my_alias_suffix_birthdate_0, author_my_alias_suffix.sex AS author_my_alias_suffix_sex_0, comment_my_alias_suffix.comment_id AS comment_my_alias_suffix_comment_id_0, comment_my_alias_suffix.blog_id AS comment_my_alias_suffix_blog_id_0, comment_my_alias_suffix.comment_text AS comment_my_alias_suffix_comment_text_0, comment_my_alias_suffix.date_creation AS comment_my_alias_suffix_date_creation_0, comment_my_alias_suffix.blog_id AS comment_my_alias_suffix_blog_id_0_5, category_my_alias_suffix.category_id AS category_my_alias_suffix_category_id_0, category_my_alias_suffix.name AS category_my_alias_suffix_name_0, category_my_alias_suffix.description AS category_my_alias_suffix_description_0
  FROM blog AS blog_alias
  LEFT OUTER JOIN author author_alias ON author_alias.author_id = blog_alias.author_id
  LEFT OUTER JOIN comment list_comment_alias ON list_comment_alias.blog_id = blog_alias.blog_id
  LEFT OUTER JOIN blog blog_alias_2 ON blog_alias_2.blog_id = list_comment_alias.blog_id
  LEFT OUTER JOIN author author_my_alias_suffix ON author_my_alias_suffix.author_id = blog_alias_2.author_id
  LEFT OUTER JOIN comment comment_my_alias_suffix ON comment_my_alias_suffix.blog_id = blog_alias_2.blog_id
  LEFT OUTER JOIN category_blog category_blog_6 ON blog_alias_2.blog_id = category_blog_6.blog_id
  LEFT OUTER JOIN category category_my_alias_suffix ON category_blog_6.category_id = category_my_alias_suffix.category_id


Ajout SQL dans les clauses LEFT OUTER JOIN / INNER JOIN

La classe qx::QxSqlQuery (ou son alias qx_query) dispose de la méthode suivante :

QxSqlQuery & QxSqlQuery::addJoinQuery(const QString & relationKeyOrAlias, const QxSqlQuery & joinQuery);


La méthode qx::QxSqlQuery::addJoinQuery() permet d'insérer des sous-requêtes SQL dans les clauses LEFT OUTER JOIN / INNER JOIN.
Par exemple :

// Test to add join SQL sub-queries (inside LEFT OUTER JOIN or INNER JOIN)
list_blog lstBlogWithJoinQueries;
qx_query query = qx_query().where("blog_alias.blog_text").isEqualTo("update blog_text_1");
query.addJoinQuery("list_comment_alias", "AND list_comment_alias.comment_text IS NOT NULL");
query.addJoinQuery("author_alias", qx_query().freeText("AND author_alias.sex = :sex", QVariantList() << author::female));
daoError = qx::dao::fetch_by_query_with_relation(QStringList() << "<blog_alias> { blog_text }" << "author_id <author_alias> { name, birthdate, sex }" 
                                                               << "list_comment <list_comment_alias> { comment_text }", query, lstBlogWithJoinQueries);
qx::dump(lstBlogWithJoinQueries);
qAssert(lstBlogWithJoinQueries.size() > 0);
qAssert(lstBlogWithJoinQueries[0]->m_text == "update blog_text_1");
qAssert(lstBlogWithJoinQueries[0]->m_author->m_sex == author::female);


Le code C++ ci-dessus va construire la requête SQL suivante :

SELECT blog_alias.blog_id AS blog_alias_blog_id_0, blog_alias.blog_text AS blog_alias_blog_text_0, blog_alias.author_id AS blog_alias_author_id_0, author_alias.author_id AS author_alias_author_id_0, author_alias.name AS author_alias_name_0, author_alias.birthdate AS author_alias_birthdate_0, author_alias.sex AS author_alias_sex_0, list_comment_alias.comment_id AS list_comment_alias_comment_id_0, list_comment_alias.blog_id AS list_comment_alias_blog_id_0, list_comment_alias.comment_text AS list_comment_alias_comment_text_0
  FROM blog AS blog_alias
  LEFT OUTER JOIN author author_alias ON (author_alias.author_id = blog_alias.author_id
      AND author_alias.sex = :sex)
  LEFT OUTER JOIN comment list_comment_alias ON (list_comment_alias.blog_id = blog_alias.blog_id
      AND list_comment_alias.comment_text IS NOT NULL)
  WHERE blog_alias.blog_text = :blog_alias_blog_text_1_0


Collections supportées par QxOrm

QxOrm supporte de nombreux conteneurs livrés avec Qt, boost ou la bibliothèque standard std. La bibliothèque QxOrm fournit également son propre conteneur, nommé qx::QxCollection, particulièrement adapté pour stocker les données issues d'une base de données. Le développeur a donc à sa disposition un large choix : QxOrm n'impose aucune contrainte sur l'utilisation des collections.

Collections de Qt

  QList<T>  
  QVector<T>  
  QSet<T>  
  QLinkedList<T>  
  QHash<Key, Value>  
  QMap<Key, Value>  
  QMultiHash<Key, Value>  
  QMultiMap<Key, Value>  

Collections de boost

  boost::unordered_map<Key, Value>  
  boost::unordered_set<T>  
  boost::unordered_multimap<Key, Value>  
  boost::unordered_multiset<T>  

Collections fournies par l'espace de nom standard std

  std::list<T>  
  std::vector<T>  
  std::set<T>  
  std::map<Key, Value>  

  std::unordered_map<Key, Value>  
  std::unordered_set<T>  
  std::unordered_multimap<Key, Value>  
  std::unordered_multiset<T>  

qx::QxCollection

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 ;
  • thread-safe.
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();


Pointeurs intelligents supportés par QxOrm (smart-pointers)

QxOrm supporte de nombreux pointeurs intelligents livrés avec Qt, boost ou la bibliothèque standard std. La bibliothèque QxOrm fournit également son propre pointeur intelligent, nommé qx::dao::ptr, apportant de nouvelles fonctionnalités lorsqu'il est utilisé avec les fonctions de l'espace de nom qx::dao. Le développeur a donc à sa disposition un large choix : QxOrm n'impose aucune contrainte sur l'utilisation des pointeurs intelligents.

Pointeurs intelligents de Qt

  QSharedPointer<T>  
  QScopedPointer<T>  
  QWeakPointer<T>  
  QSharedDataPointer<T>  

Pointeurs intelligents de boost

  boost::shared_ptr<T>  
  boost::intrusive_ptr<T>  
  boost::scoped_ptr<T>  
  boost::weak_ptr<T>  

Pointeurs intelligents fournis par l'espace de nom standard std

  std::shared_ptr<T>  
  std::unique_ptr<T>  
  std::weak_ptr<T>  

qx::dao::ptr

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);


Déclencheurs (triggers)

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 'qx::dao::detail::QxDao_Trigger<T>' 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";
}


Validation d'une instance C++ (validators)

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 :
  • qx::IxValidator : chaque contrainte définie dans la fonction de mapping void qx::register_class<T> est associée à une interface de type qx::IxValidator ;
  • qx::IxValidatorX : pour une classe donnée, la liste des contraintes est associée à une interface de type qx::IxValidatorX. Cette collection peut être parcourue à l'exécution du programme : ça peut être intéressant par exemple pour générer le schéma DDL SQL et prendre en compte les contraintes au niveau de la base de données (voir le chapitre suivant du manuel utilisateur : Générer le schéma DDL SQL de la base de données) ;
  • qx::QxInvalidValueX : au moment du processus de validation, lorsqu'une instance n'est pas valide, la liste des contraintes non respectées est représentée par une collection de type qx::QxInvalidValueX ;
  • qx::QxInvalidValue : chaque élément de cette collection est de type qx::QxInvalidValue et contient un message d'erreur (description expliquant pourquoi l'instance est invalide).
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.

Gérer la valeur NULL de la base de données

Les bases de données possèdent la notion de valeur NULL : pour plus de détails sur la valeur NULL, rendez-vous sur la page Wikipedia.
La bibliothèque QxOrm permet de gérer la valeur NULL de plusieurs façons différentes :
  • utilisation de la classe boost::optional fournie par boost ;
  • utilisation de la classe QVariant fournie par Qt ;
  • utilisation de pointeurs ou pointeurs intelligents : un pointeur NULL est associé à la valeur NULL en base de données.

boost::optional

La classe boost::optional<T> fournie par boost est particulièrement adaptée pour gérer la notion de valeur NULL en base de données.
Pour utiliser boost::optional<T> avec la bibliothèque QxOrm, il est nécessaire de définir l'option de compilation _QX_ENABLE_BOOST, ou bien d'inclure l'en-tête <QxExtras/QxBoostOptionalOnly.h>.
Voici un exemple de classe dont toutes les propriétés (sauf la clé primaire) peuvent être NULL en utilisant boost::optional :

#ifndef _PERSON_H_
#define _PERSON_H_

class person
{
public:
   long id;
   boost::optional<QString> firstName;
   boost::optional<QString> lastName;
   boost::optional<QDateTime> birthDate;  

   person() : id(0) { ; }
   virtual ~person() { ; }
};

#endif // _PERSON_H_

La classe boost::optional<T> se manipule facilement : rendez-vous sur la documentation fournie par boost pour plus de détails.

QVariant

La classe QVariant fournie par Qt permet également de gérer la notion de valeur NULL en base de données.
Voici un exemple de classe dont toutes les propriétés (sauf la clé primaire) peuvent être NULL en utilisant QVariant :

#ifndef _PERSON_H_
#define _PERSON_H_

class person
{
public:
   long id;
   QVariant firstName;
   QVariant lastName;
   QVariant birthDate;  

   person() : id(0) { ; }
   virtual ~person() { ; }  
};

#endif // _PERSON_H_

Cette solution a pour désavantage de perdre le type de donnée comparé à boost::optional<T>.
Il est donc recommandé d'utiliser boost::optional<T> pour gérer la valeur NULL avec la bibliothèque QxOrm.

Héritage et polymorphisme

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.

Interface qx::IxPersistable (classe abstraite)

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 :

Utiliser le pattern C++ PIMPL (Private Implementation idiom ou d-pointer)

Définition du site cppreference : "Pointer to implementation" or "pImpl" is a C++ programming technique that removes implementation details of a class from its object representation by placing them in a separate class, accessed through an opaque pointer. This technique is used to construct C++ library interfaces with stable ABI and to reduce compile-time dependencies.

Les avantages à utiliser le pattern PIMPL pour définir une classe persistente enregistrée dans le contexte QxOrm :
  • Compilation Firewall : si l'implémentation privée change, le code client n'a pas besoin d'être recompilé ;
  • Réduction des temps de compilation : les fichiers d'en-têtes (*.h, *.hpp) sont moins volumineux ;
  • Compatibilité binaire : vous pouvez développer plusieurs versions d'une bibliothèque sans casser la compatibilité ;
  • Réduction de la taille des exécutables générés.
Les inconvients du pattern PIMPL :
  • Légère baisse des performances : nécessité d'utiliser un niveau d'indirection supplémentaire avec le pointeur opaque ;
  • Nécessité d'allouer un pointeur par instance (peut causer des problèmes de memory fragmentation).

La bibliothèque QxOrm fournit un projet de test où toutes les classes persistantes sont codées en utilisant le pattern PIMPL : qxBlogPImpl (avec gestion des relations).
Il est également possible d'utiliser l'application QxEntityEditor pour générer facilement et automatiquement toutes les classes persistantes d'un projet C++ avec l'option PIMPL.

Exemple de classe persistante C++ enregistrée dans le contexte QxOrm en utilisant le pattern PIMPL (avec relations 1-n, n-1 et n-n) :

#ifndef _QX_BLOG_BLOG_H_
#define _QX_BLOG_BLOG_H_

class author;
class comment;
class category;

class QX_BLOG_DLL_EXPORT blog
{

   QX_REGISTER_FRIEND_CLASS(blog)

private:

   struct blog_impl;
   std::unique_ptr<blog_impl> m_pImpl; //!< Private implementation idiom

public:

   blog();
   virtual ~blog();

   blog(const blog & other);
   blog & operator=(const blog & other);

#ifdef Q_COMPILER_RVALUE_REFS
   blog(blog && other) Q_DECL_NOEXCEPT;
   blog & operator=(blog && other) Q_DECL_NOEXCEPT;
#endif // Q_COMPILER_RVALUE_REFS

   long id() const;
   QString text() const;
   QDateTime dateCreation() const;

   void setId(long l);
   void setText(const QString & s);
   void setDateCreation(const QDateTime & d);

   std::shared_ptr<author> & getAuthor();
   QList< std::shared_ptr<comment> > & listOfComments();
   qx::QxCollection<long, QSharedPointer<category> > & listOfCategories();

};

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

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

#endif // _QX_BLOG_BLOG_H_

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

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

#include <QxOrm_Impl.h>

QX_REGISTER_CPP_QX_BLOG(blog)

struct Q_DECL_HIDDEN blog::blog_impl
{
   long           m_id;
   QString        m_text;
   QDateTime      m_dt_creation;
   author_ptr     m_author;
   list_comment   m_commentX;
   list_category  m_categoryX;

   blog_impl() : m_id(0) { ; }
   ~blog_impl() { ; }
};

namespace qx {
template <> void register_class(QxClass<blog> & t)
{
   IxDataMember * pImpl = t.pimpl(& blog::m_pImpl);

   t.id(& blog::blog_impl::m_id, "blog_id", 0, pImpl);

   t.data(& blog::blog_impl::m_text, "blog_text", 0, true, true, pImpl);
   t.data(& blog::blog_impl::m_dt_creation, "date_creation", 0, true, true, pImpl);

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

blog::blog() : m_pImpl(new blog_impl()) { ; }

blog::~blog() { ; }

blog::blog(const blog & other) : m_pImpl(new blog_impl(* other.m_pImpl)) { ; }

blog & blog::operator=(const blog & other)
{
   if (this != (& other)) { (* m_pImpl) = (* other.m_pImpl); }
   return (* this);
}

#ifdef Q_COMPILER_RVALUE_REFS
blog::blog(blog && other) Q_DECL_NOEXCEPT : m_pImpl(std::move(other.m_pImpl)) { ; }
blog & blog::operator=(blog && other) Q_DECL_NOEXCEPT { if (this != (& other)) { m_pImpl = std::move(other.m_pImpl); }; return (* this); }
#endif // Q_COMPILER_RVALUE_REFS

long blog::id() const { return m_pImpl->m_id; }

QString blog::text() const { return m_pImpl->m_text; }

QDateTime blog::dateCreation() const { return m_pImpl->m_dt_creation; }

void blog::setId(long l) { m_pImpl->m_id = l; }

void blog::setText(const QString & s) { m_pImpl->m_text = s; }

void blog::setDateCreation(const QDateTime & d) { m_pImpl->m_dt_creation = d; }

std::shared_ptr<author> & blog::getAuthor() { return m_pImpl->m_author; }

QList< std::shared_ptr<comment> > & blog::listOfComments() { return m_pImpl->m_commentX; }

qx::QxCollection<long, QSharedPointer<category> > & blog::listOfCategories() { return m_pImpl->m_categoryX; }


Persister des types personnalisés

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, qx::cvt::context::ctx_type ctx)
{ /* 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, qx::cvt::context::ctx_type ctx)
{ /* Ici je convertis QVariant en ExtObject3D */; return qx_bool(true); } };

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


Remarque : Voici un template pour créer un type personnalisé persistable :

#ifndef _MY_CUSTOM_PERSISTABLE_TYPE_H_
#define _MY_CUSTOM_PERSISTABLE_TYPE_H_

#ifdef _MSC_VER
#pragma once
#endif

#include <QxOrm.h>

class MyPersistableType
{
   /* What you want here */
};

QX_REGISTER_CLASS_NAME(MyPersistableType)
QX_CLASS_VERSION(MyPersistableType, 0)

QDataStream & operator<< (QDataStream & stream, const MyPersistableType & t)
{
   /* Your implementation here */
}

QDataStream & operator>> (QDataStream & stream, MyPersistableType & t)
{
   /* Your implementation here */
}

namespace qx {
namespace cvt {
namespace detail {

template <> struct QxConvert_ToVariant< MyPersistableType > {
static inline QVariant toVariant(const MyPersistableType & t, const QString & format, int index, qx::cvt::context::ctx_type ctx)
{
   /* Here I convert from MyPersistableType to QVariant */
} };

template <> struct QxConvert_FromVariant< MyPersistableType > {
static inline qx_bool fromVariant(const QVariant & v, MyPersistableType & t, const QString & format, int index, qx::cvt::context::ctx_type ctx)
{
   /* Here I convert from QVariant to MyPersistableType */
   return qx_bool(true);
} };

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

#ifndef _QX_NO_JSON

namespace qx {
namespace cvt {
namespace detail {

template <>
struct QxConvert_ToJson< MyPersistableType >
{
   static inline QJsonValue toJson(const MyPersistableType & t, const QString & format)
   {
      /* Your implementation here */
   }
};

template <>
struct QxConvert_FromJson< MyPersistableType >
{
   static inline qx_bool fromJson(const QJsonValue & j, MyPersistableType & t, const QString & format)
   {
      /* Your implementation here */
   }
};

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

#endif // _QX_NO_JSON

// ------------------------------------
// If you are using boost serialization, you have also to implement save/load functions like above 'ExtObject3D' example
// ------------------------------------

#endif // _MY_CUSTOM_PERSISTABLE_TYPE_H_


Générer le schéma DDL SQL de la base de données

!!! Il est fortement 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.).

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 ce chapitre du manuel utilisateur : Persister des types personnalisés), 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")


Effectuer des requêtes asynchrones à la base de données

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;
   //...
}


Gestion du cache pour sauvegarder des instances C++ (module QxCache)

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();


Travailler avec plusieurs bases de données

Dans le chapitre Connexion à la base de données, nous avons vu comment paramétrer la connexion par défaut avec la classe singleton : qx::QxSqlDatabase. La bibliothèque QxOrm étant basée sur le moteur QtSql de Qt, elle utilise en interne la classe QSqlDatabase de Qt. Toutes les fonctions d'accès à la base de données (namespace qx::dao, classe qx::QxSession, etc...) ont un paramètre optionnel : QSqlDatabase * pDatabase = NULL :
  • si la valeur de ce paramètre est à NULL (valeur par défaut) : alors la bibliothèque QxOrm utilise la classe singleton qx::QxSqlDatabase pour se connecter à la base de données (avec gestion automatique du multi-threading) ;
  • si la valeur est non nulle : alors la bibliothèque QxOrm utilise la connexion fournie par le pointeur QSqlDatabase * pDatabase.
Ce paramètre permet donc de gérer son propre pool de connexions à une ou plusieurs bases de données.

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.

Déclarer automatiquement les méta-propriétés de Qt (macro Q_PROPERTY)

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).

Sérialisation

La sérialisation est un mécanisme permettant de sauvegarder l'état d'une instance d'objet dans un flux (fichier, réseau, etc...) sous un certain format (binaire, XML, JSON, texte, etc...). La désérialisation est le processus inverse permettant de restaurer l'état d'un objet à partir d'un flux. Pour plus d'informations sur la notion de sérialisation : rendez-vous sur la page Wikipedia.

Toute classe C++ enregistrée dans le contexte QxOrm peut être sérialisée dans différents formats : Remarque : le moteur de sérialisation de la bibliothèque QxOrm permet de proposer des fonctionnalités supplémentaires comme le clonage d'entités, le dump d'entités (format XML ou JSON) ou encore le module QxService.

Autre remarque : par défaut, toutes les propriétés enregistrées dans le contexte QxOrm sont sérialisables. Pour supprimer une propriété du moteur de sérialisation, il est possible d'écrire :

namespace qx {
template <> void register_class(QxClass<person> & t)
{
  IxDataMember * pDataMember = t.data(& person::age, "age");
  pDataMember->setSerialize(false);
}}


N° version pour assurer une compatibilité ascendante

La compatibilité ascendante permet à une application de pouvoir désérialiser un flux provenant d'une version antérieure. La bibliothèque QxOrm impose un numéro de version par classe ainsi qu'un numéro de version pour chaque propriété enregistrée dans le contexte QxOrm afin de pouvoir assurer une compatibilité ascendante automatiquement.

Par exemple, imaginons une classe person créée dans une application en version A : nous renseignons dans la macro QX_REGISTER_HPP une n° de version à 0 (correspond à la 1ère version de notre classe person), ainsi qu'un n° de version à 0 pour chacune des propriétés de la classe (si paramètre non renseigné, 0 est la valeur par défaut). Ce qui donne le résultat suivant :

* Fichier person.h :
#ifndef _PERSON_H_
#define _PERSON_H_

class person
{
public:
   long id;
   QString firstName;
   QString lastName;
   QDateTime birthDate;

   person() : id(0) { ; }
   virtual ~person() { ; }
};

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

#endif // _PERSON_H_

* Fichier person.cpp :
namespace qx {
template <> void register_class(QxClass<person> & t)
{
  t.id(& person::id, "id");
  t.data(& person::firstName, "first_name", 0);
  t.data(& person::lastName, "last_name", 0);
  t.data(& person::birthDate, "birth_date", 0);
}}


Dans la version B de notre application, nous modifions la classe person pour ajouter 2 nouvelles propriétés : sex et address. Notre classe ayant évoluée, il faut donc incrémenter son n° de version, et les nouvelles propriétés doivent avoir un n° de version à 1, ce qui donne :

* Fichier person.h :
#ifndef _PERSON_H_
#define _PERSON_H_

class person
{
public:
   long id;
   QString firstName;
   QString lastName;
   QDateTime birthDate;
   QString sex;
   QString address;

   person() : id(0) { ; }
   virtual ~person() { ; }
};

QX_REGISTER_HPP_MY_TEST_EXE(person, qx::trait::no_base_class_defined, 1)

#endif // _PERSON_H_

* Fichier person.cpp :
namespace qx {
template <> void register_class(QxClass<person> & t)
{
  t.id(& person::id, "id");
  t.data(& person::firstName, "first_name", 0);
  t.data(& person::lastName, "last_name", 0);
  t.data(& person::birthDate, "birth_date", 0);
  t.data(& person::sex, "sex", 1);
  t.data(& person::address, "address", 1);
}}


Remarque : en procédant ainsi, la bibliothèque QxOrm peut sérialiser une instance de la classe person dans une application en version A, puis désérialiser à partir de ce flux issu de la version A afin de recréer une instance de la classe person dans une version B de l'application.

Autre remarque : la suppression d'une propriété casse la compatibilité ascendante. Il est donc recommandé de ne jamais supprimer de propriété pour utiliser le moteur de sérialisation : il est possible par exemple de mettre une visibilité à private et de supprimer les accesseurs get/set, la propriété devenant ainsi inaccessible à l'extérieur de la classe, elle peut alors être considérée comme étant obsolète.

Moteur QDataStream de Qt

Toute classe C++ enregistrée dans le contexte QxOrm peut être sérialisée en utilisant le moteur QDataStream de Qt. Les fonctions pour utiliser ce type de sérialisation sont disponibles dans l'espace de nom : namespace qx::serialization::qt. Remarque : la sérialisation QDataStream est portable (sérialisation/désérialisation compatible sur tous types d'environnement : Windows, Linux, Mac OS X, etc...). Le flux sérialisé est au format binaire : la taille du flux est donc réduite (comparé à un flux XML par exemple). La sérialisation QDataStream étant basée sur le moteur d'introspection de la bibliothèque QxOrm, elle est moins performante que les sérialisations basées sur le moteur boost::serialization.

Par exemple :
   // Fetch a drug with id '3' in a new variable
   // drug is a C++ class registered in QxOrm context
   drug d;
   d.id = 3;
   QSqlError daoError = qx::dao::fetch_by_id(d);

   // Serialize the drug to a file
   qx::serialization::qt::to_file(d, "export_drug.txt");

   // Import drug from file in a new instance
   drug d2;
   qx::serialization::qt::from_file(d2, "export_drug.txt");

   // Check if d == d2
   qAssert(d == d2);

Remarque : dans l'exemple ci-dessus, nous sérialisons une instance C++. Toutes les fonctions du namespace qx::serialization peuvent également sérialiser des listes d'instances C++. Pour connaitre la liste des collections supportées, rendez-vous dans le chapitre : Collections supportées par QxOrm.

Moteur JSON de Qt

Toute classe C++ enregistrée dans le contexte QxOrm peut être sérialisée en utilisant le moteur QJson de Qt (nécessite Qt5). Les fonctions pour utiliser ce type de sérialisation sont disponibles dans l'espace de nom : namespace qx::serialization::json. Remarque : le moteur de sérialisation JSON est le plus permissif (comparé au moteur XML par exemple) : en effet, les propriétés d'une instance peuvent être définies dans n'importe quel ordre, les propriétés peuvent être supprimées ou ajoutées. La désérialisation JSON ne retourne jamais d'erreur : elle ignore tout si le format des données est incorrect (le flux JSON doit par contre être valide) ou bien si des propriétés sont absentes : le moteur JSON est donc beaucoup plus flexible que le moteur XML.

Autre remarque : la sérialisation JSON est basée sur le moteur d'introspection de la bibliothèque QxOrm, elle est moins performante que les sérialisations basées sur le moteur boost::serialization.

Par exemple :
   // Fetch a list of authors from database and serialize them to a JSON file
   list_author list_of_author;
   qx::dao::fetch_all(list_of_author);
   qx::serialization::json::to_file(list_of_author, "list_of_author.json");

L'exemple ci-dessus génère le flux JSON suivant :
{
    "author_id_2": {
        "author_id": "author_id_2",
        "birthdate": "2016-03-24",
        "list_blog": [
        ],
        "name": "author_2",
        "sex": 1
    },
    "author_id_3": {
        "author_id": "author_id_3",
        "birthdate": "2016-03-24",
        "list_blog": [
        ],
        "name": "author_3",
        "sex": 1
    }
}


Remarque : le module QxRestApi de la bibliothèque QxOrm est basé sur le moteur de sérialisation JSON.

Autre remarque : il est possible de personnaliser le format de sortie JSON (filtrer les propriétés du flux JSON généré par la sérialisation). Les fonctions de sérialisation JSON dispose d'un paramètre optionnel de type QString nommé format. Les pré-requis pour utiliser le paramètre format sont :
  • le paramètre format doit être préfixé par : filter: ;
  • les propriétés à exporter sont définies entre { } ;
  • les relations sont séparées par le caractère | ;
  • il est possible d'utiliser le caractère * pour définir : toutes les relations sur 1 niveau ;
  • le caractère - devant les { } signifie : toutes les propriétés sauf.

Exemple : voici un exemple de sérialisation JSON en définissant un format de sortie pour filtrer certaines propriétés :

// Serialize a C++ instance to a JSON string
QString jsonFormat = "filter: { blog_text } | author_id { name, birthdate } | list_comment { comment_text } -> blog_id -> *";
QString outputJsonFiltered = qx::serialization::json::to_string(blog, 1, jsonFormat);
qDebug("[QxOrm] custom JSON serialization process (filtered) : \n%s", qPrintable(outputJsonFiltered));

// Fill a C++ instance based on a JSON string
blog_ptr blogFromJsonFiltered; blogFromJsonFiltered.reset(new blog());
qx::serialization::json::from_string(blogFromJsonFiltered, outputJsonFiltered, 1, jsonFormat);
qx::dump(blogFromJsonFiltered);
qAssert(blogFromJsonFiltered->m_text != ""); // Fetched
qAssert(blogFromJsonFiltered->m_dt_creation.isNull()); // Not fetched
qAssert(blogFromJsonFiltered->m_author->m_sex == author::unknown); // Not fetched
qAssert(blogFromJsonFiltered->m_author->m_name != ""); // Fetched
qAssert(blogFromJsonFiltered->m_commentX.size() > 0);
qAssert(blogFromJsonFiltered->m_commentX[0]->m_dt_create.isNull()); // Not fetched
qAssert(blogFromJsonFiltered->m_commentX[0]->m_text != ""); // Fetched
qAssert(blogFromJsonFiltered->m_commentX[0]->m_blog);


Moteur XML de boost::serialization

Le moteur XML de boost::serialization n'est pas activé par défaut : pour activer cette fonctionnalité, il est nécessaire de définir les options de compilation _QX_ENABLE_BOOST_SERIALIZATION et _QX_ENABLE_BOOST_SERIALIZATION_XML dans le fichier de configuration QxOrm.pri (ou QxOrm.cmake). Il est également nécessaire de compiler le binaire boost::serialization (ce module de boost n'étant pas header only), et de renseigner le chemin d'accès à ce binaire dans les variables QX_BOOST_LIB_PATH, QX_BOOST_LIB_SERIALIZATION_DEBUG et QX_BOOST_LIB_SERIALIZATION_RELEASE du fichier de configuration QxOrm.pri (ou QxOrm.cmake).

Toute classe C++ enregistrée dans le contexte QxOrm peut être sérialisée en utilisant le moteur XML de boost::serialization. Les fonctions pour utiliser ce type de sérialisation sont disponibles dans l'espace de nom : namespace qx::serialization::xml (mêmes fonctions que dans l'espace de nom qx::serialization::qt).

Ce type de sérialisation possède les caractéristiques suivantes :
  • portable : compatible sur tous types d'environnement : Windows, Linux, Mac OS X, etc... ;
  • slowest : plus lente que les sérialisations binary et text ;
  • largest : taille des flux générés plus importante que les sérialisations binary et text ;
  • human-readable : un flux XML peut facilement être analysé et lu par un éditeur externe ou un être humain.

Moteur binaire de boost::serialization

Le moteur binaire de boost::serialization n'est pas activé par défaut : pour activer cette fonctionnalité, il est nécessaire de définir les options de compilation _QX_ENABLE_BOOST_SERIALIZATION et _QX_ENABLE_BOOST_SERIALIZATION_BINARY dans le fichier de configuration QxOrm.pri (ou QxOrm.cmake). Il est également nécessaire de compiler le binaire boost::serialization (ce module de boost n'étant pas header only), et de renseigner le chemin d'accès à ce binaire dans les variables QX_BOOST_LIB_PATH, QX_BOOST_LIB_SERIALIZATION_DEBUG et QX_BOOST_LIB_SERIALIZATION_RELEASE du fichier de configuration QxOrm.pri (ou QxOrm.cmake).

Toute classe C++ enregistrée dans le contexte QxOrm peut être sérialisée en utilisant le moteur binaire de boost::serialization. Les fonctions pour utiliser ce type de sérialisation sont disponibles dans l'espace de nom : namespace qx::serialization::binary (mêmes fonctions que dans l'espace de nom qx::serialization::qt).

Ce type de sérialisation possède les caractéristiques suivantes :
  • non-portable : un flux sérialisé sur un environnement Windows peut être incompatible si désérialisation sur un environnement Linux par exemple : il est donc fortement recommandé de rester sur le même environnement ;
  • fastest : plus rapide que les sérialisations XML et text ;
  • smallest : taille des flux générés réduite comparé aux sérialisations XML et text ;
  • non-human-readable : un flux binaire n'est pas lisible (pas de log possible par exemple).

Autres types de sérialisation proposés par boost

Le moteur boost::serialization propose d'autres types de sérialisation. Ces différents types ne sont pas activés par défaut, pour utiliser ces fonctionnalités (mêmes fonctions que dans l'espace de nom qx::serialization::qt), il est nécessaire de définir les options de compilation suivantes dans le fichier de configuration QxOrm.pri (ou QxOrm.cmake) :

Cloner une instance C++

Toute classe C++ enregistrée dans le contexte QxOrm peut être clonée en utilisant une des fonctions suivantes : Par exemple :

   drug_ptr d1;
   d1.reset(new drug());
   d1->name = "name1";
   d1->description = "desc1";

   // Clone a drug
   drug_ptr d_clone = qx::clone(* d1);

   // Check if (d1 == d_clone)
   qAssert((* d1) == (* d_clone));

Remarque importante : il faut faire attention lorsqu'on clone un pointeur intelligent (boost::shared_ptr ou QSharedPointer par exemple) dont l'élément parent (root) peut être référencé plusieurs fois dans sa hiérarchie (cas d'une structure en arbre par exemple). Dans ce cas, afin de protéger le pointeur parent d'une double suppression (2 pointeurs intelligents qui pointent sur le même pointeur brut), il est conseillé de cloner de cette façon :

// 'pOther' type is boost::shared_ptr<myClass> (smart-pointer)
boost::shared_ptr<myClass> * pCloneTemp = qx::clone_to_nude_ptr(pOther);
boost::shared_ptr<myClass> pClone = (pCloneTemp ? (* pCloneTemp) : boost::shared_ptr<myClass>());
if (pCloneTemp) { delete pCloneTemp; pCloneTemp = NULL; }
// Now use 'pClone' ...


Afficher le détail d'une instance C++ (dump au format XML ou JSON)

Toute instance C++ enregistrée dans le contexte QxOrm peut être affichée au format JSON. Si le moteur XML de boost::serialization est activé, alors il est également possible d'afficher un dump sous format XML (paramètre d'entrée de la fonction qx::dump). Cette fonctionnalité peut être utile pour faire du débogage par exemple, ou bien pour générer des logs.

   blog_ptr b;
   b.reset(new blog());
   b->id = 36;
   qx::dao::fetch_by_id_with_all_relation(b);

   // Dump 'b' instance result from database (XML or JSON serialization)
   // Second parameter is optional : 'true' = JSON format, 'false' = XML format
   qx::dump(b, false);

Ce qui génère le flux XML suivant :

[QxOrm] start dump 'boost::shared_ptr<blog>'
<boost.shared_ptr-blog- class_id="0" tracking_level="0" version="1">
	<px class_id="1" tracking_level="1" version="0" object_id="_0">
		<blog_id>113</blog_id>
		<blog_text class_id="2" tracking_level="0" version="0">update blog_text_1</blog_text>
		<date_creation class_id="3" tracking_level="0" version="0">20100409162612000</date_creation>
		<author_id class_id="4" tracking_level="0" version="1">
			<px class_id="5" tracking_level="1" version="0" object_id="_1">
				<author_id>author_id_2</author_id>
				<name>author_2</name>
				<birthdate class_id="6" tracking_level="0" version="0">20100409</birthdate>
				<sex>1</sex>
				<list_blog class_id="7" tracking_level="0" version="0">
					<count>0</count>
					<item_version>1</item_version>
				</list_blog>
			</px>
		</author_id>
		<list_comment class_id="8" tracking_level="0" version="0">
			<count>2</count>
			<item class_id="9" tracking_level="0" version="1">
				<px class_id="10" tracking_level="1" version="0" object_id="_2">
					<comment_id>209</comment_id>
					<comment_text>comment_1 text</comment_text>
					<date_creation>20100409162612000</date_creation>
					<blog_id>
						<px class_id_reference="1" object_id="_3">
							<blog_id>113</blog_id>
							<blog_text></blog_text>
							<date_creation></date_creation>
							<author_id>
								<px class_id="-1"></px>
							</author_id>
							<list_comment>
								<count>0</count>
							</list_comment>
							<list_category class_id="11" tracking_level="0" version="0">
								<count>0</count>
							</list_category>
						</px>
					</blog_id>
				</px>
			</item>
			<item>
				<px class_id_reference="10" object_id="_4">
					<comment_id>210</comment_id>
					<comment_text>comment_2 text</comment_text>
					<date_creation>20100409162612000</date_creation>
					<blog_id>
						<px class_id_reference="1" object_id="_5">
							<blog_id>113</blog_id>
							<blog_text></blog_text>
							<date_creation></date_creation>
							<author_id>
								<px class_id="-1"></px>
							</author_id>
							<list_comment>
								<count>0</count>
							</list_comment>
							<list_category>
								<count>0</count>
							</list_category>
						</px>
					</blog_id>
				</px>
			</item>
		</list_comment>
		<list_category>
			<count>2</count>
			<item class_id="12" tracking_level="0" version="0">
				<first>355</first>
				<second class_id="13" tracking_level="0" version="0">
					<qt_shared_ptr class_id="14" tracking_level="1" version="0" object_id="_6">
						<category_id>355</category_id>
						<name>category_1</name>
						<description>desc_1</description>
						<list_blog class_id="15" tracking_level="0" version="0">
							<count>0</count>
						</list_blog>
					</qt_shared_ptr>
				</second>
			</item>
			<item>
				<first>357</first>
				<second>
					<qt_shared_ptr class_id_reference="14" object_id="_7">
						<category_id>357</category_id>
						<name>category_3</name>
						<description>desc_3</description>
						<list_blog>
							<count>0</count>
						</list_blog>
					</qt_shared_ptr>
				</second>
			</item>
		</list_category>
	</px>
</boost.shared_ptr-blog->
[QxOrm] end dump 'boost::shared_ptr<blog>'

Introspection - Réflexion

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. Pour plus de détails sur l'introspection (ou réflexion), rendez-vous sur la page Wikipedia.

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 (static ou non static).
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 :
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, ainsi que pour créer dynamiquement les instances des classes de paramètre (entrée/sortie).

Autre 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).

Autre remarque : afin d'initialiser le moteur d'introspection QxOrm, il est recommandé d'appeler la fonction suivante en début de programme (main par exemple) :

// Following command is recommanded to initialize QxOrm introspection engine
qx::QxClassX::registerAllClasses(true);


Obtenir dynamiquement la valeur d'une donnée membre

Pour obtenir dynamiquement la valeur d'une donnée membre en utilisant le moteur d'introspection de la bibliothèque QxOrm, il est nécessaire de passer par la classe : qx::IxDataMember. La classe qx::IxDataMember fournit plusieurs méthodes pour obtenir la valeur d'une donnée membre (chacune prenant en paramètre un pointeur générique de type void * correspondant à l'adresse de l'instance courante) : Par exemple : imaginons un pointeur générique de type void * vers une classe person. Nous pouvons obtenir la valeur de la propriété firstName de type QString de la façon suivante :

// Generic pointer of type void * : we know that p is of type 'person'
void * p = ...;

// Get a pointer to the registered data member 'firstName' of class 'person'
qx::IxDataMember * pDataMember = qx::QxClassX::getDataMember("person", "firstName");

// First method to get the data member value with the real type
QString sFirstName = pDataMember->getValue<QString>(p);

// Second method to get the data member value converted in QVariant
QVariant vFirstName = pDataMember->toVariant(p);

// Third method to get the value encapsulated in qx::any type
boost::any aFirstName = pDataMember->getValueAnyPtr(p);

// Check if all values are equals
qAssert((sFirstName == vFirstName.toString()) && (sFirstName == (* boost::any_cast<QString *>(aFirstName))));


Valoriser dynamiquement une donnée membre

De la même façon que pour obtenir la valeur d'une donnée membre, la classe qx::IxDataMember permet de valoriser une donnée membre (modifier sa valeur). La classe qx::IxDataMember fournit les 2 méthodes suivantes (chacune prend en paramètre un pointeur de type void * correspondant à l'adresse de l'instance courante, ainsi que la nouvelle valeur à positionner) :
  • fromVariant() : valorise la donnée membre en fonction du paramètre de type QVariant ;
  • setValue<T>() : valorise la donnée membre avec un paramètre du type réel T de la donnée membre.
Par exemple : imaginons un pointeur générique de type void * vers une classe person. Nous pouvons modifier la valeur de la propriété firstName de type QString de la façon suivante :

// Generic pointer of type void * : we know that p is of type 'person'
void * p = ...;

// Get a pointer to the registered data member 'firstName' of class 'person'
qx::IxDataMember * pDataMember = qx::QxClassX::getDataMember("person", "firstName");

// First method to change the data member value
QVariant vFirstName = QVariant("my new firstname 1");
pDataMember->fromVariant(p, vFirstName);

// Other method to change the data member value (using real type)
QString sFirstName = "other firstname 2";
pDataMember->setValue<QString>(p, sFirstName);


Appeler dynamiquement une fonction

Tout comme les données membre (propriétés), il est possible d'enregistrer des méthodes membre (fonctions) dans le contexte QxOrm (support des méthodes static et non static). Le moteur d'introspection de la bibliothèque QxOrm permet d'invoquer dynamiquement des méthodes de classe. Toutes les fonctions enregistrées dans le contexte QxOrm sont associées à une instance de la classe : qx::IxFunction. Pour enregistrer des méthodes dans le contexte QxOrm, il faut utiliser : Par exemple : on souhaite enregistrer dans le contexte QxOrm plusieurs méthodes d'une classe person :

* Fichier person.h :
#ifndef _PERSON_H_
#define _PERSON_H_

class person
{
public:
   long id;
   QString firstName;
   QString lastName;
   QDateTime birthDate;

   person() : id(0) { ; }
   virtual ~person() { ; }

   long getId() const;
   void myMethodWith2Params(int param1, const QString & param2);

   static double myStaticMethodWith1Param(long param1);

};

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

#endif // _PERSON_H_

* Fichier person.cpp :
namespace qx {
template <> void register_class(QxClass<person> & t)
{
  t.id(& person::id, "id");
  t.data(& person::firstName, "first_name");
  t.data(& person::lastName, "last_name");
  t.data(& person::birthDate, "birth_date");

  t.fct_0<long>(& person::getId, "getId");
  t.fct_2<void, int, const QString &>(& person::myMethodWith2Params, "myMethodWith2Params");

  t.fctStatic_1<double, long>(& person::myStaticMethodWith1Param, "myStaticMethodWith1Param");
}}


Une fois enregistrées dans le contexte QxOrm, il est possible d'appeler dynamiquement ces fonctions avec les méthodes qx::QxClassX::invoke() et qx::QxClassX::invokeStatic() :

   // Generic pointer of type void * : we know that p is of type 'person'
   void * p = ...;

   // Call method 'long getId() const' and get return value
   boost::any returnValue;
   qx::QxClassX::invoke("person", "getId", p, "", (& returnValue));
   long lId = boost::any_cast<long>(returnValue);

   // Call method 'myMethodWith2Params' with 2 parameters encapsulated in a string (default separator for parameters is character '|')
   // This way to pass parameters to the function works only if parameters are numeric or string
   // If parameters are more complex, then you have to encapsulate parameters in a list of qx::any, as shown below
   qx::QxClassX::invoke("person", "myMethodWith2Params", p, "36|my string param 2");

   // Call method 'myMethodWith2Params' with 2 parameters encapsulated in a list of qx::any : std::vector<qx::any>
   std::vector<boost::any> lstParams;
   int iParam1 = 36; lstParams.push_back(iParam1); // Parameter at position 1
   QString sParam2 = "my string param 2"; lstParams.push_back(sParam2); // Parameter at position 2
   qx::QxClassX::invoke("person", "myMethodWith2Params", p, lstParams);

   // Call static method 'myStaticMethodWith1Param' with 1 parameter and get return value
   qx::QxClassX::invokeStatic("person", "myStaticMethodWith1Param", "19", (& returnValue));
   double dValue = boost::any_cast<double>(returnValue);


Créer une instance C++ dynamiquement

Le moteur d'introspection de la bibliothèque QxOrm permet de créer dynamiquement des instances de classe (module QxFactory, modèle de conception fabrique ou design pattern factory) avec les méthodes suivantes :
  • qx::create(const QString & sKey) : création d'une instance de type sKey sous la forme qx::any (contenant un pointeur intelligent de type std::shared_ptr, alias de boost::shared_ptr par défaut) ;
  • qx::create_nude_ptr<T>(const QString & sKey) : création d'une instance de type sKey sous la forme d'un pointeur nu de type T * (attention à libérer la mémoire de ce pointeur pour éviter les fuites mémoire) ;
  • qx::create_void_ptr(const QString & sKey) : création d'une instance de type sKey sous la forme d'un pointeur nu de type void * (attention à libérer la mémoire de ce pointeur pour éviter les fuites mémoire).
Par exemple : le module QxService de la bibliothèque QxOrm utilise ce mécanisme pour créer dynamiquement les instances de classe de service pour exécuter les routines côté serveur :

   qx::service::IxService * ptr = qx::create_nude_ptr<qx::service::IxService>(m_sServiceName);   


Parcourir la liste des classes/propriétés enregistrées dans le contexte QxOrm

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

Services : transférer la couche de données persistante sur le réseau (module QxService)

Le module QxService de la bibliothèque QxOrm permet de créer rapidement un serveur d'applications C++ performant (notion de services avec demande du client et réponse du serveur). Un tutoriel est disponible sur le site QxOrm afin de présenter un exemple d'utilisation du module QxService. Le module QxService est basé sur le moteur d'introspection ainsi que le moteur de sérialisation de la bibliothèque QxOrm afin de transférer la couche de données persistante sur le réseau et exécuter automatiquement les routines côté serveur.

Remarque : pour activer le module QxService, il faut définir l'option de compilation _QX_ENABLE_QT_NETWORK dans le fichier de configuration QxOrm.pri (ou QxOrm.cmake). Cette option de compilation ajoute une dépendance au binaire QtNetwork fourni avec la bibliothèque Qt.

Autre remarque : l'application QxEntityEditor est livrée avec le plugin QxEECppServicesExport : ce plugin génère automatiquement le code source nécessaire pour transférer l'ensemble des entités d'un projet sur le réseau. Une liste de méthodes client/serveur est générée automatiquement :
  • count() : requête client/serveur pour compter le nombre d'éléments (avec possibilité d'utiliser un filtre SQL) ;
  • fetchById() : requête client/serveur pour alimenter les propriétés d'une entité en fonction de son identifiant ;
  • fetchAll() : requête client/serveur pour alimenter les propriétés de toutes les entités d'une table ;
  • fetchByQuery() : requête client/serveur pour alimenter les propriétés des entités filtrées par une requête SQL ;
  • insert() : requête client/serveur pour insérer les données d'une entité ;
  • update() : requête client/serveur pour mettre à jour les données d'une entité ;
  • save() : requête client/serveur pour sauvegarder les données d'une entité (insertion ou mise à jour) ;
  • deleteById() : requête client/serveur pour supprimer une entité en fonction de son identifiant ;
  • deleteAll() : requête client/serveur pour supprimer tous les éléments de la table mappée à une entité ;
  • deleteByQuery() : requête client/serveur pour supprimer tous les éléments en fonction d'une requête SQL ;
  • destroyById() : requête client/serveur pour supprimer une entité en fonction de son identifiant (avec prise en compte de la suppression logique) ;
  • destroyAll() : requête client/serveur pour supprimer tous les éléments de la table mappée à une entité (avec prise en compte de la suppression logique) ;
  • destroyByQuery() : requête client/serveur pour supprimer tous les éléments en fonction d'une requête SQL (avec prise en compte de la suppression logique) ;
  • executeQuery() : requête client/serveur pour exécuter une requête SQL personnalisée ou procédure stockée ;
  • exist() : requête client/serveur pour tester l'existence d'une entité en fonction de son identifiant ;
  • isValid() : requête client/serveur pour tester la validité d'une entité (module QxValidator).
Il est possible d'ajouter de nouveaux services ou de personnaliser les services générés automatiquement par l'application QxEntityEditor.

L'objectif de ce chapite est de présenter les concepts à mettre en oeuvre pour utiliser le module QxService :

Paramètres d'entrée/sortie d'un service (requête/réponse)

Chaque fonction publiée par un service dispose de paramètres d'entrée (demande du client) et de paramètres de sortie (réponse du serveur). Ces paramètres d'entrée/sortie doivent hériter de l'interface qx::service::IxParameter et doivent être enregistrées dans le contexte QxOrm (par la fonction void qx::register_class<T>).

Par exemple : voici un exemple de paramètres d'entrée/sortie générés automatiquement par l'application QxEntityEditor basé sur la classe blog du tutoriel qxBlog :

* Fichier blog.services.gen.h :
namespace services {

typedef boost::shared_ptr<blog> blog_ptr;
typedef qx::QxCollection<long, blog_ptr> list_of_blog;
typedef boost::shared_ptr<list_of_blog> list_of_blog_ptr;

/* -- Service Input Parameters -- */

class QXBLOG_SERVICES_EXPORT blog_input : public qx::service::IxParameter
{

public:

   blog_input();
   virtual ~blog_input();

   long id;                   //!< Id to fetch or delete
   blog_ptr instance;         //!< Single instance to fetch, insert, update, delete or validate
   list_of_blog_ptr list;     //!< List of instances to fetch, insert, update, delete or validate
   qx_query query;            //!< Query to execute when fetching, updating or deleting
   QStringList columns;       //!< List of columns to fetch or update
   QStringList relations;     //!< List of relations to fetch

};

typedef boost::shared_ptr<services::blog_input> blog_input_ptr;

/* -- Service Output Parameters -- */

class QXBLOG_SERVICES_EXPORT blog_output : public qx::service::IxParameter
{

public:

   blog_output();
   virtual ~blog_output();

   blog_ptr instance;            //!< Single instance from server
   list_of_blog_ptr list;        //!< List of instances from server
   QSqlError error;              //!< If a SQL error occurred, this output parameter is not empty
   qx::QxInvalidValueX invalid;  //!< Check if a single instance (or a list of instances) is valid
   qx_query query;               //!< Query which contains all results
   long count;                   //!< Count how many items in database using a query or not
   qx_bool exist;                //!< Check if a single instance (or a list of instances) exist in database

};

typedef boost::shared_ptr<services::blog_output> blog_output_ptr;

} // namespace services

QX_REGISTER_COMPLEX_CLASS_NAME_HPP_QXBLOG_SERVICES(services::blog_input, qx::service::IxParameter, 0, services_blog_input)
QX_REGISTER_COMPLEX_CLASS_NAME_HPP_QXBLOG_SERVICES(services::blog_output, qx::service::IxParameter, 0, services_blog_output)

* Fichier blog.services.gen.cpp :
QX_REGISTER_COMPLEX_CLASS_NAME_CPP_QXBLOG_SERVICES(services::blog_input, services_blog_input)
QX_REGISTER_COMPLEX_CLASS_NAME_CPP_QXBLOG_SERVICES(services::blog_output, services_blog_output)

namespace qx {

template <>
void register_class(QxClass<services::blog_input> & t)
{
   t.data(& services::blog_input::id, "id");
   t.data(& services::blog_input::instance, "instance");
   t.data(& services::blog_input::list, "list");
   t.data(& services::blog_input::query, "query");
   t.data(& services::blog_input::columns, "columns");
   t.data(& services::blog_input::relations, "relations");
}

template <>
void register_class(QxClass<services::blog_output> & t)
{
   t.data(& services::blog_output::instance, "instance");
   t.data(& services::blog_output::list, "list");
   t.data(& services::blog_output::error, "error");
   t.data(& services::blog_output::invalid, "invalid");
   t.data(& services::blog_output::query, "query");
   t.data(& services::blog_output::count, "count");
   t.data(& services::blog_output::exist, "exist");
}

} // namespace qx


Remarque : comme on peut le constater sur l'exemple ci-dessus, les paramètres d'entrée/sortie peuvent contenir des types complexes (des collections, des pointeurs, etc...). Il est donc possible et très simple de transférer des structures complexes sur le réseau avec le module QxService.

Définir les fonctions publiées par un service

Chaque service enregistré dans le module QxService publie une liste de fonctions accessibles côté client (requêtes client/serveur). Les services doivent hériter de la classe de base qx::service::QxService<INPUT, OUTPUT> (les paramètres template INPUT et OUTPUT correspondant aux paramètres d'entrée/sortie) et doivent être enregistrés dans le contexte QxOrm (par la fonction void qx::register_class<T>).

Par exemple : voici un exemple de service généré automatiquement par l'application QxEntityEditor basé sur la classe blog du tutoriel qxBlog :

* Fichier blog.services.gen.h :
namespace services {

/* -- Service Definition -- */

typedef qx::service::QxService< blog_input, blog_output > blog_base_class;
class QXBLOG_SERVICES_EXPORT blog_services : public blog_base_class
{

   QX_REGISTER_FRIEND_CLASS(services::blog_services)

public:

   blog_services();
   virtual ~blog_services();

protected:

   void fetchById_();
   void fetchAll_();
   void fetchByQuery_();

   void insert_();
   void update_();
   void save_();
   void deleteById_();
   void deleteAll_();
   void deleteByQuery_();
   void destroyById_();
   void destroyAll_();
   void destroyByQuery_();

   void executeQuery_();
   void callQuery_();
   void exist_();
   void count_();
   void isValid_();

#ifdef _QXBLOG_SERVICES_MODE_CLIENT

public:

   blog_ptr fetchById(long id, const QStringList & columns = QStringList(), const QStringList & relations = QStringList());
   QSqlError fetchById(blog_ptr & p, const QStringList & columns = QStringList(), const QStringList & relations = QStringList());
   QSqlError fetchById(list_of_blog_ptr & lst, const QStringList & columns = QStringList(), const QStringList & relations = QStringList());
   QSqlError fetchAll(list_of_blog_ptr & lst, const QStringList & columns = QStringList(), const QStringList & relations = QStringList());
   QSqlError fetchByQuery(const qx_query & query, list_of_blog_ptr & lst, const QStringList & columns = QStringList(),
                                       const QStringList & relations = QStringList());

   QSqlError insert(blog_ptr & p, const QStringList & relations = QStringList());
   QSqlError insert(list_of_blog_ptr & lst, const QStringList & relations = QStringList());
   QSqlError update(blog_ptr & p, const qx_query & query = qx_query(),
                              const QStringList & columns = QStringList(), const QStringList & relations = QStringList());
   QSqlError update(list_of_blog_ptr & lst, const qx_query & query = qx_query(),
                              const QStringList & columns = QStringList(), const QStringList & relations = QStringList());
   QSqlError save(blog_ptr & p, const QStringList & relations = QStringList());
   QSqlError save(list_of_blog_ptr & lst, const QStringList & relations = QStringList());

   QSqlError deleteById(long id);
   QSqlError deleteById(blog_ptr & p);
   QSqlError deleteById(list_of_blog_ptr & lst);
   QSqlError deleteAll();
   QSqlError deleteByQuery(const qx_query & query);
   QSqlError destroyById(long id);
   QSqlError destroyById(blog_ptr & p);
   QSqlError destroyById(list_of_blog_ptr & lst);
   QSqlError destroyAll();
   QSqlError destroyByQuery(const qx_query & query);

   QSqlError executeQuery(qx_query & query, blog_ptr & p);
   QSqlError executeQuery(qx_query & query, list_of_blog_ptr & lst);
   QSqlError callQuery(qx_query & query);
   qx_bool exist(blog_ptr & p);
   qx_bool exist(list_of_blog_ptr & lst);
   QSqlError count(long & lCount, const qx_query & query = qx_query());
   qx::QxInvalidValueX isValid(blog_ptr & p);
   qx::QxInvalidValueX isValid(list_of_blog_ptr & lst);

#endif // _QXBLOG_SERVICES_MODE_CLIENT

};

typedef boost::shared_ptr<services::blog_services> blog_services_ptr;

} // namespace services

QX_REGISTER_COMPLEX_CLASS_NAME_HPP_QXBLOG_SERVICES(services::blog_services, qx::service::IxService, 0, services_blog_services)

* Fichier blog.services.gen.cpp :
QX_REGISTER_COMPLEX_CLASS_NAME_CPP_QXBLOG_SERVICES(services::blog_services, services_blog_services)

namespace qx {

template <>
void register_class(QxClass<services::blog_services> & t)
{
   t.fct_0<void>(& services::blog_services::fetchById_, "fetchById");
   t.fct_0<void>(& services::blog_services::fetchAll_, "fetchAll");
   t.fct_0<void>(& services::blog_services::fetchByQuery_, "fetchByQuery");

   t.fct_0<void>(& services::blog_services::insert_, "insert");
   t.fct_0<void>(& services::blog_services::update_, "update");
   t.fct_0<void>(& services::blog_services::save_, "save");
   t.fct_0<void>(& services::blog_services::deleteById_, "deleteById");
   t.fct_0<void>(& services::blog_services::deleteAll_, "deleteAll");
   t.fct_0<void>(& services::blog_services::deleteByQuery_, "deleteByQuery");
   t.fct_0<void>(& services::blog_services::destroyById_, "destroyById");
   t.fct_0<void>(& services::blog_services::destroyAll_, "destroyAll");
   t.fct_0<void>(& services::blog_services::destroyByQuery_, "destroyByQuery");

   t.fct_0<void>(& services::blog_services::executeQuery_, "executeQuery");
   t.fct_0<void>(& services::blog_services::callQuery_, "callQuery");
   t.fct_0<void>(& services::blog_services::exist_, "exist");
   t.fct_0<void>(& services::blog_services::count_, "count");
   t.fct_0<void>(& services::blog_services::isValid_, "isValid");
}

} // namespace qx

// Then there is the implementation of all functions provided by the service...


Remarque : une fois définies dans le contexte QxOrm, le client peut appeler les fonctions publiées par le service : les routines côté serveur sont alors exécutées automatiquement. La sérialisation des données ainsi que la gestion de la couche réseau pour le transfert des données sont gérées de manière transparente par le module QxService.

Liste des options disponibles côté serveur

Le serveur d'application C++ basé sur le module QxService dispose de plusieurs paramètres accessibles par la classe singleton qx::service::QxConnect :
  • setPort() : port d'écoute pour recevoir les requêtes du client et envoyer les réponses du serveur ;
  • setThreadCount() : nombre de threads disponibles côté serveur pour traiter les demandes du client ;
  • setSerializationType() : type de sérialisation utilisé pour envoyer les réponses du serveur ;
  • setCompressData() : permet de définir si les données renvoyées par le serveur sont compressées ou non ;
  • setEncryptData() : permet de définir si les données renvoyées par le serveur sont cryptées ou non (avec possibilité de renseigner une clé de cryptage).

Paramétrage de la connexion côté client

La couche cliente basée sur le module QxService dispose de plusieurs paramètres accessibles par la classe singleton qx::service::QxConnect :
  • setIp() : adresse IP du serveur d'application C++ ;
  • setPort() : port utilisé par le serveur d'application C++ ;
  • setSerializationType() : type de sérialisation utilisé par la couche cliente pour envoyer les requêtes du client au serveur ;
  • setCompressData() : permet de définir si les données envoyées au serveur sont compressées ou non ;
  • setEncryptData() : permet de définir si les données envoyées au serveur sont cryptées ou non (avec possibilité de renseigner une clé de cryptage).

Gestion de l'authentification dans un service

Il est classique d'implémenter un contrôle au niveau du serveur pour vérifier l'utilisateur connecté à la couche cliente. L'interface qx::service::IxService (classe de base de tous les services enregistrés par le module QxService) fournit des méthodes virtuelles qui peuvent être surchargées pour gérer cette problématique :
  • onBeforeProcess() : méthode virtuelle appelée systématiquement avant exécution de la routine serveur ;
  • onAfterProcess() : méthode virtuelle appelée systématiquement après exécution de la routine serveur.

Par exemple : voici une classe de base nommée ParameterAuthentication qui peut être utilisée par tous les paramètres d'entrée/sortie, cette classe fournit 3 propriétés login, password et token :

* Fichier ParameterAuthentication.h :
class MY_DLL_EXPORT ParameterAuthentication : public qx::service::IxParameter
{
 
public:
 
   ParameterAuthentication();
   virtual ~ParameterAuthentication();
 
   QString login;
   QString password;
   QString token;
   // etc..., put here all properties required by the authentication process
 
};
 
typedef boost::shared_ptr<ParameterAuthentication> ParameterAuthentication_ptr;
 
QX_REGISTER_COMPLEX_CLASS_NAME_HPP_MY_DLL(ParameterAuthentication, qx::service::IxParameter, 0, ParameterAuthentication)

* Fichier ParameterAuthentication.cpp :
QX_REGISTER_COMPLEX_CLASS_NAME_CPP_MY_DLL(ParameterAuthentication, ParameterAuthentication)
 
namespace qx {
 
template <>
void register_class(QxClass<ParameterAuthentication> & t)
{
   t.data(& ParameterAuthentication::login, "login");
   t.data(& ParameterAuthentication::password, "password");
   t.data(& ParameterAuthentication::token, "token");
}
 
} // namespace qx


Maintenant que l'on dispose d'une classe de base pour nos paramètres (ParameterAuthentication), nous allons créer une classe de base utilisée par tous nos services nommée ServiceAuthentication<INPUT, OUTPUT>. Cette classe de base des services va surcharger la méthode virtuelle onBeforeProcess() afin de gérer l'authentification avant exécution de la routine serveur :

* Fichier ServiceAuthentication.h :
#include "ParameterAuthentication.h"
 
template <class INPUT, class OUTPUT>
class ServiceAuthentication : public qx::service::QxService<INPUT, OUTPUT>
{
 
public:
 
   ServiceAuthentication(const QString & sServiceName) : qx::service::QxService<INPUT, OUTPUT>(sServiceName) { ; }
   virtual ~ServiceAuthentication() { ; }
 
   virtual void onBeforeProcess()
   {
      // Here you can implement your own authentication control (checking login/password for example)
      // You can get input authentication parameters like this :
      ParameterAuthentication_ptr pParams = getInputParameter();
      pParams->login, pParams->password, etc...
 
      // If authentication is not valid, then you can throw an exception (and stop process before executing service function)
      throw qx::exception("Authentication error !");
   }
 
};


A présent, nous disposons des classes de base ParameterAuthentication et ServiceAuthentication<INPUT, OUTPUT> : toutes les classes de paramètres et toutes les classes de services doivent hériter de ces classes de base pour gérer automatiquement l'authentification, et retourner une erreur au client si les paramètres de l'utilisateur ne sont pas valides.

Remarque : de la même façon que pour gérer l'authentification, il est possible de mettre en place des logs automatiques en surchargeant les méthodes virtuelles onBeforeProcess() et onAfterProcess().

Requêtes client/serveur asynchrones

Par défaut, les requêtes client/serveur sont synchrones : ce qui signifie que la couche cliente attend la réponse du serveur pour continuer son exécution. Dans une interface graphique utilisateur (GUI), une requête client/serveur bloque l'application (freeze) si elle est exécutée dans le thread principal : si le serveur met du temps pour renvoyer sa réponse, l'utilisateur peut alors penser qu'il s'agit d'un crash de l'application. La module QxService propose une solution simple pour effectuer des requêtes asynchrones (qui ne bloquent donc pas l'interface graphique de l'utilisateur) grâce à la classe qx::service::QxClientAsync.

La classe qx::service::QxClientAsync utilise le moteur d'introspection de la bibliothèque QxOrm ainsi que le mécanisme SIGNAL-SLOT de Qt. Elle prend en paramètre :
  • une instance de service ;
  • les paramètres d'entrée/sortie du service ;
  • le nom de la routine serveur à exécuter (sous forme de chaine de caractères) ;
  • une fonction à appeler une fois que la transaction est terminée (connexion à l'évènement signal finished()).

Voici l'exemple issu du tutoriel qxClientServer qui exécute une routine serveur de manière asynchrone :

void main_dlg::onClickBtnDateTimeAsync()
{
   if (m_pDateTimeAsync) { qDebug("[QxOrm] '%s' transaction is already running", "server_infos::get_current_date_time"); return; }

   // Création d'une instance de service et appel à la méthode pour recevoir la date-heure courante du serveur (mode asynchrone)
   server_infos_ptr service = server_infos_ptr(new server_infos());
   m_pDateTimeAsync.reset(new qx::service::QxClientAsync());
   QObject::connect(m_pDateTimeAsync.get(), SIGNAL(finished()), this, SLOT(onDateTimeAsyncFinished()));
   m_pDateTimeAsync->setService(service, "get_current_date_time");
   m_pDateTimeAsync->start();
}

void main_dlg::onDateTimeAsyncFinished()
{
   if (! m_pDateTimeAsync || ! m_pDateTimeAsync->getService()) { return; }
   updateLastTransactionLog(m_pDateTimeAsync->getService()->getTransaction());
   m_pDateTimeAsync.reset();
}


Remarque : l'exemple ci-dessus montre comment effectuer une requête asynchrone avec les actions suivantes :
  • création d'une instance d'un service (de type server_infos_ptr pour cet exemple) ;
  • création d'une instance de type qx::service::QxClientAsync ;
  • connexion à l'évènement finished (pour indiquer qu'une réponse du serveur vient d'arriver) ;
  • passage de l'instance du service et de la méthode à appeler (sous forme de chaine de caractères) à l'objet qx::service::QxClientAsync ;
  • démarrage de la transaction avec l'appel de la méthode start().

Moteur modèle/vue (module QxModelView)

Le module QxModelView permet d'utiliser le moteur model/view de Qt avec toutes les classes enregistrées dans le contexte QxOrm :
  • 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 ;
  • Qt widgets : utilisation de QTableView ou QListView par exemple pour afficher/modifier le contenu d'une table de la base 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. L'interface qx::IxModel fournit également des méthodes définies Q_INVOKABLE et sont donc accessibles directement en QML :
  • qxCount_() : compte le nombre d'éléments dans la table de la base de données associée au modèle (avec possibilité d'indiquer un filtre SQL) ;
  • qxFetchById_() : alimente le modèle en fonction de l'identifiant passé en paramètre ;
  • qxFetchAll_() : alimente le modèle avec tous les éléments contenus dans la table de la base de données associée au modèle ;
  • qxFetchByQuery_() : alimente le modèle avec les éléments de la table de la base de données associée au modèle en fonction d'une requête SQL ;
  • qxFetchRow_() : alimente (met à jour) une ligne du modèle (chaque ligne du modèle dispose de son propre identifiant de base de données) ;
  • qxInsert_() : insère l'intégralité du modèle en base de données ;
  • qxInsertRow_() : insère une ligne du modèle en base de données ;
  • qxUpdate_() : met à jour l'intégralité du modèle en base de données ;
  • qxUpdateRow_() : met à jour une ligne du modèle en base de données ;
  • qxSave_() : sauvegarde l'intégralité du modèle en base de données (insertion ou mise à jour) ;
  • qxSaveRow_() : sauvegarde une ligne du modèle en base de données (insertion ou mise à jour) ;
  • qxDeleteById_() : supprime un élément de la base de données en fonction de l'identifiant passé en paramètre ;
  • qxDeleteAll_() : supprime tous les éléments de la table de la base de données associée au modèle ;
  • qxDeleteByQuery_() : supprime les éléments de la table de la base de données associée au modèle en fonction d'une requête SQL ;
  • qxDeleteRow_() : supprime une ligne du modèle de la base de données (chaque ligne du modèle dispose de son propre identifiant de base de données) ;
  • qxDestroyById_() : supprime un élément de la base de données en fonction de l'identifiant passé en paramètre (avec prise en compte de la suppression logique) ;
  • qxDestroyAll_() : supprime tous les éléments de la table de la base de données associée au modèle (avec prise en compte de la suppression logique) ;
  • qxDestroyByQuery_() : supprime les éléments de la table de la base de données associée au modèle en fonction d'une requête SQL (avec prise en compte de la suppression logique) ;
  • qxDestroyRow_() : supprime une ligne du modèle de la base de données (chaque ligne du modèle dispose de son propre identifiant de base de données), avec prise en compte de la suppression logique ;
  • qxExecuteQuery_() : alimente le modèle en fonction d'une requête SQL personnalisée ou procédure stockée ;
  • qxExist_() : teste l'existence d'un élément en fonction de l'identifiant passé en paramètre ;
  • qxValidate_() : teste la validité de l'intégralité du modèle (module QxValidator) ;
  • qxValidateRow_() : teste la validité d'une ligne du modèle (module QxValidator).

Remarque : 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).

Définir un modèle "simple" (sans relation)

Toute classe enregistrée dans le contexte QxOrm peut être utilisée en tant que modèle afin d'alimenter des vues. La classe de base qx::IxModel des modèles QxOrm hérite de la classe Qt QAbstractItemModel : les modèles QxOrm sont donc entièrement compatibles avec le moteur model/view de Qt.

Une seule ligne de code est suffisante pour instancier un modèle QxOrm :

   qx::IxModel * pModel = new qx::QxModel<MyClass>();   

Remarque : le modèle créé avec cette ligne de code expose automatiquement toutes les propriétés enregistrées dans le contexte QxOrm au moteur model/view.

Modèles avec relations (notion de modèles imbriqués)

Adapter les relations entre classe (1-n, n-1 ou n-n) au moteur model/view de Qt est complexe : la solution proposée par la bibliothèque QxOrm est l'utilisation de modèles imbriqués. Pour plus de détails sur la notion de modèles imbriqués, un tutoriel est disponible sur le site developpez.com.

Pour utiliser les relations (1-n, n-1 ou n-n) avec le module QxModelView, il est important de comprendre qu'il peut y avoir une hiérarchie entre modèles (un modèle parent peut avoir plusieurs modèles enfants associés, c'est la notion de modèles imbriqués).

Afin de pouvoir travailler avec des relations (modèles imbriqués), il est nécessaire de créer des classes modèles qui héritent de : qx::QxModel<T>. Ainsi, toutes les propriétés simples (non relation) sont automatiquement exposées aux vues (grâce à la classe de base), il reste à écrire uniquement les accesseurs pour accéder aux relations. L'application QxEntityEditor est livrée avec le plugin QxEECppModelViewExport : ce plugin génère automatiquement le code source pour pouvoir travailler avec des modèles imbriqués.

Voici un exemple de code généré par l'application QxEntityEditor afin de créer un modèle basé sur la classe blog (voir le tutoriel qxBlog pour plus de détails). La classe blog dispose de 3 relations : author (n-1), list_of_comment (1-n) et list_of_category (n-n) :

* Fichier blog.model_view.gen.h :
namespace model_view {

typedef qx::QxModel<blog> blog_model_base_class;

class QXBLOG_MODEL_VIEW_EXPORT blog_model : public blog_model_base_class
{

   Q_OBJECT

public:

   blog_model(QObject * parent = 0);
   blog_model(qx::IxModel * other, QObject * parent);
   virtual ~blog_model();

   Q_INVOKABLE QObject * author(int row, bool bLoadFromDatabase = false, const QString & sAppendRelations = QString());
   Q_INVOKABLE QObject * list_of_comment(int row, bool bLoadFromDatabase = false, const QString & sAppendRelations = QString());
   Q_INVOKABLE QObject * list_of_category(int row, bool bLoadFromDatabase = false, const QString & sAppendRelations = QString());

   /* List of properties exposed by the model (3) :
      - blog_id
      - title
      - text
   */

protected:

   virtual void syncNestedModel(int row, const QStringList & relation);
   virtual void syncAllNestedModel(const QStringList & relation);

};

} // namespace model_view

* Fichier blog.model_view.gen.cpp :
namespace model_view {

blog_model::blog_model(QObject * parent /* = 0 */) : blog_model_base_class(parent) { ; }

blog_model::blog_model(qx::IxModel * other, QObject * parent) : blog_model_base_class(other, parent) { ; }

blog_model::~blog_model() { ; }

QObject * blog_model::author(int row, bool bLoadFromDatabase /* = false */, const QString & sAppendRelations /* = QString() */)
{
   QString sRelation = "author";
   qx::IxModel * pChild = (bLoadFromDatabase ? NULL : this->getChild(row, sRelation));
   if (pChild) { return static_cast<QObject *>(pChild); }

   if ((row < 0) || (row >= this->m_model.count())) { qAssert(false); return NULL; }
   blog_model_base_class::type_ptr ptr = this->m_model.getByIndex(row);
   if (! ptr) { qAssert(false); return NULL; }
   long id = ptr->getblog_id();
   blog::type_author value = ptr->getauthor();

   if (bLoadFromDatabase)
   {
      if (! sAppendRelations.isEmpty() && ! sAppendRelations.startsWith("->") && ! sAppendRelations.startsWith(">>")) { sRelation += "->" + sAppendRelations; }
      else if (! sAppendRelations.isEmpty()) { sRelation += sAppendRelations; }
      blog tmp;
      tmp.setblog_id(id);
      this->m_lastError = qx::dao::fetch_by_id_with_relation(sRelation, tmp);
      if (this->m_lastError.isValid()) { return NULL; }
      value = tmp.getauthor();
      ptr->setauthor(value);
   }

   model_view::author_model * pNewChild = NULL;
   pChild = qx::model_view::create_nested_model_with_type(this, QModelIndex(), value, pNewChild);
   if (pChild) { this->insertChild(row, "author", pChild); }
   return static_cast<QObject *>(pChild);
}

QObject * blog_model::list_of_comment(int row, bool bLoadFromDatabase /* = false */, const QString & sAppendRelations /* = QString() */)
{
   QString sRelation = "list_of_comment";
   qx::IxModel * pChild = (bLoadFromDatabase ? NULL : this->getChild(row, sRelation));
   if (pChild) { return static_cast<QObject *>(pChild); }

   if ((row < 0) || (row >= this->m_model.count())) { qAssert(false); return NULL; }
   blog_model_base_class::type_ptr ptr = this->m_model.getByIndex(row);
   if (! ptr) { qAssert(false); return NULL; }
   long id = ptr->getblog_id();
   blog::type_list_of_comment value = ptr->getlist_of_comment();

   if (bLoadFromDatabase)
   {
      if (! sAppendRelations.isEmpty() && ! sAppendRelations.startsWith("->") && ! sAppendRelations.startsWith(">>")) { sRelation += "->" + sAppendRelations; }
      else if (! sAppendRelations.isEmpty()) { sRelation += sAppendRelations; }
      blog tmp;
      tmp.setblog_id(id);
      this->m_lastError = qx::dao::fetch_by_id_with_relation(sRelation, tmp);
      if (this->m_lastError.isValid()) { return NULL; }
      value = tmp.getlist_of_comment();
      ptr->setlist_of_comment(value);
   }

   model_view::comment_model * pNewChild = NULL;
   pChild = qx::model_view::create_nested_model_with_type(this, QModelIndex(), value, pNewChild);
   if (pChild) { this->insertChild(row, "list_of_comment", pChild); }
   return static_cast<QObject *>(pChild);
}

QObject * blog_model::list_of_category(int row, bool bLoadFromDatabase /* = false */, const QString & sAppendRelations /* = QString() */)
{
   QString sRelation = "list_of_category";
   qx::IxModel * pChild = (bLoadFromDatabase ? NULL : this->getChild(row, sRelation));
   if (pChild) { return static_cast<QObject *>(pChild); }

   if ((row < 0) || (row >= this->m_model.count())) { qAssert(false); return NULL; }
   blog_model_base_class::type_ptr ptr = this->m_model.getByIndex(row);
   if (! ptr) { qAssert(false); return NULL; }
   long id = ptr->getblog_id();
   blog::type_list_of_category value = ptr->getlist_of_category();

   if (bLoadFromDatabase)
   {
      if (! sAppendRelations.isEmpty() && ! sAppendRelations.startsWith("->") && ! sAppendRelations.startsWith(">>")) { sRelation += "->" + sAppendRelations; }
      else if (! sAppendRelations.isEmpty()) { sRelation += sAppendRelations; }
      blog tmp;
      tmp.setblog_id(id);
      this->m_lastError = qx::dao::fetch_by_id_with_relation(sRelation, tmp);
      if (this->m_lastError.isValid()) { return NULL; }
      value = tmp.getlist_of_category();
      ptr->setlist_of_category(value);
   }

   model_view::category_model * pNewChild = NULL;
   pChild = qx::model_view::create_nested_model_with_type(this, QModelIndex(), value, pNewChild);
   if (pChild) { this->insertChild(row, "list_of_category", pChild); }
   return static_cast<QObject *>(pChild);
}

void blog_model::syncNestedModel(int row, const QStringList & relation)
{
   Q_UNUSED(relation);
   qx::IxModel * pNestedModel = NULL;
   if ((row < 0) || (row >= this->m_model.count())) { return; }
   blog_model_base_class::type_ptr ptr = this->m_model.getByIndex(row);
   if (! ptr) { return; }

   pNestedModel = this->getChild(row, "author");
   if (pNestedModel)
   {
      this->syncNestedModelRecursive(pNestedModel, relation);
      blog::type_author value;
      qx::model_view::sync_nested_model(pNestedModel, value);
      ptr->setauthor(value);
   }

   pNestedModel = this->getChild(row, "list_of_comment");
   if (pNestedModel)
   {
      this->syncNestedModelRecursive(pNestedModel, relation);
      blog::type_list_of_comment value;
      qx::model_view::sync_nested_model(pNestedModel, value);
      ptr->setlist_of_comment(value);
   }

   pNestedModel = this->getChild(row, "list_of_category");
   if (pNestedModel)
   {
      this->syncNestedModelRecursive(pNestedModel, relation);
      blog::type_list_of_category value;
      qx::model_view::sync_nested_model(pNestedModel, value);
      ptr->setlist_of_category(value);
   }
}

void blog_model::syncAllNestedModel(const QStringList & relation)
{
   if (this->m_lstChild.count() <= 0) { return; }
   for (long l = 0; l < this->m_model.count(); l++)
   { this->syncNestedModel(static_cast<int>(l), relation); }
}

} // namespace model_view


Remarque : comme on peut le constater sur l'exemple ci-dessus, le code source à écrire pour travailler avec des modèles imbriqués est verbeux. Afin de travailler avec les relations, il est donc fortement recommandé d'utiliser l'application QxEntityEditor afin de générer le code source automatiquement.

Intéraction avec les vues QML

Voici un exemple en QML (en Qt5, le module QxModelView étant également compatible avec Qt4) qui utilise la table 'author' définie dans le tutoriel qxBlog (le code source de cet exemple QML est disponible dans le projet de test qxBlogModelView présent dans le package QxOrm) :

// 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

Remarque : 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.

Autre remarque : un plugin de l'application 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 (pour plus de détails sur la notion de modèles imbriqués, rendez-vous sur ce tutoriel du site developpez.com).

Intéraction avec les vues QtWidget

Voici un 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 (le code source de cet exemple est disponible dans le projet de test qxBlogModelView présent dans le package QxOrm) :

   // 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

Remarque : Qt propose par défaut plusieurs vues QtWidget qui peuvent être mappées sur un modèle, par exemple : QListView, QTableView, QTreeView. Il est également possible d'utiliser la classe QDataWidgetMapper pour créer ses propres formulaires basés sur des modèles (un tutoriel est disponible sur le site developpez.com).

Connexion d'un modèle au module QxService

Le module QxModelView fournit la classe template : qx::QxModelService<T, S> (qui hérite de : qx::QxModel<T> >> qx::IxModel >> QAbstractItemModel). Cette classe dispose de 2 paramètres template :
  • T : classe enregistrée dans le contexte QxOrm dont toutes les propriétés sont exposées au moteur model/view de Qt ;
  • S : classe de services du module QxService pour accéder/enregistrer les données du modèle (requêtes client/serveur).
Les données proposées par le modèle sont ainsi issues de requêtes client/serveur grâce au module QxService (elles ne proviennent pas directement de la base de données). La classe de services S doit proposer les méthodes suivantes :
  • count() : requête client/serveur pour compter le nombre d'éléments (avec possibilité d'utiliser un filtre SQL) ;
  • fetchById() : requête client/serveur pour alimenter les propriétés du modèle en fonction de son identifiant ;
  • fetchAll() : requête client/serveur pour alimenter les propriétés du modèle qui contiendra tous les éléments d'une table ;
  • fetchByQuery() : requête client/serveur pour alimenter les propriétés du modèle qui contiendra les éléments filtrés par une requête SQL ;
  • insert() : requête client/serveur pour insérer les données du modèle ;
  • update() : requête client/serveur pour mettre à jour les données du modèle ;
  • save() : requête client/serveur pour sauvegarder les données du modèle (insertion ou mise à jour) ;
  • deleteById() : requête client/serveur pour supprimer un modèle en fonction de son identifiant ;
  • deleteAll() : requête client/serveur pour supprimer tous les éléments de la table mappée au modèle ;
  • deleteByQuery() : requête client/serveur pour supprimer tous les éléments en fonction d'une requête SQL ;
  • destroyById() : requête client/serveur pour supprimer un modèle en fonction de son identifiant (avec prise en compte de la suppression logique) ;
  • destroyAll() : requête client/serveur pour supprimer tous les éléments de la table mappée au modèle (avec prise en compte de la suppression logique) ;
  • destroyByQuery() : requête client/serveur pour supprimer tous les éléments en fonction d'une requête SQL (avec prise en compte de la suppression logique) ;
  • executeQuery() : requête client/serveur pour exécuter une requête SQL personnalisée ou procédure stockée ;
  • exist() : requête client/serveur pour tester l'existence du modèle en fonction de son identifiant ;
  • isValid() : requête client/serveur pour tester la validité du modèle (module QxValidator).

Remarque : l'application QxEntityEditor est livrée avec les plugins QxEECppServicesExport et QxEECppModelViewExport : ces plugins génèrent automatiquement tout le code source nécessaire pour travailler avec des modèles qui utilisent le module QxService. Afin de travailler avec la classe qx::QxModelService<T, S>, il est donc fortement recommandé d'utiliser l'application QxEntityEditor afin de générer le code source automatiquement.

QxOrm et MongoDB (C++ ODM Object Document Mapper)

En plus des bases de données relationnelles classiques (MySQL, PostgreSQL, SQLite, Oracle, Microsoft SQLServer, MariaDB, etc...), la bibliothèque QxOrm supporte également la base de données MongoDB.

Définition du site Wikipedia : MongoDB est un système de gestion de base de données orientée documents, répartissable sur un nombre quelconque d'ordinateurs et ne nécessitant pas de schéma prédéfini des données. Il fait partie de la mouvance NoSQL.

La base de données MongoDB présente de nombreux avantages par rapport à une base de données relationnelle classique (liste non exhaustive) :
  • Aucun schéma à définir : il est inutile d'avoir à maintenir des tables et colonnes (donc fini les scripts complexes pour migrer la base de données d'une version à une autre). Les Collections MongoDB peuvent contenir des Documents avec différents champs, différentes tailles, etc... Concernant QxOrm, ça signifie que vous pouvez faire évoluer vos classes C++ sans vous soucier d'un schéma DDL à maintenir d'une version à une autre (convient parfaitement dans un environnement de développement AGILE par exemple) ;
  • Les données sont stockées dans un format BSON (correspond à du JSON) : facile à lire même avec des structures de données complexes ;
  • Moteur de requêtes (JSON) très puissant avec possibilité de positionner des index sur n'importe qu'elle propriété d'un Document ;
  • La base de données est gratuite, et propose un support pour les professionnels ;
  • Depuis la version 3.6 de MongoDB : le moteur de requêtes permet de créer des jointures (entre Documents) de manière équivalente à une base de données relationnelle classique.

L'utilisation de la bibliothèque QxOrm avec MongoDB est très proche des bases de données relationnelles classiques. Toutes les fonctionnalités de la bibliothèque QxOrm sont supportées avec MongoDB : donc tout ce qui se trouve dans ce manuel utilisateur est disponible avec une base de données MongoDB. Les principales différences à prendre en compte sont :
Remarque : le package QxOrm fournit un projet de test nommé qxBlogMongoDB (dans le dossier ./test/). Ce projet montre comment se connecter à une base de données MongoDB avec la bibliothèque QxOrm.

Pré-requis : driver libmongoc et libbson

Le module QtSql fourni par le framework Qt sur lequel est basé la bibliothèque QxOrm ne propose pas de connecteur à une base de données MongoDB. La bibliothèque QxOrm a donc besoin de 2 dépendances supplémentaires pour se connecter à une base MongoDB :
Un guide d'installation est disponible pour installer ces 2 bibliothèques sur votre environnement de développement.

Paramétrage du fichier QxOrm.pri (ou QxOrm.cmake)

Une fois que les bibliothèques libmongoc et libbson sont correctement installées sur votre environnement de développement, il est nécessaire de paramétrer le fichier de configuration QxOrm.pri (ou QxOrm.cmake) en activant l'option de compilation _QX_ENABLE_MONGODB.

#######################################
# MongoDB Driver Library Dependencies #
#######################################

# If you enable _QX_ENABLE_MONGODB option, then QxOrm library will be able to use mongoc driver to store all QxOrm registered classes in a MongoDB database
# When _QX_ENABLE_MONGODB compilation option is defined, you must provide following paths to manage mongoc library dependencies :
#  - a BSON_INCLUDE environment variable to define where bson library source code is located (or a QX_BSON_INCLUDE_PATH qmake variable)
#  - a MONGOC_INCLUDE environment variable to define where mongoc library source code is located (or a QX_MONGOC_INCLUDE_PATH qmake variable)
#  - a BSON_LIB environment variable to define where bson library is located (or a QX_BSON_LIB_PATH qmake variable)
#  - a MONGOC_LIB environment variable to define where mongoc library is located (or a QX_MONGOC_LIB_PATH qmake variable)


Remarque : une fois l'option de compilation _QX_ENABLE_MONGODB activée, il est possible de compiler et exécuter le projet de test qxBlogMongoDB du dossier ./test/ afin de valider l'environnement de développement.

Connexion à la base de données MongoDB

Voici un exemple de paramétrage pour se connecter à une base de données MongoDB :

// Parameters to connect to MongoDB database
qx::QxSqlDatabase * pDatabase = qx::QxSqlDatabase::getSingleton();
pDatabase->setDriverName("QXMONGODB");
pDatabase->setDatabaseName("qxBlog");
pDatabase->setHostName("localhost");
pDatabase->setPort(27017);
pDatabase->setUserName("");
pDatabase->setPassword("");


Définition d'une classe persistante MongoDB (Collection) dans le contexte QxOrm (mapping)

Déclarer une classe persistante MongoDB dans le contexte QxOrm est équivalent à déclarer une classe persistante pour une base de données relationnelle classique. Voici un exemple du projet de test qxBlogMongoDB :

Fichier blog.h :
#ifndef _QX_BLOG_BLOG_H_
#define _QX_BLOG_BLOG_H_

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

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

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

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

#endif // _QX_BLOG_BLOG_H_

Fichier blog.cpp :
#include "../include/precompiled.h"
#include "../include/blog.h"
#include <QxOrm_Impl.h>

QX_REGISTER_CPP_QX_BLOG(blog)

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

   t.data(& blog::m_text, "blog_text");
   t.data(& blog::m_dt_creation, "date_creation");
   t.data(& blog::m_categoryX, "list_category"); // Embedded relationship

   t.relationManyToOne(& blog::m_author, "author_id"); // Referenced relationship
   t.relationOneToMany(& blog::m_commentX, "list_comment", "blog_id"); // Referenced relationship
}}

Remarque : l'exemple ci-dessus montre comment définir :

Gestion des clés primaires ObjectId

Comme indiqué en jaune dans l'exemple précédent, il est conseillé de définir en C++ une clé primaire de type QString. Il n'y a pas de notion de clé numérique auto-incrémentée : MongoDB utilise un type ObjectId qui peut être mappé en C++ avec QString et généré automatiquement (il est également possible de définir son propre type C++ pour mapper un ObjectId MongoDB).

Insérer une instance C++ (Document) dans la base de données MongoDB (INSERT)

Voici un exemple d'insertion de document avec génération automatique de la clé primaire (de type MongoDB ObjectId) :

// Insert one author without id
author_ptr author_1 = std::make_shared<author>();
author_1->m_name = "author_1";
author_1->m_sex = author::male;
author_1->m_birthdate = QDate(1998, 07, 12);
daoError = qx::dao::insert(author_1);


Voici un exemple d'insertion de document avec une clé primaire personnalisée :

// Insert one author with a custom id
author_ptr author_2 = std::make_shared<author>();
author_2->m_id = "my_custom_id_author_2";
author_2->m_name = "author_2";
author_2->m_sex = author::female;
author_2->m_birthdate = QDate(2003, 02, 28);
daoError = qx::dao::insert(author_2);


Insérer une liste d'instances C++ (plusieurs Documents) dans la base de données MongoDB (INSERT)

Voici un exemple d'insertion de plusieurs documents dans la base de données MongoDB :

// Insert many authors with/without ids
QList<author> authorX;
author author_3; author_3.m_name = "author_3"; author_3.m_sex = author::female; author_3.m_birthdate = QDate(1968, 05, 01);
author author_4; author_4.m_id = "my_custom_id_author_4"; author_4.m_name = "author_4"; author_4.m_sex = author::male;
author author_5; author_5.m_name = "author_5"; author_5.m_sex = author::female; author_5.m_birthdate = QDate(1978, 03, 03);
authorX.append(author_3); authorX.append(author_4); authorX.append(author_5);
daoError = qx::dao::insert(authorX);


Remarque : QxOrm supporte plusieurs types C++ de listes / collections.

Mettre à jour une instance C++ (Document) dans la base de données MongoDB (UPDATE)

Voici un exemple de mise à jour d'un document dans la base MongoDB :

// Update one author
author author_4;
author_4.m_id = "my_custom_id_author_4";
author_4.m_name = "author_4_modified";
daoError = qx::dao::update(author_4);


Mettre à jour une liste d'instances C++ (plusieurs Documents) dans la base de données MongoDB (UPDATE)

Voici un exemple de mise à jour de plusieurs documents dans la base MongoDB :

// Update many authors
QList<author> authorX;
author_3.m_name = "author_3_modified_twice"; authorX.append(author_3);
author_2->m_name = "author_2_modified"; authorX.append(* author_2);
author_1->m_name = "author_1_modified"; authorX.append(* author_1);
daoError = qx::dao::update(authorX);


Remarque : QxOrm supporte plusieurs types C++ de listes / collections.

Supprimer une instance C++ (Document) de la base de données MongoDB (DELETE)

Voici un exemple de suppression d'un document de la base MongoDB :

// Delete one author by id
author_ptr pAuthor = std::make_shared<author>();
pAuthor->m_id = "my_custom_id_author_4";
daoError = qx::dao::delete_by_id(pAuthor);


Supprimer une liste d'instances C++ (plusieurs Documents) de la base de données MongoDB (DELETE)

Voici un exemple de suppression de plusieurs documents de la base MongoDB par identifiant (clé primaire) :

// Delete many authors by id
QList<author> authorX;
author_3.m_id = "id_author_3"; authorX.append(author_3);
author_2->m_id = "id_author_2"; authorX.append(* author_2);
author_1->m_id = "id_author_1"; authorX.append(* author_1);
daoError = qx::dao::delete_by_id(authorX);


Voici un exemple de suppression de plusieurs documents de la base MongoDB par requête JSON :

// Delete authors by query (all male)
qx_query query{ { "sex", author::male } };
daoError = qx::dao::delete_by_query<author>(query);


Pour supprimer tous les documents de la Collection author :

// Delete all authors
daoError = qx::dao::delete_all<author>();


Remarque : QxOrm supporte plusieurs types C++ de listes / collections.

Récupérer une instance C++ (Document) de la base de données MongoDB (FETCH)

Voici un exemple pour récupérer (FETCH) un document de la base MongoDB par identifiant (clé primaire) :

// Fetch one author by id
author_ptr pAuthor = std::make_shared<author>();
pAuthor->m_id = "my_custom_id_author_2";
daoError = qx::dao::fetch_by_id(pAuthor);


Récupérer une liste d'instances C++ (plusieurs Documents) de la base de données MongoDB (FETCH)

Voici un exemple pour récupérer (FETCH) plusieurs documents de la base MongoDB par identifiant (clé primaire) :

// Fetch many authors by id
QList<author> authorX;
author_3.m_id = "id_author_3"; authorX.append(author_3);
author_2->m_id = "id_author_2"; authorX.append(* author_2);
author_1->m_id = "id_author_1"; authorX.append(* author_1);
daoError = qx::dao::fetch_by_id(authorX);


Voici un exemple pour récupérer (FETCH) plusieurs documents de la base MongoDB par requête JSON :

// Fetch many authors by query (only female)
list_author list_of_female_author;
qx_query query{ { "sex", author::female } };
daoError = qx::dao::fetch_by_query(query, list_of_female_author);


Voici un exemple pour récupérer (FETCH) tous les documents de la Collection author de la base MongoDB :

// Fetch all authors
list_author allAuthors;
daoError = qx::dao::fetch_all(allAuthors);


Voici un exemple pour récupérer (FETCH) tous les documents de la Collection author de la base MongoDB (en sélectionnant les propriétés/colonnes à récupérer) :

// Fetch all authors (with only 'date_creation' and 'name' properties)
list_author allAuthors;
QStringList columns = QStringList() << "date_creation" << "name";
daoError = qx::dao::fetch_all(allAuthors, NULL, columns);


Remarque : QxOrm supporte plusieurs types C++ de listes / collections.

Requêtes JSON

La principale différence entre une base de données relationnelle classique et MongoDB est la façon de requêter les données : à la place du SQL, MongoDB propose un moteur de requête JSON.

Utilisation de la classe qx::QxSqlQuery (ou son alias qx_query)

La classe qx::QxSqlQuery (ou son alias qx_query) utilisée pour construire du SQL classique est également compatible pour construire des requêtes JSON MongoDB. Cette classe utilise la fonctionnalité C++11 std::initializer_list afin d'écrire les requêtes en C++ avec une syntaxe proche du JSON (il est également possible d'écrire la requête sous forme de chaîne de caractères). Par exemple :

// Fetch many authors by query (only female)
list_author list_of_female_author;
qx_query query { { "sex", author::female } };
daoError = qx::dao::fetch_by_query(query, list_of_female_author);


Utiliser le moteur d'aggregation MongoDB

La base de données MongoDB fournit également un puissant moteur d'aggregation qui étend encore plus les possibilités pour requêter les données. Voici comment utiliser ce moteur d'aggregation avec la classe qx::QxSqlQuery (ou son alias qx_query), le 1er paramètre du constructeur doit être égal à aggregate :

// Fetch by query using MongoDB aggregation framework (only female)
list_author list_of_female_author;
qx_query queryAggregate("aggregate",
               "[ { \"$match\" : { \"sex\" : " + QString::number(static_cast<int>(author::female)) + " } } ]");
daoError = qx::dao::fetch_by_query(queryAggregate, list_of_female_author);


Ajouter des propriétés à la requête de type : 'sort', 'limit', 'skip', etc...

Il est souvent nécessaire de limiter les données, ou bien de les trier par exemple. Pour effectuer ces opérations, la base de données MongoDB utilise la notion de projection. Voici un exemple d'utilisation avec la classe qx::QxSqlQuery (ou son alias qx_query), avec une QStringList en constructeur (ou bien 2ème paramètre std::initializer_list) :

// Fetch by query (only female) adding 'sort', 'limit', 'skip', etc... commands (see second query QStringList parameter)
list_of_female_author.clear();
qx_query queryOpts(QStringList() << "{ \"sex\" : " + QString::number(static_cast(author::female)) + " }"
                              << "{ \"sort\" : { \"sex\" : -1 }, \"limit\" : 2 }");
daoError = qx::dao::fetch_by_query(queryOpts, list_of_female_author);


Exécuter une requête personnalisée

Il est possible d'exécuter une requête personnalisée avec la fonction qx::dao::call_query(). Le résultat de la requête peut être facilement convertie en QVariantMap ou bien QList<QVariantMap> (si la requête retourne un curseur) afin de faciliter la lecture des résultats de la requête personnalisée. Voici quelques exemples d'appels de requêtes personnalisées :

// Drop database
qx_query dropDB("{ \"dropDatabase\" : 1 }");
QSqlError daoError = qx::dao::call_query(dropDB);


// Call a custom query and get JSON response as QVariantMap
qx_query customQuery("{ \"find\": \"author\", \"filter\": { } }");
daoError = qx::dao::call_query(customQuery); qAssert(! daoError.isValid());
QString responseCustomQuery = customQuery.response().toString();
QVariantMap responseCustomQueryAsJson;
qx::serialization::json::from_string(responseCustomQueryAsJson, responseCustomQuery);


// Call a custom query with cursor and get JSON response as QList<QVariantMap>
qx_query customQueryCursor("cursor", "{ \"find\": \"author\", \"filter\": { } }");
daoError = qx::dao::call_query(customQueryCursor); qAssert(! daoError.isValid());
QString responseCustomQueryCursor = customQueryCursor.response().toString();
QList<QVariantMap> responseCustomQueryCursorAsJson;
qx::serialization::json::from_string(responseCustomQueryCursorAsJson, responseCustomQueryCursor);


Moteur de relations (nécessite une version MongoDB 3.6 ou +)

Le moteur de relations de la bibliothèque QxOrm est compatible avec la base de données MongoDB (version 3.6 minimale). QxOrm est donc capable de récupérer les données d'un Document sur plusieurs Collections en une seule requête.

Voici un exemple pour récupérer un Document et toutes ses relations sur 1 niveau de profondeur (parent > enfants) :

// Fetch blog with all relations : 'author', 'comment' and 'category' (MongoDB version 3.6+ is required for relationships)
blog_ptr blog = std::make_shared<blog>();
blog->m_id = "id_blog_1";
daoError = qx::dao::fetch_by_id_with_all_relation(blog);


Voici un exemple pour récupérer un Document et toutes ses relations sur 4 niveaux de profondeur (utilisation de la syntaxe *->*->*->*) :

// Fetch blog with many relations using "*->*->*->*" (4 levels of relationships)
blog_ptr blog = std::make_shared<blog>();
blog->m_id = "id_blog_1";
daoError = qx::dao::fetch_by_id_with_relation("*->*->*->*", blog);


Voici un exemple pour récupérer un Document en sélectionnant les relations et propriétés à alimenter (utilisation de la syntaxe { <col_1>, <col_2>, etc... }) :

// Fetch relations defining fields to fetch with syntax { col_1, col_2, etc... }
list_blog lstBlogComplexRelation;
QStringList relations = QStringList() << "{ blog_text }" << "author_id { name, birthdate }" << "list_comment { comment_text } -> blog_id -> *";
daoError = qx::dao::fetch_all_with_relation(relations, lstBlogComplexRelation);


Voici un exemple pour récupérer un Document en sélectionnant les relations et les propriétés à ne pas alimenter (utilisation de la syntaxe -{ <col_1>, <col_2>, etc... }) :

// Fetch relations defining columns to remove before fetching with syntax -{ col_1, col_2, etc... }
list_blog lstBlogComplexRelation2;
QStringList relations = QStringList() << "-{ blog_text }" << "author_id -{ name, birthdate }" << "list_comment -{ comment_text } -> blog_id -> *";
daoError = qx::dao::fetch_all_with_relation(relations, lstBlogComplexRelation2);


Relations : Embedded vs Referenced

Un des points forts de la base de données MongoDB est de pouvoir stocker des structures complexes de données (on n'est pas limité à une structure en tableau table/colonne des bases de données relationnelles classiques). Un Document MongoDB peut donc contenir un objet et plusieurs sous-objets (notion de hiérarchie dans la structure du Document). Inclure un sous-objet dans un même Document présente des avantages (aucune jointure par exemple, donc plus rapide à récupérer) et inconvénients (un même objet peut être dupliqué plusieurs fois dans la base). Il est donc important de réfléchir sur la stratégie à adopter pour stocker les données.

La bibliothèque QxOrm supporte les 2 façons de procéder :
  • Embedded relation : le sous-objet est inclu dans le Document ;
  • Referenced relation : créé une jointure comme dans une base de données relationnelle classique.

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

   t.data(& blog::m_text, "blog_text");
   t.data(& blog::m_dt_creation, "date_creation");
   t.data(& blog::m_categoryX, "list_category"); // Embedded relationship

   t.relationManyToOne(& blog::m_author, "author_id"); // Referenced relationship
   t.relationOneToMany(& blog::m_commentX, "list_comment", "blog_id"); // Referenced relationship
}}


Création automatique des index

La bibliothèque QxOrm fournit une méthode pour générer automatiquement les index (cette fonction peut être appelée en début de programme, par exemple dans le main) :
  • les index liés aux relations entre les Collections (pour optimiser les jointures) ;
  • les index définis par la méthode qx::IxDataMember::setIndex() (dans la fonction qx::register_class()).

// To optimize queries : create automatically indexes based on relationships and properties marked as 'index'
daoError = qx::dao::mongodb::QxMongoDB_Helper::autoCreateIndexes(true);


Serveur web HTTP/HTTPS (module QxHttpServer)

La bibliothèque QxOrm fournit un serveur web compatible HTTP 1.1 autonome (aucune nécessité d'installer une application tierce comme Apache ou Nginx), performant (multi-thread) et simple d'utilisation : il s'agit du module QxHttpServer (basé sur le module QxService).

Le module QxHttpServer supporte de nombreuses fonctionnalités : Combiné avec le module QxRestApi (qui propose une API JSON pour requêter les données persistantes), le module QxHttpServer est particulièrement adapté pour développer des applications web modernes. Par exemple, des applications web de type SPA (Single-Page Applications) avec les célèbres frameworks Javascript comme AngularJS, React, Meteor.js, etc...

Remarque : pour activer le module QxHttpServer, il faut définir l'option de compilation _QX_ENABLE_QT_NETWORK dans le fichier de configuration QxOrm.pri (ou QxOrm.cmake). Cette option de compilation ajoute une dépendance au binaire QtNetwork fourni avec la bibliothèque Qt.

Autre remarque : le package QxOrm est livré avec le projet de test qxBlogRestApi. Ce projet de test est une application web avec de nombreux exemples pour requêter les données persistantes depuis une page web (HTML et Javascript).

Hello World !

Voici le code source d'une application web basée sur le module QxHttpServer (cette application renvoie Hello World ! au navigateur web client) :

#include <QtCore/qcoreapplication.h>
#include <QxOrm.h>

int main(int argc, char * argv[])
{
   QCoreApplication app(argc, argv);

   // HTTP server settings
   qx::service::QxConnect * serverSettings = qx::service::QxConnect::getSingleton();
   serverSettings->setPort(9642); // HTTP server listening port
   serverSettings->setKeepAlive(5000); // Keep-alive connection with client during 5s, then socket is disconnected and thread becomes available for other clients
   serverSettings->setThreadCount(50); // Number of threads waiting for client's requests,
                                                           // which means also how many requests can be handled simultaneously (in parallel) by HTTP server

   // Create a QxOrm HTTP server instance
   qx::QxHttpServer httpServer;

   // Define all HTTP server routes (dispatcher) to handle requests
   // Each callback is executed in a dedicated thread, so QxOrm HTTP server can handle several requests in parallel
   httpServer.dispatch("GET", "/", [](qx::QxHttpRequest & request, qx::QxHttpResponse & response) {
      response.data() = "Hello World !";
   });

   // Start HTTP server
   httpServer.startServer();

   // Start event loop
   return app.exec();
}


Résultat : ouvrir un navigateur web (Chrome, Firefox, Safari, Internet Explorer, Opera, etc...) et aller à l'adresse http://localhost:9642/, l'écran suivant doit apparaitre :

QxHttpServer Hello World !

Paramétrage du serveur web HTTP

Les paramètres du serveur web HTTP sont accessibles avec la classe singleton qx::service::QxConnect :
  • setPort() : port d'écoute du serveur web (un serveur web classique écoute sur le port 80 mais vous pouvez définir une autre valeur) ;
  • setThreadCount() : nombre de threads utilisés par le serveur web pour traiter les requêtes HTTP (nombre de connexions clientes simultanées) ;
  • setMaxWait() : temps d'attente maximum en milli-secondes (par exemple lecture ou écriture sur la socket), la valeur -1 signifie attendre indéfiniment ;
  • setCompressData() : si le client HTTP supporte la compression GZIP, alors les réponses de type texte (fichier HTML / Javascript / CSS, flux JSON, etc...) seront compressés au format GZIP ;
  • setKeepAlive() : la socket avec le client reste connectée pendant le laps de temps défini par cette fonction (en milli-secondes), la valeur -1 signifie ne jamais se déconnecter ;
  • setSessionTimeOut() : indique le temps d'attente (en milli-secondes) avant de détruire une session (stockage par client côté serveur) non utilisée.

Connexions sécurisées SSL/TLS

La classe singleton qx::service::QxConnect fournit également des paramètres pour gérer les connexions sécurisées HTTPS (SSL et/ou TLS).
Voici un exemple de paramétrage de connexions sécurisées avec certificat serveur et autorité de certification (voir le projet de test qxBlogRestApi pour tester ce code) :

// Certificates created with this tutorial : https://deliciousbrains.com/ssl-certificate-authority-for-local-https-development/
QFile::copy(":/documents/cert_qxorm_ca.pem", appPath.filePath("files/cert_qxorm_ca.pem"));
QFile::copy(":/documents/cert_qxorm_server.crt", appPath.filePath("files/cert_qxorm_server.crt"));
QFile::copy(":/documents/cert_qxorm_server.key", appPath.filePath("files/cert_qxorm_server.key"));

QFile fileCertCA(appPath.filePath("files/cert_qxorm_ca.pem"));
fileCertCA.open(QIODevice::ReadOnly);
QList<QSslCertificate> certCA; certCA << QSslCertificate(fileCertCA.readAll());

QFile fileCertServerPublic(appPath.filePath("files/cert_qxorm_server.crt"));
fileCertServerPublic.open(QIODevice::ReadOnly);
QSslCertificate certServerPublic(fileCertServerPublic.readAll());

QFile fileCertServerPrivate(appPath.filePath("files/cert_qxorm_server.key"));
fileCertServerPrivate.open(QIODevice::ReadOnly);
QSslKey certServerPrivate(fileCertServerPrivate.readAll(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, "qxorm");

qx::service::QxConnect * serverSettings = qx::service::QxConnect::getSingleton();
serverSettings->setSSLEnabled(true);
serverSettings->setSSLCACertificates(certCA);
serverSettings->setSSLLocalCertificate(certServerPublic);
serverSettings->setSSLPrivateKey(certServerPrivate);


Remarque : par défaut, toutes les erreurs SSL sont ignorées (erreurs souvent liées à des problèmes de certificats). Pour adapter votre serveur web à vos normes de sécurité, vous pouvez utiliser les fonctions suivantes (classe singleton qx::service::QxConnect) :

Routage des URL (définir les endpoints / dispatcher)

Le module QxHttpServer fournit un moteur de routage des URL (dispatcher) pour définir les fonctions (ou lambda) à exécuter en fonction des paramètres de la requête HTTP (méthode HTTP GET, POST, DELETE, etc... + URL demandée).
Les fonctions (ou lambda) doivent avoir cette signature : void myRequestHandler(qx::QxHttpRequest & request, qx::QxHttpResponse & response);

La classe qx::QxHttpServer (ou son alias qx_http_server) dispose des méthodes suivantes :
  • setCustomRequestHandler() : défini une fonction (ou lambda) exécutée si aucune autre fonction n'a été trouvée par le dispatcher ;
  • dispatch() : le 1er paramètre correspond à la méthode HTTP (GET, POST, DELETE, etc...), le 2ème paramètre correspond à l'URL demandée (ou son pattern), le 3ème paramètre correpond à la fonction (ou lambda) à exécuter ;
  • beforeDispatching() : fonction (ou lambda) exécutée avant de traiter la requête HTTP (peut être utile par exemple pour tracer des logs, ou bien mettre en place un système d'authentification) ;
  • afterDispatching() : fonction (ou lambda) exécutée après le traitement de la requête HTTP (peut être utile par exemple pour tracer des logs) ;
  • clearDispatcher() : néttoie toutes les règles de routage (seule la fonction ou lambda définie par setCustomRequestHandler() sera exécutée).
Remarque : le dispatcher est thread-safe, vous pouvez donc redéfinir les règles de routage des URL même si le serveur web est en cours d'exécution.

Autre remarque : chaque fonction (ou lambda) est exécutée dans son propre thread. Le serveur web HTTP fourni par la bibliothèque QxOrm peut donc gérer plusieurs requêtes HTTP simultanément.

Exemple n°1 : cette règle de routage intercepte toutes les requêtes de type GET dont l'URL commence par /files/, et renvoie comme réponse le contenu d'un fichier statique stocké sur le serveur (QDir::currentPath() indique le répertoire parent du stockage des fichiers statiques, et 5000 correspond à la taille pour l'envoi des fichiers par bloc / chunked response, ce dernier paramètre étant optionnel) :

httpServer.dispatch("GET", "/files/*", [](qx::QxHttpRequest & request, qx::QxHttpResponse & response) {
   qx::QxHttpServer::buildResponseStaticFile(request, response, QDir::currentPath(), 5000);
});


Exemple n°2 : cette règle de routage intercepte toutes les requêtes de type POST dont l'URL est /qx, et utilise le module QxRestApi (qui propose une API JSON pour requêter les données persistantes). Ces 2 exemples (avec l'exemple n°1 qui renvoie des fichiers statiques) sont suffisants pour démarrer une application web de type SPA (Single-Page Applications) avec les célèbres frameworks Javascript comme AngularJS, React, Meteor.js, etc...

httpServer.dispatch("POST", "/qx", [](qx::QxHttpRequest & request, qx::QxHttpResponse & response) {
   qx::QxHttpServer::buildResponseQxRestApi(request, response);
});


Exemple n°3 : cette règle de routage intercepte toutes les requêtes de type GET dont l'URL est /test_big_json, et construit une réponse de type JSON contenant un tableau de 10000 éléments :

httpServer.dispatch("GET", "/test_big_json", [](qx::QxHttpRequest & request, qx::QxHttpResponse & response) {
   // To compare with this benchmark : https://blog.binaryspaceship.com/2017/cpp-rest-api-frameworks-benchmark/
   // This is more a JSON benchmark than HTTP server benchmark (RapidJSON is faster than Qt QJson engine)
   QJsonArray arr; Q_UNUSED(request);
   for (int i = 0; i < 10000; ++i)
   {
      QJsonObject item;
      item.insert("id", QString::number(i));
      item.insert("name", QString("Hello World"));
      item.insert("type", QString("application"));
      arr.append(item);
   }
   QJsonDocument doc(arr);
   response.headers().insert("Content-Type", "application/json; charset=utf-8");
   response.data() = doc.toJson(QJsonDocument::Compact);
});


Remarque : l'ordre dans lequel est appelé les différentes fonctions dispatch() est important. Le 1er élément trouvé par le dispatcher correspondant à l'URL demandée est exécuté (et ignore les éléments suivants). Il est donc indispensable de définir en 1er les URL les plus spécifiques, jusqu'aux URL les plus génériques (par exemple, le pattern correspondant à toutes les URL est /*).

Routage dynamique des URL

Le dispatcher du module QxHttpServer supporte également la notion de routage dynamique des URL.
Il est possible de définir des variables au niveau de l'URL à router avec la syntaxe : <var_name:var_type> (var_type étant optionnel, et peut prendre comme valeur : int, long, float, double, string).

Le routage dynamique est particulièrement utile pour mettre en place une API REST.
Par exemple, le pattern /blog/<blog_id:int> associé à la méthode HTTP GET peut être utilisé pour récupérer les données d'un blog en fonction de son identifiant de type numérique (fetch_by_id).

Il faut voir l'URL comme une liste de segments dont le séparateur est le caractère /.
Le dispatcher vérifie que chaque segment de l'URL demandée correspond au pattern utilisé afin de valider une fonction (ou lambda) à exécuter.
Pour récupérer les valeurs des paramètres de l'URL, il faut utiliser : request.dispatchParams().value("var_name") (retourne un QVariant).

Exemple : la règle de routage suivante intercepte toutes les requêtes de type GET dont l'URL commence /params/, suivi par un segment qui sera associé à la variable var1, suivi par un segment de type numérique associé à la variable var2. Elle construit une réponse qui renvoie la valeur des 2 paramètres var1 et var2. Si le navigateur web appelle l'URL /params/abc/123/ alors la fonction (ou lambda) sera exécutée, par contre si le navigateur web appelle l'URL /params/abc/def/ alors la fonction (ou lambda) ne sera pas exécutée (car def n'est pas numérique) et cherchera un autre élément du dispatcher :

httpServer.dispatch("GET", "/params/<var1>/<var2:int>", [](qx::QxHttpRequest & request, qx::QxHttpResponse & response) {
   response.data() = "Test URL dispatch parameters :\r\n";
   response.data() += " - var1 = " + request.dispatchParams().value("var1").toByteArray() + "\r\n";
   response.data() += " - var2 = " + request.dispatchParams().value("var2").toByteArray() + "\r\n";
});


Remarque : il est également possible de définir une expression régulière pour router les URL avec la syntaxe : <var_name:{my_reg_exp}>.

Récupérer les paramètres de la requête HTTP

La classe qx::QxHttpRequest (ou son alias qx_http_request) contient tous les paramètres d'appel de la requête HTTP :
  • QUrl & url() : URL demandée par le navigateur web client ;
  • QString & command() : méthode HTTP utilisée par le navigateur web client (GET, POST, PUT, DELETE, etc...) ;
  • QString & version() : version HTTP fournie par le navigateur web client (en général HTTP/1.1) ;
  • QByteArray & data() : contenu de la requête HTTP ;
  • QByteArray header(const QByteArray & key) : permet de récupérer la valeur d'un en-tête HTTP fourni par le navigateur web client (par exemple : request.header("Accept-Encoding")) ;
  • QxHttpCookie cookie(const QByteArray & name) : permet de récupérer la valeur d'un cookie HTTP fourni par le navigateur web client ;
  • QString param(const QString & key) : permet de récupérer la valeur d'un paramètre de la requête HTTP (fourni soit dans l'URL, soit dans le contenu si l'en-tête HTTP 'content-type' est 'application/x-www-form-urlencoded') ;
  • QHash<QString, QVariant> & dispatchParams() : liste des paramètres dynamiques de l'URL calculés par le routage/dispatcher ;
  • QString & sourceAddress() : adresse IP du navigateur web client ;
  • long & sourcePort() : port utilisé par le navigateur web client ;
  • QString guid() : identifiant unique de la requête HTTP à usage interne uniquement (peut être utilisé pour tracer des logs par exemple).

Générer la réponse HTTP

La classe qx::QxHttpResponse (ou son alias qx_http_response) permet de générer la réponse HTTP avec les méthodes suivantes :
  • int & status() : code retour de la réponse HTTP (par défaut 200) ;
  • QByteArray & data() : contenu de la réponse HTTP ;
  • QByteArray header(const QByteArray & key) : renseigne un en-tête HTTP à envoyer au navigateur web client (par défaut, les en-têtes suivants sont créés : Server, Date, Content-Type et Connection) ;
  • QxHttpCookie cookie(const QByteArray & name) : renseigne un cookie HTTP à envoyer au navigateur web client ;
  • qx_bool writeChunked(const QByteArray & data) : permet d'envoyer le contenu de la réponse par bloc (chunked responses).

Sessions (stockage par client côté serveur)

Les sessions HTTP sont un mécanisme côté serveur web permettant de stocker des données spécifiques à un client. Ces données sont accessibles pendant un laps de temps pour toutes les requêtes envoyées par le client. Lors du 1er accès à une session pour un client, un cookie HTTP contenant un identifiant unique est généré et associé automatiquement à la réponse HTTP. Par la suite, toutes les requêtes HTTP envoyées par le client contiendront automatiquement un cookie HTTP avec l'identifiant unique calculé précédemment. Lorsqu'une session n'est plus utilisée pendant un certain laps de temps, alors elle est détruite automatiquement.

La classe qx::QxHttpSession (ou son alias qx_http_session) représente une session HTTP côté serveur.
Une session est accessible avec le singleton qx::QxHttpSessionManager :

httpServer.dispatch("GET", "/", [](qx::QxHttpRequest & request, qx::QxHttpResponse & response) {
   // If this is the first time to access to session, then a cookie is created automatically and attached to the response
   // Then each request sent by web browser will contain a cookie with the session id
   // The session expires on server side after qx::service::QxConnect::setSessionTimeOut() milliseconds
   qx::QxHttpSession_ptr session = qx::QxHttpSessionManager::getSession(request, response);
   if (session) { session->set("last_request_per_user", QDateTime::currentDateTime()); }
});


Remarque : la classe qx::QxHttpSession contient une hash-map (QHash<QByteArray, QVariant>) pouvant stocker n'importe quelle valeur.

Autre remarque : la durée (en milli-secondes) pour supprimer une session non utilisée est paramétrée par la méthode : qx::service::QxConnect::setSessionTimeOut().

Cookies

Les cookies HTTP sont un mécanisme d'échange de données entre client HTTP et serveur HTTP.
Les cookies sont utilisés par exemple pour :
  • gérer les sessions HTTP ;
  • mémoriser l'information sur l'utilisateur d'un site, dans le but de lui montrer un contenu approprié dans le futur. Par exemple, un serveur web peut envoyer un cookie contenant le dernier nom d'utilisateur utilisé pour se connecter à ce site web, afin que ce nom d'utilisateur puisse être pré-rempli lors des prochaines visites ;
  • voir Wikipedia pour d'autres cas d'utilisation.

Les classes qx::QxHttpRequest et qx::QxHttpResponse disposent des méthodes nécessaires pour lire les cookies envoyés par le navigateur web client ou bien générer un cookie dans la réponse HTTP. Par exemple :

qx::QxHttpCookie cookie;
cookie.name = "my_http_cookie";
cookie.value = "my_value";
response.cookies().insert(cookie.name, cookie);


Gestion des fichiers statiques

La classe qx::QxHttpServer (ou son alias qx_http_server) dispose d'une méthode statique permettant d'envoyer au navigateur web client des fichiers stockés sur le serveur (par exemple : fichiers HTML, Javascript, CSS, images PNG, JPEG, vidéos, etc...).

httpServer.dispatch("GET", "/files/*", [](qx::QxHttpRequest & request, qx::QxHttpResponse & response) {
   qx::QxHttpServer::buildResponseStaticFile(request, response, QDir::currentPath(), 5000);
});

  • Le 3ème paramètre (dans l'exemple QDir::currentPath()) représente le répertoire parent où sont stockés les fichiers statiques sur le serveur web ;
  • Le 4ème paramètre (dans l'exemple 5000) est optionnel et correspond à la taille pour l'envoi des fichiers par bloc (chunked response). Ce paramètre peut être utile pour envoyer des fichiers volumineux (streaming).

Encodage de transfert en bloc (chunked responses)

Définition du site Wikipedia : Chunked transfer encoding (ou Encodage de transfert en bloc) est un mécanisme de transfert de données de la version 1.1 du protocole Hypertext Transfer Protocol (HTTP), qui permet à un serveur ou à un client de commencer à transmettre des données par blocs sans avoir à connaître à l'avance la taille totale des données qui seront transmises. Dans le protocole HTTP, l'en-tête "Content-Length" peut remplacer la directive "Chunked transfer encoding" décrite ici. La taille en octets de chaque bloc est envoyée, sous forme de texte en hexadecimal, juste avant le bloc lui-même afin que le serveur puisse dire au client quand il a fini de recevoir les données de ce bloc. Le transfert total d'un fichier encodé par blocs se termine par un bloc final au contenu nul.

L'introduction de l'encodage de transfert en bloc du protocole HTTP 1.1 a fourni un certain nombre d'avantages :
  • Permettre à un serveur de maintenir une connexion HTTP persistante pour un contenu généré dynamiquement.
  • Permettre à l'expéditeur d'envoyer des en-têtes supplémentaires après le corps du message. Sans l'encodage de transfert en bloc, l'expéditeur devrait tamponner le contenu jusqu'à ce qu'il soit complété afin de calculer une valeur et l'envoyer avant le contenu.

La classe qx::QxHttpResponse dispose de la méthode qx_bool writeChunked(const QByteArray & data). Cette méthode permet d'envoyer la réponse par bloc. Elle est utilisée par exemple pour envoyer des fichiers statiques volumineux :

while (! file.atEnd())
{
   if (! response.writeChunked(file.read(chunkedSize))) { return; }
}


Remarque : le 1er appel de la méthode response.writeChunked() déclenche automatiquement l'envoi de tous les en-têtes HTTP de la réponse. Il faut donc définir tous les en-têtes de la réponse HTTP avant d'appeler response.writeChunked() pour la 1ère fois.

Requêtes par les API JSON (module QxRestApi)

Le module QxRestApi propose une API JSON générique pour requêter les données persistantes (opérations CRUD, requêtes complexes avec plusieurs niveaux de relations, possibilité de définir un format de sortie JSON, appels dynamiques à des fonctions natives C++, validation d'instances, requêtes personnalisées à la base de données).

Ce manuel utilisateur dispose d'un chapitre entier dédié au module QxRestApi : il contient notamment de nombreux exemples d'utilisation. En combinant le module QxRestApi et le module QxHttpServer : vous avez tous les outils nécessaires pour développer des applications web modernes. Par exemple, des applications web de type SPA (Single-Page Applications) avec les célèbres frameworks Javascript comme AngularJS, React, Meteor.js, etc...

Remarque : le package QxOrm contient un projet de test qxBlogRestApi. Ce projet présente la création d'un serveur web HTTP avec QxOrm, et également l'écriture de la partie cliente en HTML + Javascript (avec utilisation de jQuery).

Par exemple, voici la fonction Javascript utilisée pour envoyer les requêtes JSON (méthode POST) depuis le client (navigateur web) vers le serveur web HTTP QxOrm (toutes les requêtes sont envoyées à la même adresse /qx) :

function sendRequest(request) {
   $.post("/qx", request, function(data, status, xhr) {
      $("#txtResponse").val(JSON.stringify(data, null, 3));
   }, "json").fail(function(error) {
      alert("An error occurred sending request to QxOrm HTTP server : " + error);
   });
}


Côté serveur, la réception et le traitement de ces requêtes est très simple : la classe qx::QxHttpServer (ou son alias qx_http_server) dispose de la méthode statique qx::QxHttpServer::buildResponseQxRestApi() :

httpServer.dispatch("POST", "/qx", [](qx::QxHttpRequest & request, qx::QxHttpResponse & response) {
   qx::QxHttpServer::buildResponseQxRestApi(request, response);
});


Voici un exemple de requête JSON envoyée par le navigateur web, elle récupère la liste des tous les blogs de la base de données (fetch_all) :

{
   "request_id": "2b393e4c-a00c-45dc-a279-e9d76f1c55cf",
   "action": "fetch_all",
   "entity": "blog"
}


Voici la réponse JSON renvoyée par le serveur web HTTP contenant la liste de blogs :

{
   "data": [
      {
         "author_id": {
            "author_id": "author_id_2",
            "birthdate": null,
            "list_blog": [],
            "name": "",
            "sex": 2
         },
         "blog_id": 1,
         "blog_text": "blog property 'text' modified => blog is dirty !!!",
         "date_creation": "2019-03-27T20:51:23.107",
         "list_category": [],
         "list_comment": []
      },
      {
         "author_id": {
            "author_id": "author_id_2",
            "birthdate": null,
            "list_blog": [],
            "name": "",
            "sex": 2
         },
         "blog_id": 2,
         "blog_text": "blog property 'text' modified => blog is dirty !!!",
         "date_creation": "2019-03-27T20:51:23.107",
         "list_category": [],
         "list_comment": []
      },
      {
         "author_id": {
            "author_id": "author_id_2",
            "birthdate": null,
            "list_blog": [],
            "name": "",
            "sex": 2
         },
         "blog_id": 3,
         "blog_text": "blog property 'text' modified => blog is dirty !!!",
         "date_creation": "2019-03-27T20:51:23.107",
         "list_category": [],
         "list_comment": []
      },
      {
         "author_id": {
            "author_id": "author_id_2",
            "birthdate": null,
            "list_blog": [],
            "name": "",
            "sex": 2
         },
         "blog_id": 4,
         "blog_text": "blog property 'text' modified => blog is dirty !!!",
         "date_creation": "2019-03-27T20:51:23.107",
         "list_category": [],
         "list_comment": []
      }
   ],
   "request_id": "2b393e4c-a00c-45dc-a279-e9d76f1c55cf"
}


WebSocket

Définition du site Wikipedia : le protocole WebSocket vise à développer un canal de communication full-duplex sur un socket TCP pour les navigateurs et les serveurs web et permet :
  • la notification au client d'un changement d'état du serveur ;
  • l'envoi de données en mode « pousser » (méthode Push) du serveur vers le client (sans que ce dernier ait à effectuer une requête).

La bibliothèque QxOrm est basée sur le framework Qt qui dispose déjà d'une implémentation WebSocket.
La mise en place d'un serveur web avec les WebSockets Qt est très simple : il y a plusieurs exemples dans la documentation Qt.

Il est donc tout à fait possible d'implémenter un serveur web avec :
  • un port d'écoute dédié à toutes les connexions HTTP (utilisant le module QxHttpServer) ;
  • un autre port d'écoute dédié à toutes les connexions WebSockets (utilisant le module QtWebSockets fourni par Qt).

Remarque : une connexion WebSocket étant généralement créée par le code Javascript du navigateur web client, le fait d'avoir 2 ports ouverts sur le serveur web n'est pas un problème.

Performance (testé avec Apache Benchmark)

Voici les résultats d'un test de performance réalisé avec les paramètres suivants :
  • Système d'exploitation : Windows 2010 64bits ;
  • Processeur : Intel Core i7-6820HQ @ 2.70GHz (laptop) ;
  • Version Qt : 5.1.1 (en mode release) ;
  • Version QxOrm : 1.4.6 (compilé en mode release avec Visual Studio 2012, avec les paramètres par défaut, aucune optimisation particulière) ;
  • Serveur web HTTP : projet de test qxBlogRestApi ;
  • Outil de test : Apache Benchmark ;
  • Simulation de 20000 requêtes avec 50 clients connectés simultanément : ab -n 20000 -c 50 -k http://localhost:9642/params/abc/123

Le résultat indique que le serveur web HTTP QxOrm peut gérer plus de 12000 requêtes par seconde :

QxHttpServer performance

Améliorer les performances avec epoll dispatcher sous Linux

Sous Linux, il est possible d'ameliorer significativement les performances du serveur web HTTP en utilisant le mécanisme epoll pour gérer les socket. Par défaut, le framework Qt utilise un autre mécanisme (select) plus lent, mais donne la possibilité de définir une autre gestion d'évènements. Plusieurs bibliothèques existent, par exemple :
La classe qx::QxHttpServer (ou son alias qx_http_server) dispose de la méthode suivante pour définir des évènements basés sur epoll (à appeler avant le démarrage du serveur web) :

   httpServer.setEventDispatcher(new QEventDispatcherEpoll());   


API REST JSON (module QxRestApi)

Le module QxRestApi est une API JSON pour gérer (de façon générique) la couche de données persistantes (base de données) ou appeler des fonctions natives C++ (enregistrées dans le contexte QxOrm). Le module QxRestApi est basé sur un mécanisme requête/réponse : envoi d'une requête au format JSON et réception d'une réponse au format JSON. Le module QxRestApi est particulièrement adapté pour développer des services REST.

Le module QxRestApi supporte les fonctionnalités suivantes :
  • opérations CRUD ;
  • requêtes complexes avec plusieurs niveaux de relations ;
  • possibilité de définir un format de sortie JSON ;
  • appels dynamiques à des fonctions natives C++ ;
  • validation d'instances ;
  • requêtes personnalisées à la base de données ou procédures stockées.

Principe de fonctionnement

Le module QxRestApi est très simple d'utilisation : la classe qx::QxRestApi permet d'utiliser les API JSON avec une seule méthode : processRequest().
Prérequis : pour pouvoir utiliser le module QxRestApi, les classes enregistrées dans le contexte QxOrm doivent implémenter l'interface qx::IxPersistable.

La structure d'une requête JSON est générique et contient les éléments suivants :

{
   "request_id" : // [optional] unique identifier generated by client to associate response to request (if provided by caller, then the response will contain the same unique identifier)
   "action" : // [required] what is the action to execute on the server
   "entity" : // [optional or required depending on action] C++ class registered in QxOrm context
   "data" : // [optional or required depending on action] data in JSON format needed to execute action
   "columns" : // [optional] list of columns to fetch or update (if empty, means all columns)
   "relations" : // [optional] list of relationships to fetch or save
   "query" : // [optional or required depending on action] query to execute on database
   "output_format" : // [optional] output fields for the response (filter), if empty then response will contain all fields
   "fct" : // [required only with action 'call_entity_function'] used to call C++ native functions
   "save_mode" : // [optional] used only with action 'save' to define insert or update or check both insert/update
}


La réponse JSON contient les éléments suivants :

{
   "request_id" : // unique identifier generated by client's request (if any)
   "data" : // contain the response data
   "error" : // if an error occured, then contain a code and description of the error
}


Cas d'utilisation

Plusieurs langages de programmation supporte le JSON nativement (Javascript, PHP, Python, etc...). Le module QxRestApi permet ainsi une interopérabilité entre la bibliothèque QxOrm et d'autres applications utilisant d'autres technologies (autre que C++/Qt par exemple).

Le module QxRestApi peut être utilisé :

Projet de test qxBlogRestApi (QML et serveur web HTTP)

Le package QxOrm est livré avec un projet de test nommé qxBlogRestApi (dans le dossier ./test/qxBlogRestApi/).
Ce projet de test montre 2 cas d'utilisation du module QxRestApi :
  • La 1ère fenêtre correspond à une application QML qui utilise le moteur JS intégré à QML pour requêter les données persistantes ou appeler des fonctions natives C++ :

    QxHttpServer performance


  • La 2ème fenêtre démarre un serveur web HTTP basé sur le module QxHttpServer, puis ouvre le navigateur web par défaut à l'adresse correspondante (HTML + Javascript avec jQuery) :

    QxHttpServer performance

Ces 2 fenêtres sont développées avec 2 technologies différentes (QML versus HTML + Javascript), mais proposent exactement les mêmes fonctionnalités :
  • En haut à gauche de l'écran : zone permettant d'écrire la requête JSON à envoyer au module QxRestApi ;
  • Juste en dessous de la requête JSON : un bouton permettant d'envoyer la requête JSON au module QxRestApi ;
  • En bas à gauche de l'écran : une liste d'exemples de requêtes JSON prêtes à être exécutées (un click dans cette liste alimente automatiquement la requête JSON à envoyer au module QxRestApi) ;
  • A droite de l'écran : la réponse JSON fournie par le module QxRestApi après traitement de la requête.

Récupération de données (fetch/count/exist)

Ce chapitre détaille les différentes méthodes pour récupérer les données issues de la base de données :

fetch_all

L'action fetch_all permet de récupérer tous les éléments d'une table de la base de données (et éventuellement les relations associées sur plusieurs niveaux).

-- Exemple n°1 -- récupérer tous les blogs (sous forme de liste) :

Requête JSON :
{
   "request_id": "5e988bac-c812-4cb1-b0d8-6a2c9dc4478b",
   "action": "fetch_all",
   "entity": "blog"
}
Réponse JSON :
{
   "data": [
      {
         "author_id": {
            "author_id": "author_id_2",
            "birthdate": null,
            "list_blog": [],
            "name": "",
            "sex": 2
         },
         "blog_id": 1,
         "blog_text": "blog property 'text' modified => blog is dirty !!!",
         "date_creation": "2019-04-01T16:18:54",
         "list_category": [],
         "list_comment": []
      },
      {
         "author_id": {
            "author_id": "author_id_2",
            "birthdate": null,
            "list_blog": [],
            "name": "",
            "sex": 2
         },
         "blog_id": 2,
         "blog_text": "blog property 'text' modified => blog is dirty !!!",
         "date_creation": "2019-04-01T16:18:54",
         "list_category": [],
         "list_comment": []
      },
      {
         "author_id": {
            "author_id": "author_id_2",
            "birthdate": null,
            "list_blog": [],
            "name": "",
            "sex": 2
         },
         "blog_id": 3,
         "blog_text": "blog property 'text' modified => blog is dirty !!!",
         "date_creation": "2019-04-01T16:18:54",
         "list_category": [],
         "list_comment": []
      },
      {
         "author_id": {
            "author_id": "author_id_2",
            "birthdate": null,
            "list_blog": [],
            "name": "",
            "sex": 2
         },
         "blog_id": 4,
         "blog_text": "blog property 'text' modified => blog is dirty !!!",
         "date_creation": "2019-04-01T16:18:54",
         "list_category": [],
         "list_comment": []
      }
   ],
   "request_id": "5e988bac-c812-4cb1-b0d8-6a2c9dc4478b"
}


-- Exemple n°2 -- récupérer tous les blogs (sous forme de collection type hash-map avec clé/valeur) :

Requête JSON :
{
   "request_id": "ad400135-19fd-40e0-8034-201be6a2ff7a",
   "action": "fetch_all",
   "entity": "blog",
   "data": [
      {
         "key": "",
         "value": ""
      }
   ]
}
Réponse JSON :
{
   "data": [
      {
         "key": 1,
         "value": {
            "author_id": {
               "author_id": "author_id_2",
               "birthdate": null,
               "list_blog": [],
               "name": "",
               "sex": 2
            },
            "blog_id": 1,
            "blog_text": "blog property 'text' modified => blog is dirty !!!",
            "date_creation": "2019-04-01T16:18:54",
            "list_category": [],
            "list_comment": []
         }
      },
      {
         "key": 2,
         "value": {
            "author_id": {
               "author_id": "author_id_2",
               "birthdate": null,
               "list_blog": [],
               "name": "",
               "sex": 2
            },
            "blog_id": 2,
            "blog_text": "blog property 'text' modified => blog is dirty !!!",
            "date_creation": "2019-04-01T16:18:54",
            "list_category": [],
            "list_comment": []
         }
      },
      {
         "key": 3,
         "value": {
            "author_id": {
               "author_id": "author_id_2",
               "birthdate": null,
               "list_blog": [],
               "name": "",
               "sex": 2
            },
            "blog_id": 3,
            "blog_text": "blog property 'text' modified => blog is dirty !!!",
            "date_creation": "2019-04-01T16:18:54",
            "list_category": [],
            "list_comment": []
         }
      },
      {
         "key": 4,
         "value": {
            "author_id": {
               "author_id": "author_id_2",
               "birthdate": null,
               "list_blog": [],
               "name": "",
               "sex": 2
            },
            "blog_id": 4,
            "blog_text": "blog property 'text' modified => blog is dirty !!!",
            "date_creation": "2019-04-01T16:18:54",
            "list_category": [],
            "list_comment": []
         }
      }
   ],
   "request_id": "ad400135-19fd-40e0-8034-201be6a2ff7a"
}


-- Exemple n°3 -- récupérer tous les blogs et toutes les relations associées sur 2 niveaux :

Requête JSON :
{
   "request_id": "cf9ea2a8-3e41-438f-9a48-bbc8593d2b99",
   "action": "fetch_all",
   "entity": "blog",
   "relations": [
      "*->*"
   ]
}
Réponse JSON :
{
   "data": [
      {
         "author_id": {
            "author_id": "author_id_2",
            "birthdate": "2019-04-01",
            "list_blog": [
               {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": null,
                     "list_blog": [],
                     "name": "",
                     "sex": 2
                  },
                  "blog_id": 1,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [],
                  "list_comment": []
               },
               {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": null,
                     "list_blog": [],
                     "name": "",
                     "sex": 2
                  },
                  "blog_id": 2,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [],
                  "list_comment": []
               },
               {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": null,
                     "list_blog": [],
                     "name": "",
                     "sex": 2
                  },
                  "blog_id": 3,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [],
                  "list_comment": []
               },
               {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": null,
                     "list_blog": [],
                     "name": "",
                     "sex": 2
                  },
                  "blog_id": 4,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [],
                  "list_comment": []
               }
            ],
            "name": "author name modified at index 1 => container is dirty !!!",
            "sex": 1
         },
         "blog_id": 1,
         "blog_text": "blog property 'text' modified => blog is dirty !!!",
         "date_creation": "2019-04-01T16:18:54",
         "list_category": [
            {
               "key": 1,
               "value": {
                  "category_id": 1,
                  "description": "desc_1",
                  "list_blog": [
                     {
                        "key": 1,
                        "value": {
                           "author_id": {
                              "author_id": "author_id_2",
                              "birthdate": null,
                              "list_blog": [],
                              "name": "",
                              "sex": 2
                           },
                           "blog_id": 1,
                           "blog_text": "blog property 'text' modified => blog is dirty !!!",
                           "date_creation": "2019-04-01T16:18:54",
                           "list_category": [],
                           "list_comment": []
                        }
                     }
                  ],
                  "name": "category_1"
               }
            },
            {
               "key": 3,
               "value": {
                  "category_id": 3,
                  "description": "desc_3",
                  "list_blog": [
                     {
                        "key": 1,
                        "value": {
                           "author_id": {
                              "author_id": "author_id_2",
                              "birthdate": null,
                              "list_blog": [],
                              "name": "",
                              "sex": 2
                           },
                           "blog_id": 1,
                           "blog_text": "blog property 'text' modified => blog is dirty !!!",
                           "date_creation": "2019-04-01T16:18:54",
                           "list_category": [],
                           "list_comment": []
                        }
                     }
                  ],
                  "name": "category_3"
               }
            }
         ],
         "list_comment": [
            {
               "blog_id": {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": null,
                     "list_blog": [],
                     "name": "",
                     "sex": 2
                  },
                  "blog_id": 1,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [],
                  "list_comment": []
               },
               "comment_id": 1,
               "comment_text": "comment_1 text",
               "date_creation": "2019-04-01T16:18:54"
            },
            {
               "blog_id": {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": null,
                     "list_blog": [],
                     "name": "",
                     "sex": 2
                  },
                  "blog_id": 1,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [],
                  "list_comment": []
               },
               "comment_id": 3,
               "comment_text": "comment_1 text",
               "date_creation": "2019-04-01T16:18:54"
            },
            {
               "blog_id": {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": null,
                     "list_blog": [],
                     "name": "",
                     "sex": 2
                  },
                  "blog_id": 1,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [],
                  "list_comment": []
               },
               "comment_id": 5,
               "comment_text": "comment_1 text",
               "date_creation": "2019-04-01T16:18:54"
            },
            {
               "blog_id": {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": null,
                     "list_blog": [],
                     "name": "",
                     "sex": 2
                  },
                  "blog_id": 1,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [],
                  "list_comment": []
               },
               "comment_id": 7,
               "comment_text": "comment_1 text",
               "date_creation": "2019-04-01T16:18:54"
            },
            {
               "blog_id": {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": null,
                     "list_blog": [],
                     "name": "",
                     "sex": 2
                  },
                  "blog_id": 1,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [],
                  "list_comment": []
               },
               "comment_id": 2,
               "comment_text": "comment_2 text",
               "date_creation": "2019-04-01T16:18:54"
            },
            {
               "blog_id": {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": null,
                     "list_blog": [],
                     "name": "",
                     "sex": 2
                  },
                  "blog_id": 1,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [],
                  "list_comment": []
               },
               "comment_id": 4,
               "comment_text": "comment_2 text",
               "date_creation": "2019-04-01T16:18:54"
            },
            {
               "blog_id": {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": null,
                     "list_blog": [],
                     "name": "",
                     "sex": 2
                  },
                  "blog_id": 1,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [],
                  "list_comment": []
               },
               "comment_id": 6,
               "comment_text": "comment_2 text",
               "date_creation": "2019-04-01T16:18:54"
            },
            {
               "blog_id": {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": null,
                     "list_blog": [],
                     "name": "",
                     "sex": 2
                  },
                  "blog_id": 1,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [],
                  "list_comment": []
               },
               "comment_id": 8,
               "comment_text": "comment_2 text",
               "date_creation": "2019-04-01T16:18:54"
            }
         ]
      },
      {
         "author_id": {
            "author_id": "author_id_2",
            "birthdate": "2019-04-01",
            "list_blog": [
               {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": null,
                     "list_blog": [],
                     "name": "",
                     "sex": 2
                  },
                  "blog_id": 1,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [],
                  "list_comment": []
               },
               {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": null,
                     "list_blog": [],
                     "name": "",
                     "sex": 2
                  },
                  "blog_id": 2,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [],
                  "list_comment": []
               },
               {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": null,
                     "list_blog": [],
                     "name": "",
                     "sex": 2
                  },
                  "blog_id": 3,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [],
                  "list_comment": []
               },
               {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": null,
                     "list_blog": [],
                     "name": "",
                     "sex": 2
                  },
                  "blog_id": 4,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [],
                  "list_comment": []
               }
            ],
            "name": "author name modified at index 1 => container is dirty !!!",
            "sex": 1
         },
         "blog_id": 2,
         "blog_text": "blog property 'text' modified => blog is dirty !!!",
         "date_creation": "2019-04-01T16:18:54",
         "list_category": [
            {
               "key": 4,
               "value": {
                  "category_id": 4,
                  "description": "desc_1",
                  "list_blog": [
                     {
                        "key": 2,
                        "value": {
                           "author_id": {
                              "author_id": "author_id_2",
                              "birthdate": null,
                              "list_blog": [],
                              "name": "",
                              "sex": 2
                           },
                           "blog_id": 2,
                           "blog_text": "blog property 'text' modified => blog is dirty !!!",
                           "date_creation": "2019-04-01T16:18:54",
                           "list_category": [],
                           "list_comment": []
                        }
                     }
                  ],
                  "name": "category_1"
               }
            },
            {
               "key": 5,
               "value": {
                  "category_id": 5,
                  "description": "desc_3",
                  "list_blog": [
                     {
                        "key": 2,
                        "value": {
                           "author_id": {
                              "author_id": "author_id_2",
                              "birthdate": null,
                              "list_blog": [],
                              "name": "",
                              "sex": 2
                           },
                           "blog_id": 2,
                           "blog_text": "blog property 'text' modified => blog is dirty !!!",
                           "date_creation": "2019-04-01T16:18:54",
                           "list_category": [],
                           "list_comment": []
                        }
                     }
                  ],
                  "name": "category_3"
               }
            }
         ],
         "list_comment": []
      },
      {
         "author_id": {
            "author_id": "author_id_2",
            "birthdate": "2019-04-01",
            "list_blog": [
               {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": null,
                     "list_blog": [],
                     "name": "",
                     "sex": 2
                  },
                  "blog_id": 1,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [],
                  "list_comment": []
               },
               {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": null,
                     "list_blog": [],
                     "name": "",
                     "sex": 2
                  },
                  "blog_id": 2,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [],
                  "list_comment": []
               },
               {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": null,
                     "list_blog": [],
                     "name": "",
                     "sex": 2
                  },
                  "blog_id": 3,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [],
                  "list_comment": []
               },
               {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": null,
                     "list_blog": [],
                     "name": "",
                     "sex": 2
                  },
                  "blog_id": 4,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [],
                  "list_comment": []
               }
            ],
            "name": "author name modified at index 1 => container is dirty !!!",
            "sex": 1
         },
         "blog_id": 3,
         "blog_text": "blog property 'text' modified => blog is dirty !!!",
         "date_creation": "2019-04-01T16:18:54",
         "list_category": [
            {
               "key": 6,
               "value": {
                  "category_id": 6,
                  "description": "desc_1",
                  "list_blog": [
                     {
                        "key": 3,
                        "value": {
                           "author_id": {
                              "author_id": "author_id_2",
                              "birthdate": null,
                              "list_blog": [],
                              "name": "",
                              "sex": 2
                           },
                           "blog_id": 3,
                           "blog_text": "blog property 'text' modified => blog is dirty !!!",
                           "date_creation": "2019-04-01T16:18:54",
                           "list_category": [],
                           "list_comment": []
                        }
                     }
                  ],
                  "name": "category_1"
               }
            },
            {
               "key": 7,
               "value": {
                  "category_id": 7,
                  "description": "desc_3",
                  "list_blog": [
                     {
                        "key": 3,
                        "value": {
                           "author_id": {
                              "author_id": "author_id_2",
                              "birthdate": null,
                              "list_blog": [],
                              "name": "",
                              "sex": 2
                           },
                           "blog_id": 3,
                           "blog_text": "blog property 'text' modified => blog is dirty !!!",
                           "date_creation": "2019-04-01T16:18:54",
                           "list_category": [],
                           "list_comment": []
                        }
                     }
                  ],
                  "name": "category_3"
               }
            }
         ],
         "list_comment": []
      },
      {
         "author_id": {
            "author_id": "author_id_2",
            "birthdate": "2019-04-01",
            "list_blog": [
               {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": null,
                     "list_blog": [],
                     "name": "",
                     "sex": 2
                  },
                  "blog_id": 1,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [],
                  "list_comment": []
               },
               {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": null,
                     "list_blog": [],
                     "name": "",
                     "sex": 2
                  },
                  "blog_id": 2,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [],
                  "list_comment": []
               },
               {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": null,
                     "list_blog": [],
                     "name": "",
                     "sex": 2
                  },
                  "blog_id": 3,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [],
                  "list_comment": []
               },
               {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": null,
                     "list_blog": [],
                     "name": "",
                     "sex": 2
                  },
                  "blog_id": 4,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [],
                  "list_comment": []
               }
            ],
            "name": "author name modified at index 1 => container is dirty !!!",
            "sex": 1
         },
         "blog_id": 4,
         "blog_text": "blog property 'text' modified => blog is dirty !!!",
         "date_creation": "2019-04-01T16:18:54",
         "list_category": [
            {
               "key": 8,
               "value": {
                  "category_id": 8,
                  "description": "desc_1",
                  "list_blog": [
                     {
                        "key": 4,
                        "value": {
                           "author_id": {
                              "author_id": "author_id_2",
                              "birthdate": null,
                              "list_blog": [],
                              "name": "",
                              "sex": 2
                           },
                           "blog_id": 4,
                           "blog_text": "blog property 'text' modified => blog is dirty !!!",
                           "date_creation": "2019-04-01T16:18:54",
                           "list_category": [],
                           "list_comment": []
                        }
                     }
                  ],
                  "name": "category_1"
               }
            },
            {
               "key": 9,
               "value": {
                  "category_id": 9,
                  "description": "desc_3",
                  "list_blog": [
                     {
                        "key": 4,
                        "value": {
                           "author_id": {
                              "author_id": "author_id_2",
                              "birthdate": null,
                              "list_blog": [],
                              "name": "",
                              "sex": 2
                           },
                           "blog_id": 4,
                           "blog_text": "blog property 'text' modified => blog is dirty !!!",
                           "date_creation": "2019-04-01T16:18:54",
                           "list_category": [],
                           "list_comment": []
                        }
                     }
                  ],
                  "name": "category_3"
               }
            }
         ],
         "list_comment": []
      }
   ],
   "request_id": "cf9ea2a8-3e41-438f-9a48-bbc8593d2b99"
}


-- Exemple n°4 -- récupérer tous les blogs et plusieurs relations associées en définissant un format de sortie (toutes les propriétés ne feront pas partie de la réponse JSON) :

Requête JSON :
{
   "request_id": "4c45fdf9-8001-4509-bb4b-ce27a4a8708a",
   "action": "fetch_all",
   "entity": "blog",
   "relations": [
      "<blog_alias> { blog_text }",
      "author_id <author_alias> { name, birthdate }",
      "list_comment <list_comment_alias> { comment_text } -> blog_id <blog_alias_2> -> * <..._my_alias_suffix>"
   ],
   "output_format": [
      "{ blog_text }",
      "author_id { name, birthdate }",
      "list_comment { comment_text } -> blog_id -> *"
   ]
}
Réponse JSON :
{
   "data": [
      {
         "author_id": {
            "author_id": "author_id_2",
            "birthdate": "2019-04-01",
            "name": "author name modified at index 1 => container is dirty !!!"
         },
         "blog_id": 1,
         "blog_text": "blog property 'text' modified => blog is dirty !!!",
         "list_comment": [
            {
               "blog_id": {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": "2019-04-01",
                     "name": "author name modified at index 1 => container is dirty !!!",
                     "sex": 1
                  },
                  "blog_id": 1,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [
                     {
                        "key": 1,
                        "value": {
                           "category_id": 1,
                           "description": "desc_1",
                           "name": "category_1"
                        }
                     },
                     {
                        "key": 3,
                        "value": {
                           "category_id": 3,
                           "description": "desc_3",
                           "name": "category_3"
                        }
                     }
                  ],
                  "list_comment": [
                     {
                        "comment_id": 1,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 3,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 5,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 7,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 2,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 4,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 6,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 8,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     }
                  ]
               },
               "comment_id": 1,
               "comment_text": "comment_1 text"
            },
            {
               "blog_id": {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": "2019-04-01",
                     "name": "author name modified at index 1 => container is dirty !!!",
                     "sex": 1
                  },
                  "blog_id": 1,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [
                     {
                        "key": 1,
                        "value": {
                           "category_id": 1,
                           "description": "desc_1",
                           "name": "category_1"
                        }
                     },
                     {
                        "key": 3,
                        "value": {
                           "category_id": 3,
                           "description": "desc_3",
                           "name": "category_3"
                        }
                     }
                  ],
                  "list_comment": [
                     {
                        "comment_id": 1,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 3,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 5,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 7,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 2,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 4,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 6,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 8,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     }
                  ]
               },
               "comment_id": 3,
               "comment_text": "comment_1 text"
            },
            {
               "blog_id": {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": "2019-04-01",
                     "name": "author name modified at index 1 => container is dirty !!!",
                     "sex": 1
                  },
                  "blog_id": 1,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [
                     {
                        "key": 1,
                        "value": {
                           "category_id": 1,
                           "description": "desc_1",
                           "name": "category_1"
                        }
                     },
                     {
                        "key": 3,
                        "value": {
                           "category_id": 3,
                           "description": "desc_3",
                           "name": "category_3"
                        }
                     }
                  ],
                  "list_comment": [
                     {
                        "comment_id": 1,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 3,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 5,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 7,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 2,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 4,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 6,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 8,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     }
                  ]
               },
               "comment_id": 5,
               "comment_text": "comment_1 text"
            },
            {
               "blog_id": {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": "2019-04-01",
                     "name": "author name modified at index 1 => container is dirty !!!",
                     "sex": 1
                  },
                  "blog_id": 1,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [
                     {
                        "key": 1,
                        "value": {
                           "category_id": 1,
                           "description": "desc_1",
                           "name": "category_1"
                        }
                     },
                     {
                        "key": 3,
                        "value": {
                           "category_id": 3,
                           "description": "desc_3",
                           "name": "category_3"
                        }
                     }
                  ],
                  "list_comment": [
                     {
                        "comment_id": 1,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 3,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 5,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 7,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 2,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 4,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 6,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 8,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     }
                  ]
               },
               "comment_id": 7,
               "comment_text": "comment_1 text"
            },
            {
               "blog_id": {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": "2019-04-01",
                     "name": "author name modified at index 1 => container is dirty !!!",
                     "sex": 1
                  },
                  "blog_id": 1,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [
                     {
                        "key": 1,
                        "value": {
                           "category_id": 1,
                           "description": "desc_1",
                           "name": "category_1"
                        }
                     },
                     {
                        "key": 3,
                        "value": {
                           "category_id": 3,
                           "description": "desc_3",
                           "name": "category_3"
                        }
                     }
                  ],
                  "list_comment": [
                     {
                        "comment_id": 1,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 3,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 5,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 7,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 2,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 4,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 6,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 8,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     }
                  ]
               },
               "comment_id": 2,
               "comment_text": "comment_2 text"
            },
            {
               "blog_id": {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": "2019-04-01",
                     "name": "author name modified at index 1 => container is dirty !!!",
                     "sex": 1
                  },
                  "blog_id": 1,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [
                     {
                        "key": 1,
                        "value": {
                           "category_id": 1,
                           "description": "desc_1",
                           "name": "category_1"
                        }
                     },
                     {
                        "key": 3,
                        "value": {
                           "category_id": 3,
                           "description": "desc_3",
                           "name": "category_3"
                        }
                     }
                  ],
                  "list_comment": [
                     {
                        "comment_id": 1,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 3,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 5,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 7,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 2,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 4,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 6,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 8,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     }
                  ]
               },
               "comment_id": 4,
               "comment_text": "comment_2 text"
            },
            {
               "blog_id": {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": "2019-04-01",
                     "name": "author name modified at index 1 => container is dirty !!!",
                     "sex": 1
                  },
                  "blog_id": 1,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [
                     {
                        "key": 1,
                        "value": {
                           "category_id": 1,
                           "description": "desc_1",
                           "name": "category_1"
                        }
                     },
                     {
                        "key": 3,
                        "value": {
                           "category_id": 3,
                           "description": "desc_3",
                           "name": "category_3"
                        }
                     }
                  ],
                  "list_comment": [
                     {
                        "comment_id": 1,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 3,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 5,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 7,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 2,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 4,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 6,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 8,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     }
                  ]
               },
               "comment_id": 6,
               "comment_text": "comment_2 text"
            },
            {
               "blog_id": {
                  "author_id": {
                     "author_id": "author_id_2",
                     "birthdate": "2019-04-01",
                     "name": "author name modified at index 1 => container is dirty !!!",
                     "sex": 1
                  },
                  "blog_id": 1,
                  "blog_text": "blog property 'text' modified => blog is dirty !!!",
                  "date_creation": "2019-04-01T16:18:54",
                  "list_category": [
                     {
                        "key": 1,
                        "value": {
                           "category_id": 1,
                           "description": "desc_1",
                           "name": "category_1"
                        }
                     },
                     {
                        "key": 3,
                        "value": {
                           "category_id": 3,
                           "description": "desc_3",
                           "name": "category_3"
                        }
                     }
                  ],
                  "list_comment": [
                     {
                        "comment_id": 1,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 3,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 5,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 7,
                        "comment_text": "comment_1 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 2,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 4,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 6,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     },
                     {
                        "comment_id": 8,
                        "comment_text": "comment_2 text",
                        "date_creation": "2019-04-01T16:18:54"
                     }
                  ]
               },
               "comment_id": 8,
               "comment_text": "comment_2 text"
            }
         ]
      },
      {
         "author_id": {
            "author_id": "author_id_2",
            "birthdate": "2019-04-01",
            "name": "author name modified at index 1 => container is dirty !!!"
         },
         "blog_id": 2,
         "blog_text": "blog property 'text' modified => blog is dirty !!!",
         "list_comment": []
      },
      {
         "author_id": {
            "author_id": "author_id_2",
            "birthdate": "2019-04-01",
            "name": "author name modified at index 1 => container is dirty !!!"
         },
         "blog_id": 3,
         "blog_text": "blog property 'text' modified => blog is dirty !!!",
         "list_comment": []
      },
      {
         "author_id": {
            "author_id": "author_id_2",
            "birthdate": "2019-04-01",
            "name": "author name modified at index 1 => container is dirty !!!"
         },
         "blog_id": 4,
         "blog_text": "blog property 'text' modified => blog is dirty !!!",
         "list_comment": []
      }
   ],
   "request_id": "4c45fdf9-8001-4509-bb4b-ce27a4a8708a"
}


fetch_by_id

L'action fetch_by_id permet de récupérer les données d'un élément d'une table en fonction de son identifiant unique.

-- Exemple n°1 -- récupérer les données du blog qui a pour identifiant unique 1 :

Requête JSON :
{
   "request_id": "4d6fbb9e-e088-482a-abfa-4e7ddee80569",
   "action": "fetch_by_id",
   "entity": "blog",
   "data": {
      "blog_id": 1
   }
}
Réponse JSON :
{
   "data": {
      "author_id": {
         "author_id": "author_id_2",
         "birthdate": null,
         "list_blog": [],
         "name": "",
         "sex": 2
      },
      "blog_id": 1,
      "blog_text": "blog property 'text' modified => blog is dirty !!!",
      "date_creation": "2019-04-01T16:18:54",
      "list_category": [],
      "list_comment": []
   },
   "request_id": "4d6fbb9e-e088-482a-abfa-4e7ddee80569"
}


-- Exemple n°2 -- récupère uniquement quelques données du blog qui a pour identifiant unique 1 (les autres données font partie du JSON mais avec une valeur vide ou null) :

Requête JSON :
{
   "request_id": "72c9b362-d194-410e-98ed-23797a34318e",
   "action": "fetch_by_id",
   "entity": "blog",
   "data": {
      "blog_id": 1
   },
   "columns": [
      "blog_text",
      "date_creation"
   ]
}
Réponse JSON :
{
   "data": {
      "author_id": null,
      "blog_id": 1,
      "blog_text": "blog property 'text' modified => blog is dirty !!!",
      "date_creation": "2019-04-01T16:18:54",
      "list_category": [],
      "list_comment": []
   },
   "request_id": "72c9b362-d194-410e-98ed-23797a34318e"
}


-- Exemple n°3 -- récupère une liste de blogs en fonction de leur identifiant :

Requête JSON :
{
   "request_id": "59c37f70-26ee-42e5-9177-b32c331adce1",
   "action": "fetch_by_id",
   "entity": "blog",
   "data": [
      {
         "blog_id": 1
      },
      {
         "blog_id": 2
      },
      {
         "blog_id": 3
      }
   ]
}
Réponse JSON :
{
   "data": [
      {
         "author_id": {
            "author_id": "author_id_2",
            "birthdate": null,
            "list_blog": [],
            "name": "",
            "sex": 2
         },
         "blog_id": 1,
         "blog_text": "blog property 'text' modified => blog is dirty !!!",
         "date_creation": "2019-04-01T16:18:54",
         "list_category": [],
         "list_comment": []
      },
      {
         "author_id": {
            "author_id": "author_id_2",
            "birthdate": null,
            "list_blog": [],
            "name": "",
            "sex": 2
         },
         "blog_id": 2,
         "blog_text": "blog property 'text' modified => blog is dirty !!!",
         "date_creation": "2019-04-01T16:18:54",
         "list_category": [],
         "list_comment": []
      },
      {
         "author_id": {
            "author_id": "author_id_2",
            "birthdate": null,
            "list_blog": [],
            "name": "",
            "sex": 2
         },
         "blog_id": 3,
         "blog_text": "blog property 'text' modified => blog is dirty !!!",
         "date_creation": "2019-04-01T16:18:54",
         "list_category": [],
         "list_comment": []
      }
   ],
   "request_id": "59c37f70-26ee-42e5-9177-b32c331adce1"
}


-- Exemple n°4 -- récupère une liste de blogs (avec quelques relations associées) en fonction de leur identifiant, et défini un format de sortie (toutes les propriétés ne feront pas partie de la réponse JSON) :

Requête JSON :
{
   "request_id": "325d64f4-29ac-47ab-9846-d6a71a9e9d73",
   "action": "fetch_by_id",
   "entity": "blog",
   "data": [
      {
         "blog_id": 1
      },
      {
         "blog_id": 2
      }
   ],
   "relations": [
      "{ blog_text }",
      "author_id <author_alias> { name, birthdate }",
      "list_comment <list_comment_alias> { comment_text }"
   ],
   "output_format": [
      "{ blog_text }",
      "author_id { name, birthdate }",
      "list_comment { comment_text }"
   ]
}
Réponse JSON :
{
   "data": [
      {
         "author_id": {
            "author_id": "author_id_2",
            "birthdate": "2019-04-01",
            "name": "author name modified at index 1 => container is dirty !!!"
         },
         "blog_id": 1,
         "blog_text": "blog property 'text' modified => blog is dirty !!!",
         "list_comment": [
            {
               "comment_id": 1,
               "comment_text": "comment_1 text"
            },
            {
               "comment_id": 2,
               "comment_text": "comment_2 text"
            },
            {
               "comment_id": 3,
               "comment_text": "comment_1 text"
            },
            {
               "comment_id": 4,
               "comment_text": "comment_2 text"
            },
            {
               "comment_id": 5,
               "comment_text": "comment_1 text"
            },
            {
               "comment_id": 6,
               "comment_text": "comment_2 text"
            },
            {
               "comment_id": 7,
               "comment_text": "comment_1 text"
            },
            {
               "comment_id": 8,
               "comment_text": "comment_2 text"
            }
         ]
      },
      {
         "author_id": {
            "author_id": "author_id_2",
            "birthdate": "2019-04-01",
            "name": "author name modified at index 1 => container is dirty !!!"
         },
         "blog_id": 2,
         "blog_text": "blog property 'text' modified => blog is dirty !!!",
         "list_comment": []
      }
   ],
   "request_id": "325d64f4-29ac-47ab-9846-d6a71a9e9d73"
}


fetch_by_query

L'action fetch_by_query permet de récupérer les éléments d'une table filtrés par une requête.

-- Exemple n°1 -- récupère uniquement les éléments de la table author dont le sexe est de type female (female == enum dont la valeur est 1) :

Requête JSON :
{
   "request_id": "c178194c-a76f-4a77-af12-2b97fc7078e4",
   "action": "fetch_by_query",
   "entity": "author",
   "query": {
      "sql": "WHERE author.sex = :sex",
      "params": [
         {
            "key": ":sex",
            "value": 1
         }
      ]
   }
}
Réponse JSON :
{
   "data": [
      {
         "author_id": "author_id_2",
         "birthdate": "2019-04-01",
         "list_blog": [],
         "name": "author name modified at index 1 => container is dirty !!!",
         "sex": 1
      },
      {
         "author_id": "author_id_3",
         "birthdate": "1998-03-06",
         "list_blog": [],
         "name": "author_3",
         "sex": 1
      }
   ],
   "request_id": "c178194c-a76f-4a77-af12-2b97fc7078e4"
}


-- Exemple n°2 -- récupère uniquement les éléments de la table author (et toutes ses relations associées) dont le sexe est de type female :

Requête JSON :
{
   "request_id": "84e2e13a-0bf9-4d78-b655-970568a97e4c",
   "action": "fetch_by_query",
   "entity": "author",
   "query": {
      "sql": "WHERE author.sex = :sex",
      "params": [
         {
            "key": ":sex",
            "value": 1,
            "type": "in"
         }
      ]
   },
   "relations": [
      "*"
   ]
}
Réponse JSON :
{
   "data": [
      {
         "author_id": "author_id_2",
         "birthdate": "2019-04-01",
         "list_blog": [
            {
               "author_id": {
                  "author_id": "author_id_2",
                  "birthdate": null,
                  "list_blog": [],
                  "name": "",
                  "sex": 2
               },
               "blog_id": 1,
               "blog_text": "blog property 'text' modified => blog is dirty !!!",
               "date_creation": "2019-04-01T16:18:54",
               "list_category": [],
               "list_comment": []
            },
            {
               "author_id": {
                  "author_id": "author_id_2",
                  "birthdate": null,
                  "list_blog": [],
                  "name": "",
                  "sex": 2
               },
               "blog_id": 2,
               "blog_text": "blog property 'text' modified => blog is dirty !!!",
               "date_creation": "2019-04-01T16:18:54",
               "list_category": [],
               "list_comment": []
            },
            {
               "author_id": {
                  "author_id": "author_id_2",
                  "birthdate": null,
                  "list_blog": [],
                  "name": "",
                  "sex": 2
               },
               "blog_id": 3,
               "blog_text": "blog property 'text' modified => blog is dirty !!!",
               "date_creation": "2019-04-01T16:18:54",
               "list_category": [],
               "list_comment": []
            },
            {
               "author_id": {
                  "author_id": "author_id_2",
                  "birthdate": null,
                  "list_blog": [],
                  "name": "",
                  "sex": 2
               },
               "blog_id": 4,
               "blog_text": "blog property 'text' modified => blog is dirty !!!",
               "date_creation": "2019-04-01T16:18:54",
               "list_category": [],
               "list_comment": []
            }
         ],
         "name": "author name modified at index 1 => container is dirty !!!",
         "sex": 1
      },
      {
         "author_id": "author_id_3",
         "birthdate": "1998-03-06",
         "list_blog": [],
         "name": "author_3",
         "sex": 1
      }
   ],
   "request_id": "84e2e13a-0bf9-4d78-b655-970568a97e4c"
}


-- Exemple n°3 -- récupère uniquement les éléments de la table author (et toutes ses relations associées) dont le sexe est de type female, et défini un format de sortie (toutes les propriétés ne feront pas partie de la réponse JSON) :

Requête JSON :
{
   "request_id": "c18b59e7-54f9-4a4f-843d-f0797f4fb676",
   "action": "fetch_by_query",
   "entity": "author",
   "query": {
      "sql": "WHERE author.sex = :sex",
      "params": [
         {
            "key": ":sex",
            "value": 1,
            "type": "in"
         }
      ]
   },
   "relations": [
      "*"
   ],
   "output_format": [
      "{ birthdate, name }",
      "list_blog { blog_text, date_creation }"
   ]
}
Réponse JSON :
{
   "data": [
      {
         "author_id": "author_id_2",
         "birthdate": "2019-04-01",
         "list_blog": [
            {
               "blog_id": 1,
               "blog_text": "blog property 'text' modified => blog is dirty !!!",
               "date_creation": "2019-04-01T16:18:54"
            },
            {
               "blog_id": 2,
               "blog_text": "blog property 'text' modified => blog is dirty !!!",
               "date_creation": "2019-04-01T16:18:54"
            },
            {
               "blog_id": 3,
               "blog_text": "blog property 'text' modified => blog is dirty !!!",
               "date_creation": "2019-04-01T16:18:54"
            },
            {
               "blog_id": 4,
               "blog_text": "blog property 'text' modified => blog is dirty !!!",
               "date_creation": "2019-04-01T16:18:54"
            }
         ],
         "name": "author name modified at index 1 => container is dirty !!!"
      },
      {
         "author_id": "author_id_3",
         "birthdate": "1998-03-06",
         "list_blog": [],
         "name": "author_3"
      }
   ],
   "request_id": "c18b59e7-54f9-4a4f-843d-f0797f4fb676"
}


count

L'action count permet de compter les éléments d'une table avec ou sans requête (et avec ou sans relation).

-- Exemple n°1 -- compter le nombre de blogs dans la base de données :

Requête JSON :
{
   "request_id": "1ef62fd7-d847-4d67-9fd0-0207af463aa4",
   "action": "count",
   "entity": "blog"
}
Réponse JSON :
{
   "data": {
      "count": 4
   },
   "request_id": "1ef62fd7-d847-4d67-9fd0-0207af463aa4"
}


-- Exemple n°2 -- compter tous les author dont le sexe est de type female :

Requête JSON :
{
   "request_id": "a80646d1-5a42-46fb-9306-3b91c7f594c8",
   "action": "count",
   "entity": "author",
   "query": {
      "sql": "WHERE author.sex = :sex",
      "params": [
         {
            "key": ":sex",
            "value": 1
         }
      ]
   }
}
Réponse JSON :
{
   "data": {
      "count": 2
   },
   "request_id": "a80646d1-5a42-46fb-9306-3b91c7f594c8"
}


-- Exemple n°3 -- compter tous les blogs dont l'author est de type female :

Requête JSON :
{
   "request_id": "6ef252f7-385c-465e-8304-b9afa9fea490",
   "action": "count",
   "entity": "blog",
   "query": {
      "sql": "WHERE author_alias.sex = :sex",
      "params": [
         {
            "key": ":sex",
            "value": 1
         }
      ]
   },
   "relations": [
      "author_id <author_alias> { sex }"
   ]
}
Réponse JSON :
{
   "data": {
      "count": 4
   },
   "request_id": "6ef252f7-385c-465e-8304-b9afa9fea490"
}


exist

L'action exist permet de tester l'existence d'un ou plusieurs éléments d'une table en fonction de l'identifiant.

-- Exemple n°1 -- tester l'existence d'un blog dont l'identifiant unique a pour valeur 1 :

Requête JSON :
{
   "request_id": "e8db33db-b249-4349-93fe-ad12e208520e",
   "action": "exist",
   "entity": "blog",
   "data": {
      "blog_id": 1
   }
}
Réponse JSON :
{
   "data": {
      "exist": true
   },
   "request_id": "e8db33db-b249-4349-93fe-ad12e208520e"
}


-- Exemple n°2 -- tester l'existence de plusieurs blogs :

Requête JSON :
{
   "request_id": "f2d6ca3f-36de-4920-8f4c-c04842603467",
   "action": "exist",
   "entity": "blog",
   "data": [
      {
         "blog_id": 1
      },
      {
         "blog_id": 999
      },
      {
         "blog_id": 3
      }
   ]
}
Réponse JSON :
{
   "data": {
      "exist": false
   },
   "request_id": "f2d6ca3f-36de-4920-8f4c-c04842603467"
}


-- Exemple n°3 -- tester l'existence d'un author :

Requête JSON :
{
   "request_id": "2c7df172-8010-4816-b8e1-3edbb0b0b90e",
   "action": "exist",
   "entity": "author",
   "data": {
      "author_id": "author_id_2"
   }
}
Réponse JSON :
{
   "data": {
      "exist": true
   },
   "request_id": "2c7df172-8010-4816-b8e1-3edbb0b0b90e"
}


Ajout de données (insert)

L'action insert permet d'insérer un ou plusieurs éléments dans la base de données. Les identifiants uniques générés par la base de données (par exemple identifiant auto-incrémenté) sont fournis dans la réponse JSON.

-- Exemple n°1 -- insérer un blog dans la base de données :

Requête JSON :
{
   "request_id": "573e4940-607a-4037-8a09-11ec52deb21c",
   "action": "insert",
   "entity": "blog",
   "data": {
      "blog_text": "this is a new blog from QxOrm REST API !",
      "date_creation": "2018-01-30T12:42:01",
      "author_id": "author_id_2"
   }
}
Réponse JSON :
{
   "data": {
      "blog_id": 5
   },
   "request_id": "573e4940-607a-4037-8a09-11ec52deb21c"
}


-- Exemple n°2 -- insérer une liste de blogs dans la base de données :

Requête JSON :
{
   "request_id": "6ade2d01-086c-45d6-971b-b65e8836475f",
   "action": "insert",
   "entity": "blog",
   "data": [
      {
         "blog_text": "new blog from QxOrm REST API !",
         "date_creation": "2018-01-30T12:42:01",
         "author_id": "author_id_2"
      },
      {
         "blog_text": "another blog from QxOrm REST API !",
         "date_creation": "2016-06-12T08:33:12",
         "author_id": "author_id_1"
      }
   ]
}
Réponse JSON :
{
   "data": [
      {
         "blog_id": 6
      },
      {
         "blog_id": 7
      }
   ],
   "request_id": "6ade2d01-086c-45d6-971b-b65e8836475f"
}


-- Exemple n°3 -- insérer un author dans la base de données :

Requête JSON :
{
   "request_id": "0cffa916-99f4-4395-bccd-02918a4b3c57",
   "action": "insert",
   "entity": "author",
   "data": {
      "author_id": "author_id_from_rest_api",
      "birthdate": "1978-05-11",
      "name": "new author created by QxOrm REST API",
      "sex": 1
   }
}
Réponse JSON :
{
   "data": {
      "author_id": "author_id_from_rest_api"
   },
   "request_id": "0cffa916-99f4-4395-bccd-02918a4b3c57"
}

Remarque : l'identifiant unique de la table author doit être fourni par l'appelant (non auto-incrémenté). Si on rejoue une 2ème fois la même requête, on obtient l'erreur suivante :
{
   "error": {
      "code": 19,
      "desc": "Unable to fetch row\ncolumn author_id is not unique"
   },
   "request_id": "0cffa916-99f4-4395-bccd-02918a4b3c57"
}


Mise à jour de données (update)

L'action update permet une mise à jour de un ou plusieurs éléments dans la base de données.

-- Exemple n°1 -- mise à jour du blog avec pour identifiant unique la valeur 1 :

Requête JSON :
{
   "request_id": "4fa24a7f-a3d8-4bbf-85c1-c86df83dec0b",
   "action": "update",
   "entity": "blog",
   "data": {
      "blog_id": 1,
      "blog_text": "modify blog from QxOrm REST API",
      "date_creation": "2013-11-25T09:56:33",
      "author_id": "author_id_1"
   }
}
Réponse JSON :
{
   "data": {
      "blog_id": 1
   },
   "request_id": "4fa24a7f-a3d8-4bbf-85c1-c86df83dec0b"
}


-- Exemple n°2 -- mise à jour uniquement de certaines colonnes d'un blog :

Requête JSON :
{
   "request_id": "d0704db1-5c3a-48ad-b27e-14aa54ac0efb",
   "action": "update",
   "entity": "blog",
   "data": {
      "blog_id": 2,
      "blog_text": "modify blog from QxOrm REST API",
      "date_creation": "2013-11-25T09:56:33"
   },
   "columns": [
      "blog_text",
      "date_creation"
   ]
}
Réponse JSON :
{
   "data": {
      "blog_id": 2
   },
   "request_id": "d0704db1-5c3a-48ad-b27e-14aa54ac0efb"
}


-- Exemple n°3 -- mise à jour de plusieurs author :

Requête JSON :
{
   "request_id": "26ec3a7b-cf2d-47f7-bab7-db303f15ee51",
   "action": "update",
   "entity": "author",
   "data": [
      {
         "author_id": "author_id_from_rest_api",
         "birthdate": "1992-11-03",
         "name": "modify author from QxOrm REST API",
         "sex": 0
      },
      {
         "author_id": "author_id_1",
         "birthdate": "1978-12-25",
         "name": "modify another author from QxOrm REST API",
         "sex": 2
      }
   ]
}
Réponse JSON :
{
   "data": [
      {
         "author_id": "author_id_from_rest_api"
      },
      {
         "author_id": "author_id_1"
      }
   ],
   "request_id": "26ec3a7b-cf2d-47f7-bab7-db303f15ee51"
}


Sauvegarde de données (save)

L'action save permet d'insérer ou mettre à jour (insert ou update) un ou plusieurs éléments dans la base de données. En cas d'insertion, les identifiants uniques générés par la base de données (par exemple identifiant auto-incrémenté) sont fournis dans la réponse JSON.

La requête JSON dispose d'un paramètre optionnel nommé save_mode qui peut prendre les valeurs suivantes :
  • check_insert_or_update : sauvegarde l'instance et les relations de façon récursive (sur plusieurs niveaux) en vérifiant pour chaque relation s'il faut faire un insert ou update (méthode pouvant être lente si beaucoup de relations à traiter) ;
  • insert_only : insère de façon récursive (sur plusieurs niveaux) l'instance et toutes les relations associées ;
  • update_only : met à jour de façon récursive (sur plusieurs niveaux) l'instance et toutes les relations associées.

-- Exemple n°1 -- sauvegarde (insère ou met à jour suivant l'identifant unique) un blog dans la base de données :

Requête JSON :
{
   "request_id": "ec3c71eb-5014-4b36-85a0-aeb7ae48a5e9",
   "action": "save",
   "entity": "blog",
   "data": {
      "blog_id": 1,
      "blog_text": "modify blog from QxOrm REST API",
      "date_creation": "2013-11-25T09:56:33",
      "author_id": "author_id_1"
   }
}
Réponse JSON :
{
   "data": {
      "blog_id": 1
   },
   "request_id": "ec3c71eb-5014-4b36-85a0-aeb7ae48a5e9"
}


-- Exemple n°2 -- sauvegarde (insère ou met à jour suivant l'identifant unique) une liste de blogs dans la base de données :

Requête JSON :
{
   "request_id": "dc7c804e-f95a-4a9b-a4e3-547adcacf090",
   "action": "save",
   "entity": "blog",
   "data": [
      {
         "blog_id": 1,
         "blog_text": "save blog from QxOrm REST API !",
         "date_creation": "2018-01-30T12:42:01",
         "author_id": "author_id_2"
      },
      {
         "blog_text": "save another blog from QxOrm REST API !",
         "date_creation": "2016-06-12T08:33:12",
         "author_id": "author_id_1"
      }
   ]
}
Réponse JSON :
{
   "data": [
      {
         "blog_id": 1
      },
      {
         "blog_id": 5
      }
   ],
   "request_id": "dc7c804e-f95a-4a9b-a4e3-547adcacf090"
}


-- Exemple n°3 -- sauvegarde (insère ou met à jour suivant l'identifant unique) un blog et toutes ses relations sur plusieurs niveaux (de façon récursive) :

Requête JSON :
{
   "request_id": "5b78e468-2fa3-4aeb-82ce-4d85408f5fa7",
   "action": "save",
   "entity": "blog",
   "data": {
      "blog_id": 1,
      "blog_text": "save recursive blog from QxOrm REST API",
      "date_creation": "2013-11-25T09:56:33",
      "author_id": {
         "author_id": "author_id_1",
         "birthdate": "1965-07-21",
         "name": "save recursive author from QxOrm REST API",
         "sex": 0
      }
   },
   "save_mode": "check_insert_or_update"
}
Réponse JSON :
{
   "data": {
      "blog_id": 1
   },
   "request_id": "5b78e468-2fa3-4aeb-82ce-4d85408f5fa7"
}


-- Exemple n°4 -- insère (save_mode = insert_only) un blog et toutes ses relations sur plusieurs niveaux (de façon récursive) :

Requête JSON :
{
   "request_id": "ef147c62-74e0-4be2-a294-ffeb020d5304",
   "action": "save",
   "entity": "blog",
   "data": {
      "blog_text": "save recursive - new blog from QxOrm REST API",
      "date_creation": "2013-11-25T09:56:33",
      "author_id": {
         "author_id": "author_id_save_recursive",
         "birthdate": "1965-07-21",
         "name": "save recursive (insert only) author from QxOrm REST API",
         "sex": 0
      }
   },
   "save_mode": "insert_only"
}
Réponse JSON :
{
   "data": {
      "blog_id": 7
   },
   "request_id": "ef147c62-74e0-4be2-a294-ffeb020d5304"
}


Suppression de données (delete)

Ce chapitre détaille les différentes méthodes pour supprimer des éléments de la base de données :
Remarque : la différence entre delete et destroy est liée à la suppression logique (soft delete) d'un élément.

delete_all / destroy_all

Les actions delete_all et destroy_all permettent de supprimer tous les éléments d'une table. La différence entre delete et destroy est liée à la suppression logique (soft delete) d'un élément.

-- Exemple n°1 -- supprime tous les éléments de la table comment :

Requête JSON :
{
   "request_id": "7b06b5c0-409f-4e0d-bfc4-acafbfe7e796",
   "action": "delete_all",
   "entity": "comment"
}
Réponse JSON :
{
   "data": {
      "deleted": true
   },
   "request_id": "7b06b5c0-409f-4e0d-bfc4-acafbfe7e796"
}


delete_by_query / destroy_by_query

Les actions delete_by_query et destroy_by_query permettent de supprimer les éléments d'une table en fonction d'une requête. La différence entre delete et destroy est liée à la suppression logique (soft delete) d'un élément.

-- Exemple n°1 -- supprime les éléments de la table author qui ont un sexe de type female (female = enum de valeur 1) :

Requête JSON :
{
   "request_id": "169ff0be-6e49-457b-a99c-22bd7141dc02",
   "action": "delete_by_query",
   "entity": "author",
   "query": {
      "sql": "WHERE author.sex = :sex",
      "params": [
         {
            "key": ":sex",
            "value": 1
         }
      ]
   }
}
Réponse JSON :
{
   "data": {
      "deleted": true
   },
   "request_id": "169ff0be-6e49-457b-a99c-22bd7141dc02"
}


delete_by_id / destroy_by_id

Les actions delete_by_id et destroy_by_id permettent de supprimer les éléments d'une table en fonction de leur identifiant unique. La différence entre delete et destroy est liée à la suppression logique (soft delete) d'un élément.

-- Exemple n°1 -- supprime de la base de données le blog qui a pour identifiant unique la valeur 4 :

Requête JSON :
{
   "request_id": "80bff383-8ebd-4bde-bb42-37b6f67bc39f",
   "action": "delete_by_id",
   "entity": "blog",
   "data": {
      "blog_id": 4
   }
}
Réponse JSON :
{
   "data": {
      "blog_id": 4
   },
   "request_id": "80bff383-8ebd-4bde-bb42-37b6f67bc39f"
}


-- Exemple n°2 -- supprime de la base de données les blogs qui ont pour identifiant unique les valeurs 2 et 3 :

Requête JSON :
{
   "request_id": "38020cb7-d725-4c0e-80a0-63db7569155e",
   "action": "delete_by_id",
   "entity": "blog",
   "data": [
      {
         "blog_id": 3
      },
      {
         "blog_id": 2
      }
   ]
}
Réponse JSON :
{
   "data": [
      {
         "blog_id": 3
      },
      {
         "blog_id": 2
      }
   ],
   "request_id": "38020cb7-d725-4c0e-80a0-63db7569155e"
}


Validation de données (validate)

L'action validate permet de valider les propriétés d'une instance (sans déclencher d'action sur la base de données). L'action validate appelle le module QxValidator de la bibliothèque QxOrm.

-- Exemple n°1 -- un blog doit contenir du texte (propriété blog_text) pour pouvoir être sauvegardé dans la base de données. La requête JSON suivante permet d'indiquer que l'instance est non valide avec un message explicite :

Requête JSON :
{
   "request_id": "92043c2b-4ba8-4583-8fad-c828251734ba",
   "action": "validate",
   "entity": "blog",
   "data": {
      "blog_id": 9999,
      "blog_text": ""
   }
}
Réponse JSON :
{
   "data": {
      "invalid_values": [
         "blog",
         [
            {
               "message": "'blog_text' property cannot be empty",
               "path": "blog"
            }
         ]
      ]
   },
   "request_id": "92043c2b-4ba8-4583-8fad-c828251734ba"
}


-- Exemple n°2 -- en ajoutant une valeur à la propriété blog_text, alors le blog devient valide (la réponse JSON dispose d'un champ invalid_values qui vaut null) :

Requête JSON :
{
   "request_id": "92043c2b-4ba8-4583-8fad-c828251734ba",
   "action": "validate",
   "entity": "blog",
   "data": {
      "blog_id": 9999,
      "blog_text": "my blog text !!!"
   }
}
Réponse JSON :
{
   "data": {
      "invalid_values": null
   },
   "request_id": "92043c2b-4ba8-4583-8fad-c828251734ba"
}


Appel RAW SQL ou procédure stockée

L'action call_custom_query permet d'appeler une requête SQL personnalisée ou une procédure stockée.

-- Exemple n°1 -- insère dans la base de données un nouveau author avec une requête SQL personnalisée :

Requête JSON :
{
   "request_id": "ff2a2256-041d-4c5f-bd86-3745ce46ead8",
   "action": "call_custom_query",
   "query": {
      "sql": "INSERT INTO author (author_id, name, birthdate, sex) VALUES (:author_id, :name, :birthdate, :sex)",
      "params": [
         {
            "key": ":author_id",
            "value": "author_id_custom_query"
         },
         {
            "key": ":name",
            "value": "new author inserted by custom query"
         },
         {
            "key": ":birthdate",
            "value": "20190215"
         },
         {
            "key": ":sex",
            "value": 2
         }
      ]
   }
}
Réponse JSON :
{
   "data": {
      "query_output": {
         "distinct": false,
         "list_values": {
            ":author_id": [
               "author_id_custom_query",
               1
            ],
            ":birthdate": [
               "20190215",
               1
            ],
            ":name": [
               "new author inserted by custom query",
               1
            ],
            ":sex": [
               2,
               1
            ]
         },
         "parenthesis_count": 0,
         "query": [
            "INSERT INTO author (author_id, name, birthdate, sex) VALUES (:author_id, :name, :birthdate, :sex)"
         ],
         "response": "",
         "result_position_by_key": {},
         "result_values": [],
         "sql_element_index": 0,
         "sql_element_list": [],
         "sql_element_temp_type": 0,
         "type": ""
      }
   },
   "request_id": "ff2a2256-041d-4c5f-bd86-3745ce46ead8"
}


Appel fonctions natives C++

L'action call_entity_function permet d'appeler des fonctions natives C++ enregistrées dans le contexte QxOrm.
Prérequis : la fonction native C++ doit être une fonction static avec pour signature : static QJsonValue myNativeCppFct(const QJsonValue & request);

Voici un exemple d'enregistrement de fonction native C++ pouvant être appelée par les API JSON de la bibliothèque QxOrm :

namespace qx {
template <> void register_class(QxClass<blog> & t)
{
   // Register 'helloWorld()' static function in QxOrm context (can be called by QxRestApi JSON API module)
   t.fctStatic_1<QJsonValue, const QJsonValue & >(& blog::helloWorld, "helloWorld");
}}

// 'helloWorld()' static function implementation
QJsonValue blog::helloWorld(const QJsonValue & request)
{
   QJsonObject response;
   response.insert("request", request);
   response.insert("response", QString("Hello World !"));
   return response;
}


Voici comment appeler cette fonction helloWorld avec les API JSON en utilisant l'action call_entity_function :

Requête JSON :
{
   "request_id": "ab1ba7d3-9f98-4b18-a310-a9c34498d043",
   "action": "call_entity_function",
   "entity": "blog",
   "fct": "helloWorld",
   "data": {
      "param1": "test",
      "param2": "static fct call"
   }
}
Réponse JSON :
{
   "data": {
      "request": {
         "param1": "test",
         "param2": "static fct call"
      },
      "response": "Hello World !"
   },
   "request_id": "ab1ba7d3-9f98-4b18-a310-a9c34498d043"
}


Meta-data (structure des classes C++ enregistrées dans le contexte QxOrm)

L'action get_meta_data permet de récupérer les méta-données d'une ou de toutes les entités enregistrées dans le contexte QxOrm (structure des classes avec liste des propriétés et relations).

-- Exemple n°1 -- récupère toutes les méta-données du projet d'exemple qxBlogRestApi :

Requête JSON :
{
   "request_id": "842ed7b5-9b94-455f-86dc-32992866b3d5",
   "action": "get_meta_data",
   "entity": "*"
}
Réponse JSON :
{
   "data": {
      "entities": [
         {
            "base_entity": "",
            "description": "",
            "entity_id": {
               "description": "",
               "key": "author_id",
               "type": "QString"
            },
            "key": "author",
            "name": "author",
            "properties": [
               {
                  "description": "",
                  "key": "name",
                  "type": "QString"
               },
               {
                  "description": "",
                  "key": "birthdate",
                  "type": "QDate"
               },
               {
                  "description": "",
                  "key": "sex",
                  "type": "enum author::enum_sex *"
               }
            ],
            "relations": [
               {
                  "description": "",
                  "key": "list_blog",
                  "target": "blog",
                  "type": "std::vector<std::shared_ptr<blog>>",
                  "type_relation": "relation one-to-many"
               }
            ],
            "version": 0
         },
         {
            "base_entity": "",
            "description": "",
            "entity_id": {
               "description": "",
               "key": "blog_id",
               "type": "long"
            },
            "key": "blog",
            "name": "blog",
            "properties": [
               {
                  "description": "",
                  "key": "blog_text",
                  "type": "QString"
               },
               {
                  "description": "",
                  "key": "date_creation",
                  "type": "QDateTime"
               }
            ],
            "relations": [
               {
                  "description": "",
                  "key": "author_id",
                  "target": "author",
                  "type": "std::shared_ptr<author>",
                  "type_relation": "relation many-to-one"
               },
               {
                  "description": "",
                  "key": "list_comment",
                  "target": "comment",
                  "type": "QList<std::shared_ptr<comment>>",
                  "type_relation": "relation one-to-many"
               },
               {
                  "description": "",
                  "key": "list_category",
                  "target": "category",
                  "type": "qx::QxCollection<long, QSharedPointer<category>>",
                  "type_relation": "relation many-to-many"
               }
            ],
            "version": 0
         },
         {
            "base_entity": "",
            "description": "",
            "entity_id": {
               "description": "",
               "key": "comment_id",
               "type": "long"
            },
            "key": "comment",
            "name": "comment",
            "properties": [
               {
                  "description": "",
                  "key": "comment_text",
                  "type": "QString"
               },
               {
                  "description": "",
                  "key": "date_creation",
                  "type": "QDateTime"
               }
            ],
            "relations": [
               {
                  "description": "",
                  "key": "blog_id",
                  "target": "blog",
                  "type": "std::shared_ptr<blog>",
                  "type_relation": "relation many-to-one"
               }
            ],
            "version": 0
         },
         {
            "base_entity": "",
            "description": "",
            "entity_id": {
               "description": "",
               "key": "category_id",
               "type": "long"
            },
            "key": "category",
            "name": "category",
            "properties": [
               {
                  "description": "",
                  "key": "name",
                  "type": "QString"
               },
               {
                  "description": "",
                  "key": "description",
                  "type": "QString"
               }
            ],
            "relations": [
               {
                  "description": "",
                  "key": "list_blog",
                  "target": "blog",
                  "type": "qx::QxCollection<long, std::shared_ptr<blog>>",
                  "type_relation": "relation many-to-many"
               }
            ],
            "version": 0
         }
      ]
   },
   "request_id": "842ed7b5-9b94-455f-86dc-32992866b3d5"
}


Envoyer une liste de requêtes JSON

Afin de limiter le nombre de transactions entre le client et le serveur, il est possible d'envoyer une liste de requêtes JSON au module QxRestApi. Chaque requête JSON de la liste peut disposer de son propre identifiant request_id (afin d'associer une réponse JSON à la requête correspondante). Lorsqu'une liste de requêtes JSON est envoyée au module QxRestApi, alors une transaction (commit/rollback) est automatiquement créée (ainsi en cas d'erreur, tous les traitements sur la base de données sont annulés).

-- Exemple n°1 -- envoi 4 requêtes JSON au module QxRestApi (1 requête pour récupérer les méta-données du projet + 3 requêtes fetch_all avec différent niveau de récupération des relations) :

Requête JSON :
[
   {
      "request_id": "53c96a23-2566-4b3d-ae6c-bff634600e79",
      "action": "get_meta_data",
      "entity": "*"
   },
   {
      "request_id": "56e3ca99-5c12-4aca-aa6c-7d0e43c1e636",
      "action": "fetch_all",
      "entity": "blog"
   },
   {
      "request_id": "692968e4-8885-41ad-b918-6ce2791b3bb8",
      "action": "fetch_all",
      "entity": "blog",
      "data": [
         {
            "key": "",
            "value": ""
         }
      ]
   },
   {
      "request_id": "4ffe38a6-d642-44b0-8be1-198e84256321",
      "action": "fetch_all",
      "entity": "blog",
      "relations": [
         "*->*"
      ]
   }
]
Réponse JSON :
[
   {
      "data": {
         "entities": [
            {
               "base_entity": "",
               "description": "",
               "entity_id": {
                  "description": "",
                  "key": "author_id",
                  "type": "QString"
               },
               "key": "author",
               "name": "author",
               "properties": [
                  {
                     "description": "",
                     "key": "name",
                     "type": "QString"
                  },
                  {
                     "description": "",
                     "key": "birthdate",
                     "type": "QDate"
                  },
                  {
                     "description": "",
                     "key": "sex",
                     "type": "enum author::enum_sex *"
                  }
               ],
               "relations": [
                  {
                     "description": "",
                     "key": "list_blog",
                     "target": "blog",
                     "type": "std::vector<std::shared_ptr<blog>>",
                     "type_relation": "relation one-to-many"
                  }
               ],
               "version": 0
            },
            {
               "base_entity": "",
               "description": "",
               "entity_id": {
                  "description": "",
                  "key": "blog_id",
                  "type": "long"
               },
               "key": "blog",
               "name": "blog",
               "properties": [
                  {
                     "description": "",
                     "key": "blog_text",
                     "type": "QString"
                  },
                  {
                     "description": "",
                     "key": "date_creation",
                     "type": "QDateTime"
                  }
               ],
               "relations": [
                  {
                     "description": "",
                     "key": "author_id",
                     "target": "author",
                     "type": "std::shared_ptr<author>",
                     "type_relation": "relation many-to-one"
                  },
                  {
                     "description": "",
                     "key": "list_comment",
                     "target": "comment",
                     "type": "QList<std::shared_ptr<comment>>",
                     "type_relation": "relation one-to-many"
                  },
                  {
                     "description": "",
                     "key": "list_category",
                     "target": "category",
                     "type": "qx::QxCollection<long, QSharedPointer<category>>",
                     "type_relation": "relation many-to-many"
                  }
               ],
               "version": 0
            },
            {
               "base_entity": "",
               "description": "",
               "entity_id": {
                  "description": "",
                  "key": "comment_id",
                  "type": "long"
               },
               "key": "comment",
               "name": "comment",
               "properties": [
                  {
                     "description": "",
                     "key": "comment_text",
                     "type": "QString"
                  },
                  {
                     "description": "",
                     "key": "date_creation",
                     "type": "QDateTime"
                  }
               ],
               "relations": [
                  {
                     "description": "",
                     "key": "blog_id",
                     "target": "blog",
                     "type": "std::shared_ptr<blog>",
                     "type_relation": "relation many-to-one"
                  }
               ],
               "version": 0
            },
            {
               "base_entity": "",
               "description": "",
               "entity_id": {
                  "description": "",
                  "key": "category_id",
                  "type": "long"
               },
               "key": "category",
               "name": "category",
               "properties": [
                  {
                     "description": "",
                     "key": "name",
                     "type": "QString"
                  },
                  {
                     "description": "",
                     "key": "description",
                     "type": "QString"
                  }
               ],
               "relations": [
                  {
                     "description": "",
                     "key": "list_blog",
                     "target": "blog",
                     "type": "qx::QxCollection<long, std::shared_ptr<blog>>",
                     "type_relation": "relation many-to-many"
                  }
               ],
               "version": 0
            }
         ]
      },
      "request_id": "53c96a23-2566-4b3d-ae6c-bff634600e79"
   },
   {
      "data": [
         {
            "author_id": {
               "author_id": "author_id_1",
               "birthdate": null,
               "list_blog": [],
               "name": "",
               "sex": 2
            },
            "blog_id": 1,
            "blog_text": "save recursive blog from QxOrm REST API",
            "date_creation": "2013-11-25T09:56:33",
            "list_category": [],
            "list_comment": []
         },
         {
            "author_id": {
               "author_id": "author_id_1",
               "birthdate": null,
               "list_blog": [],
               "name": "",
               "sex": 2
            },
            "blog_id": 5,
            "blog_text": "save another blog from QxOrm REST API !",
            "date_creation": "2016-06-12T08:33:12",
            "list_category": [],
            "list_comment": []
         },
         {
            "author_id": {
               "author_id": "author_id_save_recursive",
               "birthdate": null,
               "list_blog": [],
               "name": "",
               "sex": 2
            },
            "blog_id": 6,
            "blog_text": "save recursive - new blog from QxOrm REST API",
            "date_creation": "2013-11-25T09:56:33",
            "list_category": [],
            "list_comment": []
         },
         {
            "author_id": {
               "author_id": "author_id_save_recursive",
               "birthdate": null,
               "list_blog": [],
               "name": "",
               "sex": 2
            },
            "blog_id": 7,
            "blog_text": "save recursive - new blog from QxOrm REST API",
            "date_creation": "2013-11-25T09:56:33",
            "list_category": [],
            "list_comment": []
         }
      ],
      "request_id": "56e3ca99-5c12-4aca-aa6c-7d0e43c1e636"
   },
   {
      "data": [
         {
            "key": 1,
            "value": {
               "author_id": {
                  "author_id": "author_id_1",
                  "birthdate": null,
                  "list_blog": [],
                  "name": "",
                  "sex": 2
               },
               "blog_id": 1,
               "blog_text": "save recursive blog from QxOrm REST API",
               "date_creation": "2013-11-25T09:56:33",
               "list_category": [],
               "list_comment": []
            }
         },
         {
            "key": 5,
            "value": {
               "author_id": {
                  "author_id": "author_id_1",
                  "birthdate": null,
                  "list_blog": [],
                  "name": "",
                  "sex": 2
               },
               "blog_id": 5,
               "blog_text": "save another blog from QxOrm REST API !",
               "date_creation": "2016-06-12T08:33:12",
               "list_category": [],
               "list_comment": []
            }
         },
         {
            "key": 6,
            "value": {
               "author_id": {
                  "author_id": "author_id_save_recursive",
                  "birthdate": null,
                  "list_blog": [],
                  "name": "",
                  "sex": 2
               },
               "blog_id": 6,
               "blog_text": "save recursive - new blog from QxOrm REST API",
               "date_creation": "2013-11-25T09:56:33",
               "list_category": [],
               "list_comment": []
            }
         },
         {
            "key": 7,
            "value": {
               "author_id": {
                  "author_id": "author_id_save_recursive",
                  "birthdate": null,
                  "list_blog": [],
                  "name": "",
                  "sex": 2
               },
               "blog_id": 7,
               "blog_text": "save recursive - new blog from QxOrm REST API",
               "date_creation": "2013-11-25T09:56:33",
               "list_category": [],
               "list_comment": []
            }
         }
      ],
      "request_id": "692968e4-8885-41ad-b918-6ce2791b3bb8"
   },
   {
      "data": [
         {
            "author_id": {
               "author_id": "author_id_1",
               "birthdate": "2019-04-02",
               "list_blog": [
                  {
                     "author_id": {
                        "author_id": "author_id_1",
                        "birthdate": null,
                        "list_blog": [],
                        "name": "",
                        "sex": 2
                     },
                     "blog_id": 5,
                     "blog_text": "save another blog from QxOrm REST API !",
                     "date_creation": "2016-06-12T08:33:12",
                     "list_category": [],
                     "list_comment": []
                  },
                  {
                     "author_id": {
                        "author_id": "author_id_1",
                        "birthdate": null,
                        "list_blog": [],
                        "name": "",
                        "sex": 2
                     },
                     "blog_id": 1,
                     "blog_text": "save recursive blog from QxOrm REST API",
                     "date_creation": "2013-11-25T09:56:33",
                     "list_category": [],
                     "list_comment": []
                  }
               ],
               "name": "author_1",
               "sex": 0
            },
            "blog_id": 1,
            "blog_text": "save recursive blog from QxOrm REST API",
            "date_creation": "2013-11-25T09:56:33",
            "list_category": [],
            "list_comment": []
         },
         {
            "author_id": {
               "author_id": "author_id_1",
               "birthdate": "2019-04-02",
               "list_blog": [
                  {
                     "author_id": {
                        "author_id": "author_id_1",
                        "birthdate": null,
                        "list_blog": [],
                        "name": "",
                        "sex": 2
                     },
                     "blog_id": 5,
                     "blog_text": "save another blog from QxOrm REST API !",
                     "date_creation": "2016-06-12T08:33:12",
                     "list_category": [],
                     "list_comment": []
                  },
                  {
                     "author_id": {
                        "author_id": "author_id_1",
                        "birthdate": null,
                        "list_blog": [],
                        "name": "",
                        "sex": 2
                     },
                     "blog_id": 1,
                     "blog_text": "save recursive blog from QxOrm REST API",
                     "date_creation": "2013-11-25T09:56:33",
                     "list_category": [],
                     "list_comment": []
                  }
               ],
               "name": "author_1",
               "sex": 0
            },
            "blog_id": 5,
            "blog_text": "save another blog from QxOrm REST API !",
            "date_creation": "2016-06-12T08:33:12",
            "list_category": [],
            "list_comment": []
         },
         {
            "author_id": {
               "author_id": "author_id_save_recursive",
               "birthdate": null,
               "list_blog": [],
               "name": "",
               "sex": 2
            },
            "blog_id": 6,
            "blog_text": "save recursive - new blog from QxOrm REST API",
            "date_creation": "2013-11-25T09:56:33",
            "list_category": [],
            "list_comment": []
         },
         {
            "author_id": {
               "author_id": "author_id_save_recursive",
               "birthdate": null,
               "list_blog": [],
               "name": "",
               "sex": 2
            },
            "blog_id": 7,
            "blog_text": "save recursive - new blog from QxOrm REST API",
            "date_creation": "2013-11-25T09:56:33",
            "list_category": [],
            "list_comment": []
         }
      ],
      "request_id": "4ffe38a6-d642-44b0-8be1-198e84256321"
   }
]




QxOrm © 2011-2023 Lionel Marty - contact@qxorm.com