Apache Adobe Flex TutorialTutoriaux Adobe Flex & AIR en Français

26fév/112

AIR SQLite – Utilisation de itemClass avec jointure SQL

Voilà un sujet "assez avancé" sur AIR et sa gestion de SQLite. Dans un des tutoriaux précédent, on a vu comment de manière assez pratique, on pouvait utiliser la propriété "itemClass" pour recevoir des objets typés directement dans un retour de SELECT:

AIR SQLite – Typer les données renvoyées par un SELECT avec itemClass

Cette pratique est à encourager car elle vous permet de typer vos résultats en faisant un mapping propriété/colonne automatique. Dans un schéma très simple, cela fonctionne sans problème mais lorsque vous allez utiliser des jointures entre tables SQL, vous allez tomber sur un os.

Le problème des jointures SQL avec itemClass

Si votre base de données est bien construite, vous aurez de multiples tables, liées entre elles par des jointures. C'était le cas dans une application développée par une entreprise chez qui je faisais une formation, qui m'a remonté cette problématique. Ayant trouvé une solution assez élégante, je la partage avec vous sur ce blog :) .

Je ne suis pas un pro du schéma de base de données alors voici en ultra-simplifié, la structure de leur BDD. Celle-ci est composée de 3 tables:

La table "historique"

La table "historique", qui est la table principale de cet exemple, contenant par ligne, un produit. Voici son contenu:

sql-1

La table "Rupture"

La table rupture est une table jointe contenant les quantités livrées:

sql-2

La jointure se fait sur le champ "cod_pro".

La table "tva"

La table "tva" contient le taux de TVA à appliquer à chaque produit. Elle contient aussi un label:

sql-3

La jointure se fait sur le champ "code_tva".

Voilà, j'ai réduit grandement la complexité de la base de donnée de base pour avoir un exemple simple.

Définition des VO (Value Object)

Avec une base comme celle-ci, il est logique de créer 3 VO, un pour chaque table. Voici ces objets:

Objet vo.Historique:


package vo {

[Bindable]
public class Historique {
public function Historique() {
}

public var cod_cli:int;
public var cod_pro:int;
public var nom_pro:String;
public var px_vte:Number;
public var code_tva:int;

}
}

Objet vo.Rupture:


package vo {

[Bindable]
public class Rupture {
public function Rupture() {
}

public var cod_pro:Number = 0;
public var date_liv:Date = null;
public var qte_rea:Number = 0;
}
}

Objet vo.TauxTVA


package vo {

[Bindable]
public class TauxTVA {
public function TauxTVA() {
}
public var code_tva:Number = 0;
public var taux:Number = 0;
public var label:String = null;
}
}

Jusque là tout va bien. On continue.

Récupération des objets Historique

Pour aller plus vite, on va prendre une application qui va ouvrir directement une base pré-remplie. Pour cela, je vais utiliser le code présenté dans l'article suivant:

AIR SQLite – Embarquer une base SQLite pré-remplie dans une application AIR

Pour l'instant, on va faire simple, on va simplement récupérer nos objets historique et les afficher dans une DataGrid en utilisant itemClass:

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml"
                        applicationComplete="windowedapplication1_applicationCompleteHandler(event)">
  <mx:Script>
    <![CDATA[
      import mx.collections.ArrayCollection;
      import mx.events.FlexEvent;

      import vo.Historique;
      private var conn:SQLConnection;

      [Bindable]
      private var _rows:ArrayCollection = null;

      protected function windowedapplication1_applicationCompleteHandler(event:FlexEvent):void {
        var dbFile:File = File.applicationDirectory.resolvePath("db/histo.db");
        // copie de la base emabarquée pour la mettre dans un répertoire en lecture / écriture
        var dbRWFile:File = File.applicationStorageDirectory.resolvePath("histoRW.db");
          // copie avec option overwrite
          dbFile.copyTo(dbRWFile, true);
        // notre base dbFile est accessible, ouverture
        conn = new SQLConnection();
        try {
          conn.open(dbRWFile);
          // on commence la transaction
          var q:SQLStatement = new SQLStatement();
          q.sqlConnection = conn;

          var sql:String = "SELECT historique.NOM_PRO,historique.PX_VTE,historique.COD_PRO,historique.cod_cli FROM historique";
          q.itemClass = Historique;
          q.text = sql;
          q.execute();
          var sqlResult:SQLResult = q.getResult();

          _rows = new ArrayCollection(sqlResult.data);
        } catch (error:SQLError) {
          trace("Error message:", error.message);
          trace("Details:", error.details);
        }

      }
    ]]>
  </mx:Script>
  <mx:DataGrid id="dg" width="100%" height="100%" dataProvider="{_rows}">
  </mx:DataGrid>
</mx:WindowedApplication>

Cela fonctionne, voici le résultat:

sql-4

C'est bien tout ça mais maintenant je veux récupérer mon taux de TVA par jointure SQL. La requête SQL qu'il me faut effectuer est la suivante:

SELECT historique.NOM_PRO,historique.PX_VTE,historique.COD_PRO,historique.cod_cli,tva.TAUX FROM (historique LEFT OUTER JOIN TVA ON tva.CODE_TVA = historique.CODE_TVA);

Voilà, on a rajouté par jointure notre champ "taux" de la table "tva".

On relance l'application pour voir la nouvelle colonne apparaître, et là, crac:

ReferenceError: Error #1056: Cannot create property taux on vo.Historique.<br /> at flash.data::SQLStatement/internalExecute()<br /> at flash.data::SQLStatement/execute()

Que s'est-il passé ? Regardons tout d'abord le résultat de la requête en debug, en supprimant l'assignation d'itemClass (car c'est elle qui pose problème):

sql-5

Dans notre objet de retour, on a un nouveau champ "taux" qui est apparu à cause de la jointure. Lorsque Flex essaie de faire le mapping entre les propriétés de la classe Historique et les propriétés de l'objet, il indique qu'il ne peut pas créer la propriété "taux" sur la classe Historique. La requête échoue donc.

Pour arranger temporairement les choses, ajoutons la propriété "taux" sur Historique:


package vo {
[Bindable]
public class Historique {
public function Historique() {
}
public var cod_cli:int;
...
// Taux de TVA par jointure
public var taux:Number = 0;
}
}

Et bien sûr, cela fonctionne:

sql-6

C'est parfait, le boulot est fini, non?

Et bien non, même si cette solution fonctionne, vous venez de rendre complètement inutile votre mapping Table/VO. En effet, si vous souhaitez récupérer le label TVA par jointure, vous devrez ajouter ce champ dans la classe Historique. Et si vous voulez faire une jointure sur un champ de la table "Rupture", vous devrez aussi le rajouter sur la classe Historique. Au final, vous allez devoir dupliqué tous les champs joints, dans la classe Historique, ce n'est pas propre et difficile à maintenir.

C'est dommage quand même de ne pas pouvoir réutiliser les VO Rupture et TauxTVA, non? Ils contiennent déjà tous les champs nécessaires et ils vous permettraient de garantir un code propre.

Ce serait beaucoup plus simple si vous pouviez avoir une classe comme celle-ci:


package vo {

[Bindable]
public class Historique {
public function Historique() {
}

public var cod_cli:int;
public var cod_pro:int;
public var nom_pro:String;
public var px_vte:Number;
public var code_tva:int;

//Jointure sur TVA
public var taux:TauxTVA = null;

//Jointure sur rupture
public var rupture:Rupture = null;

}
}

Parfait, sauf que la propriété itemClass ne permet qu'un mapping simple et nous permettra pas de remplir d'autres VO que celui précisé par la propriété "itemClass". C'est justement là que l'on va intervenir, en n'utilisant plus la propriété "itemClass" mais une méthode qui va nous permettre de faire cette correspondance.

Cette méthode fait partie d'une classe que j'ai crée qui s'appelle ModelUtils.

La classe ModelUtils

Au lieu de laisser le mécanisme "itemClass" faire la correspondance, on va la faire nous-même. Au fond, ce n'est pas très difficile, un peu d'introspection, de réflexivité et le tour est joué ;) .

Tout d'abord, il vous faut récupérer les champs joints et nous permettre de les identifier par la suite. Pour cela, on va utiliser un alias SQL défini par un opérateur "AS". Celui-ci va aussi nous permettre d'identifier le VO à remplir.

La syntaxe globale est la suivante:

[SUB_VO_PROPERTY_NAME][SEPARATOR][VO_PROPERTY_NAME]

Où:

  • [SUB_VO_PROPERTY_NAME]: nom la propriété dans laquelle est conservé notre VO dans la classe principale. Pour le VO TauxTVA, c'est par exemple "taux" qui se trouve dans Historique.as
  • [SEPARATOR]: un simple séparateur, dans mon cas j'ai pris la chaîne "__" (double underscore). Cette propriété est configurable dans la classe ModelUtils, la valeur par défaut est "__";
  • [VO_PROPERTY_NAME]: la propriété du VO secondaire à remplir, issu de la jointure.

Par exemple, pour notre requête avec notre taux de TVA et le label TVA, on a:

SELECT historique.NOM_PRO,historique.PX_VTE,historique.COD_PRO,historique.cod_cli,tva.TAUX as taux__taux,tva.label as taux__label FROM (historique LEFT OUTER JOIN TVA ON tva.CODE_TVA = historique.CODE_TVA)

Cette fois, on ne passe pas d'itemClass mais on prend le SQLResult et on le passe dans la méthode fillObjectsFromSQLResult de ModelUtils:

q.execute();
var sqlResult:SQLResult = q.getResult();
var rows:Array = ModelUtils.fillObjectsFromSQLResult(sqlResult, Historique);

La méthode "fillObjectsFromSQLResult" a la signature suivante:

static function fillObjectsFromSQLResult(sqlResult:SQLResult, clazz:Class):Array

Cette méthode va nous renvoyer un tableau d'objet Historique, dont les propriétés jointes seront remplies:

sql-7

Et voilà! Votre objet Historique contient un objet de type TauxTVA contenant les valeurs de jointure provenant de la table "tva". On peut faire de même avec l'objet rupture, c'est aussi simple.

Finalisation de l'exemple

Si vous utilisez votre donnez dans une List, avec un itemRenderer, vous pouvez donc accéder à votre label de TVA avec "data.taux.label". Dans notre cas, on a une DataGrid. Pour afficher des champs en particulier, il va falloir préciser les DataGridColumn à afficher et pour pouvoir afficher une "sous-propriété", on va utiliser une petite labelFunction.

Voici le code final de notre application:

<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml"
                        applicationComplete="windowedapplication1_applicationCompleteHandler(event)">
  <mx:Script>
    <![CDATA[
      import mx.collections.ArrayCollection;
      import mx.events.FlexEvent;

      import utils.ModelUtils;

      import vo.Historique;
      private var conn:SQLConnection;

      [Bindable]
      private var _rows:ArrayCollection = null;

      protected function windowedapplication1_applicationCompleteHandler(event:FlexEvent):void {
        var dbFile:File = File.applicationDirectory.resolvePath("db/histo.db");
        // copie de la base emabarquée pour la mettre dans un répertoire en lecture / écriture
        var dbRWFile:File = File.applicationStorageDirectory.resolvePath("histoRW.db");
        // copie avec option overwrite
        dbFile.copyTo(dbRWFile, true);
        // notre base dbFile est accessible, ouverture
        conn = new SQLConnection();
        try {
          conn.open(dbRWFile);
          // on commence la transaction
          var q:SQLStatement = new SQLStatement();
          q.sqlConnection = conn;

          var sql:String = "SELECT historique.NOM_PRO,historique.PX_VTE,historique.COD_PRO,historique.cod_cli,tva.TAUX as taux__taux,tva.label as taux__label FROM (historique LEFT OUTER JOIN TVA ON tva.CODE_TVA = historique.CODE_TVA)";
          //q.itemClass = Historique;
          q.text = sql;
          q.execute();
          var sqlResult:SQLResult = q.getResult();
          var rows:Array = ModelUtils.fillObjectsFromSQLResult(sqlResult, Historique);
          _rows = new ArrayCollection(rows);
        } catch (error:SQLError) {
          trace("Error message:", error.message);
          trace("Details:", error.details);
        }

      }
    ]]>
  </mx:Script>
  <mx:DataGrid id="dg" width="100%" height="100%" dataProvider="{_rows}">
    <mx:columns>
      <mx:DataGridColumn dataField="cod_cli" headerText="Code Client" />
      <mx:DataGridColumn dataField="cod_pro" headerText="Code Produit" />
      <mx:DataGridColumn dataField="nom_pro" headerText="Nom Produit" />
      <mx:DataGridColumn dataField="px_vte" headerText="Prix de vente" />
      <mx:DataGridColumn dataField="code_tva" headerText="Code TVA" />
      <mx:DataGridColumn headerText="Taux TVA">
        <mx:labelFunction>
          {function(item:Object, dgc:DataGridColumn):String{return (Historique(item).taux.taux.toString())}}
        </mx:labelFunction>
      </mx:DataGridColumn>
      <mx:DataGridColumn headerText="Label TVA">
        <mx:labelFunction>
          {function(item:Object, dgc:DataGridColumn):String{return (Historique(item).taux.label)}}
        </mx:labelFunction>
      </mx:DataGridColumn>
    </mx:columns>
  </mx:DataGrid>
</mx:WindowedApplication>

Et voici le résultat:

sql-8

Développement de la classe ModelUtils

La classe ModelUtils n'est pas immense, 60 lignes en tout et pourtant, elle nous permet d'implémenter une fonctionnalité absente du framework Flex / AIR.

Voici le code complet de cette classe:

package utils {
  import flash.data.SQLResult;
  import flash.net.getClassByAlias;
  import flash.utils.describeType;
  import flash.utils.getDefinitionByName;
  import flash.utils.getQualifiedClassName;

  public class ModelUtils {
    public function ModelUtils() {
    }

    public static var JOIN_SEPARATOR:String = "__";

    public static var describes:Array = [];

    public static var accessorCache:Array = [];

    public static function fillObjectsFromSQLResult(sqlResult:SQLResult, clazz:Class):Array {
      var cads:Array = [];
      for each (var o:Object in sqlResult.data) {
        var currentInstance:Object = new clazz();
        fillMe(o, currentInstance);
        cads.push(currentInstance);
      }
      return cads;
    }

    public static function fillMe(data:Object, model:Object):void {
      for (var key:String in data) {
        if (key.indexOf(JOIN_SEPARATOR) != -1) {
          var split:Array = key.split(JOIN_SEPARATOR);
          var subObjectName:String = split[0];
          var subObjectKey:String = split[1];
          var subO:Object = model[subObjectName];
          if (!subO) {
            // va contenir le nom de classe "model"
            var modelClassName:String = getQualifiedClassName(model);
            var cachedValue:XML = describes[modelClassName] as XML;
            if (!cachedValue) {
              cachedValue = describes[modelClassName] = describeType(model);
            }
            var accessorTypeId:String = modelClassName + "_" + subObjectName;
            var cachedAccessorType:String = accessorCache[accessorTypeId];
            if (!cachedAccessorType) {
              cachedAccessorType = accessorCache[accessorTypeId] = cachedValue..accessor.(@name == subObjectName)[0].
                @type;
            }
            var subClass:Class = getDefinitionByName(cachedAccessorType) as Class;
            model[subObjectName] = new subClass();
            model[subObjectName][subObjectKey] = data[key];
          } else {
            subO[subObjectKey] = data[key];
          }
        } else {
          model[key] = data[key];
        }
      }
    }
  }
}

Quelques explications sur ModelUtils

Cette classe contient donc deux méthodes static. La première, "fillObjectsFromSQLResult" va parcourir tous les enregistrements qui sont revenus de la database depuis l'objet SQLResult. Pour chaque résultat (non-typé), on va appeler la méthode "fillMe" qui prend la classe Model et la donnée à faire correspondre.

On commence par parcourir toutes les propriétés de l'objet avec un for (var … in …) qui va boucler sur le clés de l'objet. Première étape, on regarde si la clé contient le séparateur "__" qui nous indique que l'on doit faire une traitement spécial. Si ce n'est pas le cas, on fait une assignation simple model /data comme le ferait AIR en interne avec itemClass.

Si nous nous trouvons sur une valeur spéciale, on va utiliser plusieurs méthodes de Flash Player permettant l'introspection et la réflexivité:

Ce traitement est réalisé pour chaque enregistrement, pour chaque champ. Afin de ne pas ralentir les opérations, j'ai mis en place 2 caches static (describes et accessorCache) qui permettent de ne réaliser ces opérations qu'une fois par Model. Comme les méthodes / propriétés de ModelUtils sont static, vous profiterez toujours de ce cache peu importe d'où le code provient.

Voilà, je crois que cette méthode ne peut pas être plus optimisée mais si vous avez des modifications à apporter, n'hésitez pas à m'en faire part dans les commentaires!

Télécharger le projet au format FXP

package utils {
import flash.data.SQLResult;
import flash.net.getClassByAlias;
import flash.utils.describeType;
import flash.utils.getDefinitionByName;
import flash.utils.getQualifiedClassName; 

public class ModelUtils {
public function ModelUtils() {
}

public static var JOIN_SEPARATOR:String = "__";

public static var describes:Array = [];

public static var accessorCache:Array = [];

public static function fillObjectsFromSQLResult(sqlResult:SQLResult, clazz:Class):Array {
var cads:Array = [];
for each (var o:Object in sqlResult.data) {
var currentInstance:Object = new clazz();
fillMe(o, currentInstance);
cads.push(currentInstance);
}
return cads;
}

public static function fillMe(data:Object, model:Object):void {
for (var key:String in data) {
if (key.indexOf(JOIN_SEPARATOR) != -1) {
var split:Array = key.split(JOIN_SEPARATOR);
var subObjectName:String = split[0];
var subObjectKey:String = split[1];
var subO:Object = model[subObjectName];
if (!subO) {
// va contenir le nom de classe "model"
var modelClassName:String = getQualifiedClassName(model);
var cachedValue:XML = describes[modelClassName] as XML;
if (!cachedValue) {
cachedValue = describes[modelClassName] = describeType(model);
}
var accessorTypeId:String = modelClassName + "_" + subObjectName;
var cachedAccessorType:String = accessorCache[accessorTypeId];
if (!cachedAccessorType) {
cachedAccessorType = accessorCache[accessorTypeId] = cachedValue..accessor.(@name == subObjectName)[0].
@type;
}
var subClass:Class = getDefinitionByName(cachedAccessorType) as Class;
model[subObjectName] = new subClass();
model[subObjectName][subObjectKey] = data[key];
} else {
subO[subObjectKey] = data[key];
}
} else {
model[key] = data[key];
}
}
}
}
}

Articles similaires

Commentaires (2) Trackbacks (0)
  1. Salut Fabien,

    Merci encore pour cette classe.
    Cependant, après pas mal d'utilisation, elle n'a qu'une seule limite, c'est lorsque tu veux faire une jointure entre deux grosses tables (plus de 30 champs), et que tu veux ramener tous les champs des deux tables.
    La première table, "historique.*", ça va, mais c'est sur la deuxième que c'est "galère", il faut tout réécrire.
    Tu crois qu'il y a moyen de récupérer tous les champs de la deuxième table ?
    Du genre, une requête : SELECT historique.*, tva.* as tva__* FROM (historique LEFT OUTER JOIN TVA ON tva.CODE_TVA = historique.CODE_TVA) , comment récupérer tous les champs des autres tables jointes et les implémenter dans historique.tva ?

    Mais en attendant, c'est plus rapide quand même d'utiliser ta méthode que d'effectuer la requête en deux fois (une fois pour alimenter historique, et la deuxième en parcourant historique et retrouver l'objet TVA associé), ça peut juste devenir long à écrire.

    Tristan

  2. Salut Tristan, j'espère que le dev se passe bien de ton côté.
    Ce serai une bonne amélioration effectivement mais on ne peut pas connaitre la liste des champs de la table comme cela avec *, il faut soit que ton code connaisse la liste des champs, soit faire une requête pr les récupérer ;)

    Fabien


Leave a comment

(required)

Aucun trackbacks pour l'instant