LCDS, manipulation parent / enfants

  • warning: array_map(): Argument #2 should be an array in /var/www/titouille.ch/www/modules/system/system.module on line 1050.
  • warning: array_keys() expects parameter 1 to be array, null given in /var/www/titouille.ch/www/includes/theme.inc on line 1845.
  • warning: Invalid argument supplied for foreach() in /var/www/titouille.ch/www/includes/theme.inc on line 1845.
Portrait de titouille

Il y a déjà quelques jours que je travaille avec Live Cycle Data Services. On trouve facilement de la documentation et des exemples simples sur le sujet, mais les configurations sont relativement complexes et dès qu'on rentre dans des domaines spécifiques, il est difficile de trouver des explications, que ça soit dans la documentation officielle ou sur les forums spécialisés sur le sujet.

C'est pourquoi je vais écrire quelques billets sur le sujet, au fur et à mesure de mes avancées.

Lorsque j'ai débuté avec Live Cycle, j'ai pris la peine de lire pas mal d'explications et de trouvailles à ce propos sur le net, au fil des blogs, des forums et des sites.

Après avoir fait mes premiers essais avec LCDS, concernant la récupération de données simple et la mise à jour, il m'est venu à l'esprit l'idée suivante, pour aller dans le sens de mes développements.

Mes besoins sont les suivants : j'ai deux tables en relation parent / enfants (one-to-many) dans ma base de données. La première table concerne des gabarits, et la seconde correspond aux items de ces gabarits. Chaque gabarit peut avoir 0, un ou plusieurs items.

Coté Flex, un treeView s'occupe d'afficher les données gabarits, tout en récupérant les items de ces gabarits pour chaque gabarit. A partir de ce treeView, un drag'n'drop peut s'opérer, et les items du gabarit sélectionné sont utilisés au moment du relâchement sur un composant particulier. Ils permettent de remplir ce composant au niveau visuel, en affecter les items.

Mon but est le suivant : permettre, à partir de Flex uniquement, d'effectuer toutes les modifications que j'appliquerais aux gabarits, ainsi qu'à leurs items respectifs, le tout en utilisant un minimum de requêtes serveur. Même mieux, sans avoir besoin de développer des requêtes particulières côté serveur.

Les possibilités que je désire sont les suivantes :

  1. ajouter un nouveau gabarit, sans items
  2. ajouter un nouveau gabarit, ainsi que de nouveaux items
  3. mettre à jour un gabarit (son nom)
  4. mettre à jour les items d'un gabarit (supprimer tous ses items et lui en affecter de nouveaux)
  5. supprimer un gabarit, en supprimant également ses items respectifs, le tout en cascade

Il a fallu pas mal de tests pour arriver à faire fonctionner correctement ces possibilités, mais voilà le résultat de mes recherches.


Mise en place de la base de données

Tout d'abord, je crée ma base de données, sur un serveur MySQL 5 (5.0.51a pour être précis) :

-- MySQL Administrator dump 1.4
--
-- ------------------------------------------------------
-- Server version	5.0.51a-community-nt
 
 
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
 
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
 
 
--
-- Create schema cineact
--
 
CREATE DATABASE IF NOT EXISTS cineact;
USE cineact;
 
--
-- Definition of table `gabarit`
--
 
DROP TABLE IF EXISTS `gabarit`;
CREATE TABLE `gabarit` (
  `gabaritid` int(10) unsigned NOT NULL auto_increment,
  `gabaritname` varchar(45) NOT NULL,
  `gabaritdesc` varchar(255) default NULL,
  `gabarittype` varchar(5) NOT NULL,
  PRIMARY KEY  (`gabaritid`)
) ENGINE=InnoDB AUTO_INCREMENT=37 DEFAULT CHARSET=latin1;
 
--
-- Dumping data for table `gabarit`
--
 
/*!40000 ALTER TABLE `gabarit` DISABLE KEYS */;
INSERT INTO `gabarit` (`gabaritid`,`gabaritname`,`gabaritdesc`,`gabarittype`) VALUES 
 (32,'nouveau gabarit',NULL,'week'),
 (33,'nouveau gabarit test',NULL,'week'),
 (34,'mon test',NULL,'week'),
 (36,'nouveau gabarit mois',NULL,'day');
/*!40000 ALTER TABLE `gabarit` ENABLE KEYS */;
 
 
--
-- Definition of table `gabarititem`
--
 
DROP TABLE IF EXISTS `gabarititem`;
CREATE TABLE `gabarititem` (
  `gabarititemid` int(10) unsigned NOT NULL auto_increment,
  `gabaritid` int(10) unsigned NOT NULL,
  `gabarititemday` int(10) unsigned NOT NULL,
  `gabarititemhour` time NOT NULL,
  `gabarititemduration` time NOT NULL,
  PRIMARY KEY  (`gabarititemid`),
  KEY `gabaritid` (`gabaritid`),
  CONSTRAINT `gabaritid` FOREIGN KEY (`gabaritid`) REFERENCES `gabarit` (`gabaritid`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=77 DEFAULT CHARSET=latin1;
 
--
-- Dumping data for table `gabarititem`
--
 
/*!40000 ALTER TABLE `gabarititem` DISABLE KEYS */;
INSERT INTO `gabarititem` (`gabarititemid`,`gabaritid`,`gabarititemday`,`gabarititemhour`,`gabarititemduration`) VALUES 
 (28,32,2,'11:00:00','01:00:00'),
 (29,32,2,'05:00:00','02:00:00'),
 (30,32,2,'10:00:00','01:00:00'),
 (31,32,2,'04:00:00','02:00:00'),
 (32,32,2,'05:30:00','02:30:00'),
 (33,32,2,'04:00:00','01:00:00'),
 (34,32,2,'09:00:00','01:00:00'),
 (35,33,2,'05:00:00','02:00:00'),
 (36,33,2,'11:00:00','01:00:00'),
 (37,33,2,'05:30:00','02:30:00'),
 (38,33,2,'10:00:00','01:00:00'),
 (39,33,2,'04:00:00','01:00:00'),
 (40,33,2,'04:00:00','02:00:00'),
 (41,33,2,'09:00:00','01:00:00'),
 (56,34,2,'05:30:00','02:30:00'),
 (57,34,2,'09:00:00','01:00:00'),
 (58,34,2,'04:00:00','01:00:00'),
 (59,34,2,'05:00:00','02:00:00'),
 (60,34,2,'10:00:00','01:00:00'),
 (61,34,2,'11:00:00','01:00:00'),
 (62,34,2,'04:00:00','02:00:00'),
 (70,36,0,'10:00:00','01:00:00'),
 (71,36,0,'05:30:00','02:30:00'),
 (72,36,0,'04:00:00','01:00:00'),
 (73,36,0,'09:00:00','01:00:00'),
 (74,36,0,'11:00:00','01:00:00'),
 (75,36,0,'05:00:00','02:00:00'),
 (76,36,0,'04:00:00','02:00:00');
/*!40000 ALTER TABLE `gabarititem` ENABLE KEYS */;
 
 
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;

Ma contrainte "cascade" concernant le point numéro 5 est déjà en place grâce à la base de données : CONSTRAINT `gabaritid` FOREIGN KEY (`gabaritid`) REFERENCES `gabarit` (`gabaritid`) ON DELETE CASCADE ON UPDATE CASCADE

ça me permet de spécifier que lorsqu'un gabarit est supprimé, tous les items rattachés à ce gabarit seront également supprimés.

Maintenant que ma base de données est en place, je peux commencer le développement à proprement dit.

Côté Serveur

Du côté du serveur, j'utilise hibernate (lcds oblige). Lors d'un développement avec java et hibernate, il est nécessaire d'avoir différents points de configuration mis en place.


Erreur de requête

Pour éviter un problème du genre :
net.sf.hibernate.PropertyAccessException: exception setting property value with CGLIB (set hibernate.cglib.use_reflection_optimizer=false for more info), je crée tout d'abord un fichier nommé "hibernate.properties" que je place à la racine du serveur par défaut (chez moi, c:\flash\lcds\jrun4\servers\default) et j'y ajoute la ligne suivante :

hibernate.cglib.use_reflection_optimizer false


Configuration du serveur

Dans hibernate, il existe différents types d'assembleurs avec lequels nous pouvons travailler :

  • sql assembler
  • hibernate assembler
  • java assembler

Chaque assembleur est utilisé de manière spécifique selon les besoins du développeur.

Un assembleur SQL permet de créer des requêtes SQL pour manipuler la base de données, sans avoir besoin d'écrire une seule ligne de code en Java. Il suffit d'appeler la requête désirée et de lui passer les paramètres adéquats afin de l'exécuter sur la base de données.

Un assembleur hibernate est une sorte de mix entre l'assembleur SQL et l'assembleur Java. C'est à partir d'un VO et d'un fichier de mapping XML que nous autorisons différentes possibilités à notre assembleur. La configuration se doit d'être très précise sous peine de se retrouver avec plein d'erreurs et des comportements un peu bizarre. L'utilisation d'assembleurs hibernate permet, avec peu de connaissances Java, de développer des objets plus ou moins puissants par rapport aux besoins requis.

Enfin, un assembleur Java est une classe Java dans laquelle il est possible de coder les comportements à adopter selon les méthodes appelées côté client. C'est surement le type d'assembleur le plus puissant pour celui qui connait bien les framework Java. Ce dernier permet de coder ce que nous désirons dans nos méthodes afin de traiter les données.

Actuellement, par rapport au premier tutorial que j'ai suivi, j'utilise les assembleurs hibernate.

Lors de l'utilisation des assembleurs hibernate, il est nécessaire d'avoir différents fichier de configuration. Le premier doit se situer à la racine du répertoire "classes" (chez moi, C:\dev\flash\lcds\jrun4\servers\default\flex\WEB-INF\classes) et se nomme hibernate.cfg.xml. Ce premier fichier contient la configuration envers la base de données, le connecteur utilisé, quelques autres variables de configuration ainsi que les fichiers de mappings liés à hibernate :

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
	<session-factory>
 
		<!-- Database connection settings -->
		<property name="connection.driver_class">com.mysql.jdbc.Driver</property>
		<property name="connection.url">jdbc:mysql://localhost/sampledb</property>
		<property name="connection.username">root</property>
		<property name="connection.password">monMotDePasse</property>
 
		<!-- JDBC connection pool (use the built-in) -->
		<property name="connection.pool_size">1</property>
 
		<!-- SQL dialect -->
		<property name="dialect">org.hibernate.dialect.MySQLDialect</property>
 
		<!-- Enable Hibernate's automatic session context management -->
		<property name="current_session_context_class">thread</property>
 
		<!-- Disable the second-level cache  -->
		<property name="cache.provider_class">org.hibernate.cache.NoCacheProvider</property>         
		<!-- Echo all executed SQL to stdout -->
		<property name="show_sql">true</property>
 
		<!-- Load the database table mapping file -->
		<mapping resource="com/myapp/app/vo/GabaritVO.hbm.xml"/>
		<mapping resource="com/myapp/app/vo/GabaritItemVO.hbm.xml"/>
	</session-factory>
</hibernate-configuration>

J'ai également copié le contenu du répertoire C:\dev\flash\lcds\resources\hibernate ainsi que mon connecteur JDBC (mysql-connector-java-5.0.8-bin.jar, que vous pourrez trouver ici) dans le répertoire C:\dev\flash\lcds\jrun4\servers\default\flex\WEB-INF\lib

Les noeuds xml "mapping" seront expliqués plus bas.

Une fois ce premier fichier ainsi que les jar mis en place, je peux créer mes classes Java :

Création des classe Java

Le but de mes classes, c'est d'avoir ce qu'on appelle un "Value Object", plus communément appelé VO. Cet objet est simplement une représentation "object" d'une table dans ma base de données.

Voici mes 2 classes Java :

GabaritVO.java

package com.myapp.app.vo;
 
import java.util.Set;
 
 
/**
 * @author titouille
 * 
 */
public class GabaritVO
{
 
	private Integer id;
	private String name;
	private String desc;
	private String type;
	private Set gabaritItems;
 
 
	/**
	 * @return the id
	 */
	public Integer getId()
	{
		return id;
	}
	/**
	 * @param id the id to set
	 */
	public void setId(Integer id)
	{
		this.id = id;
	}
 
 
	/**
	 * @return the name
	 */
	public String getName()
	{
		return name;
	}
	/**
	 * @param name the name to set
	 */
	public void setName(String name)
	{
		this.name = name;
	}
 
 
	/**
	 * @return the desc
	 */
	public String getDesc()
	{
		return desc;
	}
	/**
	 * @param desc the desc to set
	 */
	public void setDesc(String desc)
	{
		this.desc = desc;
	}
 
 
	/**
	 * @return the type
	 */
	public String getType()
	{
		return type;
	}	
	/**
	 * @param type the type to set
	 */
	public void setType(String type)
	{
		this.type = type;
	}
 
 
	/**
	 * @return the gabaritItems
	 */
	public Set getGabaritItems()
	{
		return gabaritItems;
	}
	/**
	 * @param gabaritItems the gabaritItems to set
	 */
	public void setGabaritItems(Set gabaritItems)
	{
		this.gabaritItems = gabaritItems;
	}
}

GabaritItemVO.java

package com.myapp.app.vo;
 
import java.sql.Time;
 
 
/**
 * @author titouille
 */
public class GabaritItemVO
{
	private Integer id;
	private GabaritVO gabaritid;
	private Integer day;
	private Time hour;
	private Time duration;
 
	/**
	 * @return the id
	 */
	public Integer getId()
	{
		return id;
	}
 
	/**
	 * @param id the id to set
	 */
	public void setId(Integer id)
	{
		this.id = id;
	}
 
	/**
	 * @return the gabaritid
	 */
	public GabaritVO getGabaritid()
	{
		return gabaritid;
	}
 
	/**
	 * @param gabaritid the gabaritid to set
	 */
	public void setGabaritid(GabaritVO gabaritid)
	{
		this.gabaritid = gabaritid;
	}
 
	/**
	 * @return the day
	 */
	public Integer getDay()
	{
		return day;
	}
 
	/**
	 * @param day the day to set
	 */
	public void setDay(Integer day)
	{
		this.day = day;
	}
 
	/**
	 * @return the hour
	 */
	public Time getHour()
	{
		return hour;
	}
 
	/**
	 * @param hour the hour to set
	 */
	public void setHour(Time hour)
	{
		this.hour = hour;
	}
 
	/**
	 * @return the duration
	 */
	public Time getDuration()
	{
		return duration;
	}
 
	/**
	 * @param duration the duration to set
	 */
	public void setDuration(Time duration)
	{
		this.duration = duration;
	}
}

Elles sont très simples : un getter et un setter pour chaque colonne de table. Il faut néanmoins souligner que pour la classe GabaritItemVO.java, la variable ainsi que les getter/setter de la colonne gabaritid, qui représente la clé étrangère de ma table gabarititem, n'est pas sous la forme d'un "Integer" mais sous la forme d'un "GabaritVO".

Au départ, elle était sous la forme d'un Integer, mais après différents tests, la console lcds me retournait des erreurs en me disant que cette colonne n'était pas dans le bon format. C'est après avoir fait des recherches sur le net que j'ai pu comprendre que son type devait être du type de la classe parent que je suis arrivé à cette construction, et à partir de là c'était fonctionnel. Cette modification est due au fait que je vais mapper la collection "enfant" à son parent, nous allons le voir avec les fichiers de mapping.

Ces 2 fichiers sont des fichiers source Java. Dans un précédent tutorial j'ai expliqué comment installer un jdk spécifique pour compiler des classes Java exploitables par lcds, et surtout pour que la compilation les place directement dans le répertoire où elles doivent se trouver au final, c'est à dire C:\dev\flash\lcds\jrun4\servers\default\flex\WEB-INF\classes\.
Mes classes sont inclues dans un package spécifique, et la compilation viendra les placer sous C:\dev\flash\lcds\jrun4\servers\default\flex\WEB-INF\classes\com\myapp\app\vo, tel que je le désire.


La partie sensible, les fichiers de mapping

A deux reprises, j'ai pu faire allusion aux fichiers de mapping. Nous y arrivons enfin... Tout d'abord, qu'est-ce que c'est ?? Un fichier de mapping permet de relier un VO à son équivalent dans la base de données, c'est à dire une table. Le mapping permet de faire une liaison entre les colonnes de la table et les variables du VO, mais il permet également de spécifier quelles sont les liaisons sous-jacentes, par exemple les relations entre tables, les type de données, etc... Dans la section concernant le fichier hibernate.cfg.xml, nous avons pu voir que 2 mappings étaient opérés sur les classes GabaritVO et GabaritItemVO. Voici maintenant le contenu des fichiers de mapping, ainsi que quelques explications sur mes pérégrinations dans le monde d'hibernate.

GabaritVO.hbm.xml

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
		"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
		"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="com.myapp.app.vo">
 
	<class name="GabaritVO" table="gabarit">
		<id name="id" column="gabaritid" type="java.lang.Integer" unsaved-value="0">
			<generator class="native"/>
		</id>
		<property name="name" column="gabaritname" />
		<property name="desc" column="gabaritdesc" />
		<property name="type" column="gabarittype" />
 
		<set name="gabaritItems" inverse="true" cascade="all-delete-orphan">
			<key column="gabaritid" />
			<one-to-many class="GabaritItemVO" />
		</set>
	</class>
 
	<query name="all.gabarits">From GabaritVO</query>
</hibernate-mapping> 

GabaritItemVO.hbm.xml

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
		"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
		"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="com.myapp.app.vo">
 
	<class name="GabaritItemVO" table="gabarititem">
		<id name="id" column="gabarititemid" unsaved-value="0">
			<generator class="native"/>
		</id>
 
		<many-to-one 
			name="gabaritid"
			class="GabaritVO"
			column="gabaritid"
			not-null="true" />
 
		<property name="day" column="gabarititemday" />
 
		<property name="hour" column="gabarititemhour" />
 
		<property name="duration" column="gabarititemduration" />
 
	</class>
 
 
	<query name="all.gabarititems">From GabaritItemVO</query>
 
</hibernate-mapping> 

Ce sont les fichiers de mapping qui font la grande part du travail dans les assembleurs hibernate.

Si je décompose un peu le contenu de GabaritVO.hbm.xml, nous pouvons voir les éléments suivants :


  1. une balise hibernate-mapping qui déclare le mapping. Cette première balise contient un attribut package qui permet de spécifier le package de mon VO. Cet attribut est intéressant car il évite de devoir affecter un nom complet à chaque fois qu'on doit faire une liaison avec un autre VO situé dans le même package

  2. une balise class qui défini la classe ciblée par le fichier de mapping (attribut name) ainsi que la table de la base de données qui y est liée (attribut table)

  3. vient ensuite une balise id qui représente l'identifiant de mon VO / ma table. L'attribut id doit correspondre à l'identifiant de mon VO, tandis que l'attribut column correspond à la colonne identifiant de ma base de données. L'attribut type, qui n'est pas réellement nécessaire ici puisqu'il est défini dans mon VO, représente le type de données de l'identifiant dans mon VO. Enfin, pas des moindres, l'attribut unsaved-value est un attribut qui a une importance capitale dans la construction de mon système. Il représente la valeur affectée à un nouvel item côté client, qui n'existe pas dans la base de donnée lors de son insertion dans l'application Flex. J'y reviendrai plus tard avec de plus amples explications.
    La balise id contient elle-même une balise generator car elle détermine un identifiant unique dans ma base de données. l'attribut class est ici de type native, ce qui implique qu'hibernate doit détecter que, pour une base de données MySQL, le type est auto-incrément et agir en conséquence. Pour de plus amples informations sur les générateurs, vous pouvez vous reporter à la documentation officielle hibernate

  4. Plusieurs balises property viennent ensuite, qui représentent les autres variables de mon VO / colonnes de ma table. Chacune possède un attribut name qui correspond au nom de la variable dans mon VO, ainsi qu'un attribut column qui fait la liaison avec la bonne colonne dans la base de données.

  5. Nous arrivons enfin sur la partie intéressante de mon projet, c'est à dire l'association avec la collection d'items appartenant à un gabarit.

    Cette association, coté "parent", se fait avec une balise set. Cette balise permet de spécifier qu'une autre classe (en l'occurrence la classe GabaritItemVO spécifiée par la sous-balise one-to-many) est une collection de la classe GabaritVO. Nous parlons bien de classes et non pas de tables, il faut bien faire la distinction.
    La sous-balise key, grâce à son attribut column désigne quelle est la colonne de clé étrangère dans la classe "enfant".
    La sous-balise one-to-many, via son attribut class, permet de définir quelle est la classe qui fera office de classe "enfant".

    Cette balise set possède différents attributs permettant de définir de manière précise le comportement de la collection à mettre en relation.

    Tout d'abord, l'attribut name correspond au nom de la relation. Il n'a pas d'incidence particulière sur l'association.

    L'attribut inverse est passé à "true", ce qui permet d'informer Hibernate que pour renseigner les infos de cette association, il devra prendre les valeurs dans la classe associées, ici GabaritItemVO. Dans ma construction, cet attribut a son importance.

    Enfin, l'attribut cascade permet de spécifier quelles opérations doivent êtres propagées depuis le "parent" vers ses "enfants". Ici, l'attribut est valorisé à "all-delete-orphan", ce qui indique a hibernate que lorsqu'un élément de type enfant est supprimé de son parent, il doit être supprimé de la base de données.
    Cet attribut est important, car le comportement par défaut d'hibernate lors de la suppression d'un enfant côté client, est de simplement le détacher de son parent côté serveur (supression de la clé étrangère dans la table enfant), sans pour autant le supprimer de la table. Dans mon cas, il est nécessaire qu'un item de gabarit supprimé le soit totalement, qu'il n'existe plus du tout dans la base de données. C'est pour cette raison que la valeur "all-delete-orphan" est spécifiée ici.


  6. Enfin, après la spécification de la classe, nous trouvons une balise query qui caractérise une méthode "HQL". Tout comme les bases de données ont leur langage de requête, le SQL, hibernate possède son propre langage de requête, nommé HQL. Ce dernier permet de créer des requêtes dans un format proche du format SQL, mais est basé sur les classes. Notez bien ici que la requête s'effectue sur la classe et non plus sur la table ("From GabaritVO", non pas "From Gabarit"). Cette subtilité peut rapidement prêter à confusion lorsqu'on ne la connait pas.

Voilà les caractéristiques de ma classe GabaritVO. Il a fallu maints tests afin d'arriver à ce mapping précis, et bien des messages d'erreurs m'ont obligé à me plonger dans la documentation, faire des recherches, poser des questions sur des forums spécialisés (toutes restées malheureusement sans réponses, dur de se lancer dans des technologies de pointe...).

Nous pouvons passer maintenant à la classe GabaritItemVO. Je ne vais pas la commenter autant que la classe GabaritVO, car beaucoup de points se rejoignent, mais je vais passer un peu de temps à expliquer les points essentiels, qui réside dans la balise many-to-one.

Cette balise many-to-one représente la variable "gabaritid" de ma classe GabaritItemVO.java, ou autrement dit ma clé étrangère dans la table GabaritItem.

Si vous vous souvenez, lors de mon explication sur les classes Java, j'avais mis en avant le fait que la variable gabaritid n'était pas sous la forme d'un Integer comme nous aurions pu le croire, mais sous la forme de ma classe GabaritVO. Cette configuration étant requise par hibernate, sous peine de me retourner des messages d'erreur lors de mes tests avec Flex.
Comme nous pouvons le voir ici dans la balise, l'attribut class correspond à ma classe GabaritVO.
L'attribut name représente le nom de ma variable dans la classe GabaritItemVO.java, tandis que l'attribut column représente, comme à son habitude, la relation avec la colonne dans la base de données.
Enfin, un attribut supplémentaire est affecté ici : l'attribut not-null. Ici encore, cet attribut est d'une importance capitale dans ma construction. Il indique à hibernate que cette variable ne peut être nulle. En d'autres termes, il est en relation direct avec l'attribut cascade de la collection (set) linkée dans le fichier de mapping du parent, GabaritVO.hbm.xml. Cet attribut permet de définir que la variable (de type "clé étrangère") gabaritid ne peut pas être nulle, ce qui implique que si hibernate, lors de ses opérations, mets cette valeur à null, il doit tout simplement supprimer le tuple correspondant dans la base de données. ça n'a l'air de rien, mais c'est un attribut très important pour hibernate.

Les fichiers de mapping doivent se trouver au même endroit que leurs classes respectives, donc dans le répertoire C:\dev\flash\lcds\jrun4\servers\default\flex\WEB-INF\classes\com\myapp\app\vo.

Voilà en ce qui concerne les explications des mappings. On ne dirait pas, mais il suffit d'un oubli dans tous ces paramètres et le comportement d'hibernate peut changer du tout au tout.


De l'importance de l'attribut unsaved-value

Souvenez-vous : lorsque j'ai détaillé le fichier de mapping GabaritVO.hbm.xml, j'ai souligné que l'attribut unsaved-value était d'une importance capitale dans la construction de mon système. Je vais maintenant vous expliquer pourquoi.

Lors de mes nombreuses lectures sur le sujet "hibernate", je suis tombé sur un exemple de mapping où était utilisé cet attribut, avec une valeur de "-1". Je me suis dit : tiens, voilà qui est intéressant... Je peux spécifier, lorsque je crée un nouvel élément, une valeur de -1 pour avertir hibernate que cet élément précis n'est pas encore existant dans la base de données, et qu'il doit donc l'insérer. J'ai ajouté cet attribut dans mes classes, avec une valeur de -1 comme je l'avais vu dans ce fameux exemple, et j'ai continué à faire mes tests.

Dans mon système, tout était presque fonctionnel. Je pouvais ajouter un nouvel gabarit sans items, supprimer un gabarit existant avec suppression en cascade des items, écraser les items par un autre tableau d'items. Dans ce dernier point néanmoins, un comportement un peu bizarre se reproduisait sans que je puisse y faire quoi que ce soit : lorsque j'écrasais les items par des nouveaux, pour chaque nouvel item inséré, hibernate effectuait un select sur la table gabaritItem. Ce n'était pas critique, mais un peu génant, car ça multipliait les requêtes côté serveur (même si le tout ne se faisait qu'en un seul appel côté client).
Le seul problème qui restait était de pouvoir créer un gabarit ET ses items en une seule commande, et là ça ne passait pas, Flex et lcds me retournaient tous deux une erreur en me disant qu'un item avec le même identifiant était déjà existant dans la session (le message exact est : org.hibernate.NonUniqueObjectException: a different object with the same identifier value was already associated with the session).

J'ai fait beaucoup de tests, rajouté des méthodes equals / hashcode dans mes classes Java comme le préconisaient certains lors de ce type d'erreur, le tout sans succès. Soit je continuais à avoir des messages d'erreur (concernant mes méthodes equals / hashcode), soit tout mon système de création / suppression ne me mettait à jour qu'un seul item dans une liste de 7 lors des transactions... J'avais tenté plusieurs approches différentes, et j'étais prêt à reposer une question sur un forum. C'est alors que je me suis dit que j'allais tester chaque solution l'une après l'autre, lister au fur et à mesure les erreurs et comportements non-désirés que me retournait hibernate, avant de poster le tout sur ledit forum.

Mon premier test à donc été de supprimer les méthodes equals/hashcode, de placer les valeurs unsaved-value à -1.

écrasement de données : ok, mais select après chaque insert.
insertion parent + enfants : retourne l'erreur "a different object with the same identifier..."

J'ai ensuite tenté de simplement modifier cet attribut unsaved-value pour le passer à 0. Et là, miracle...

écrasement de données : ok, multiples insertions directes sans requêtes select
insertion parent + enfants : ok, insertion du parent puis des enfants en cascade.

Comme quoi une simple valorisation différente peut entièrement changer le comportement d'hibernate par rapport aux transactions effectuées sur le serveur.


Fichier des destinations

Avant de passer côté client, il est encore nécessaire d'indiquer à hibernate quelles sont les "destinations" de nos services. En d'autres termes, côté Flex, un dataService est instancié en lui passant une destination, et hibernate doit être en mesure de comprendre cette destination demandée. Il le fait à l'aide du fichier data-management-config.xml situé dans le répertoire C:\dev\flash\lcds\jrun4\servers\default\flex\WEB-INF\flex.

Voici le contenu de mon fichier :

<?xml version="1.0" encoding="UTF-8"?>
<service id="data-service" 
    class="flex.data.DataService"
    messageTypes="flex.data.messages.DataMessage">
 
	<adapters>
		<adapter-definition id="actionscript" class="flex.data.adapters.ASObjectAdapter" default="true"/>
		<adapter-definition id="java-dao" class="flex.data.adapters.JavaAdapter"/>
	</adapters>
 
	<default-channels>
		<channel ref="my-rtmp"/>
	</default-channels>
 
	<destination id="gabaritvo.hibernate">
		<adapter ref="java-dao" />
		<properties>
			<use-transactions>true</use-transactions>
			<source>flex.data.assemblers.HibernateAssembler</source>
			<scope>application</scope>
			<metadata>
				<!--This is the unique identifier from the hibernate-entity bean -->
				<identity property="id"/>
			</metadata>
			<network>
				<session-timeout>20</session-timeout>
				<paging enabled="false" pageSize="10" />
				<throttle-inbound policy="ERROR" max-frequency="500"/>
				<throttle-outbound policy="REPLACE" max-frequency="500"/>
			</network>
			<server>
				<hibernate-entity>com.myapp.app.vo.GabaritVO</hibernate-entity>
				<fill-method>
					<name>fill</name>
					<params>java.util.List</params>
				</fill-method>
				<fill-configuration>
					<use-query-cache>false</use-query-cache>
					<allow-hql-queries>true</allow-hql-queries>
				</fill-configuration>
			</server>
		</properties>
	</destination>
 
	<destination id="gabarititemvo.hibernate">
		<adapter ref="java-dao" />
		<properties>
			<use-transactions>true</use-transactions>
			<source>flex.data.assemblers.HibernateAssembler</source>
			<scope>application</scope>
			<metadata>
				<!--This is the unique identifier from the hibernate-entity bean -->
				<identity property="id"/>
			</metadata>
			<network>
				<session-timeout>20</session-timeout>
				<paging enabled="false" pageSize="10" />
				<throttle-inbound policy="ERROR" max-frequency="500"/>
				<throttle-outbound policy="REPLACE" max-frequency="500"/>
			</network>
			<server>
				<hibernate-entity>com.myapp.app.vo.GabaritItemVO</hibernate-entity>
				<fill-method>
					<name>fill</name>
					<params>java.util.List</params>
				</fill-method>
				<fill-configuration>
					<use-query-cache>false</use-query-cache>
					<allow-hql-queries>true</allow-hql-queries>
				</fill-configuration>
			</server>
		</properties>
	</destination>
 
</service>

Il est d'abord nécessaire de lui indiquer le type d'adaptateur à utiliser avec nos classes. Ici, ce sont les 2 adaptateurs "actionscript" et "java-dao" qui sont stipulés.
Il faut ensuite indiquer quel est le canal par défaut utilisé pour récupérer les données. J'ai spécifié ici "my-rtmp", canal qui est défini dans le fichier service-config.xml situé dans le même répertoire que le fichier data-management-config.xml.

Enfin, les destinations sont insérées dans ce fichier. Ces destinations contiennent toutes les informations nécessaire à hibernate pour récupérer les données demandées du côté client :

  • le type d'assembleur (ici, hibernateASsembler) dans la balise source
  • le champs identifiant dans la balise metadata.identity
  • l'entité hibernate à utiliser dans la balise server.hibernate-identity
  • la ou les méthodes de remplissage (ici uniquement fill) dans la balise server.fill-method

Ainsi que différentes autres propriétés qui sont utilisées par hibernate pour mettre en place la configuration correcte vis-à-vis du serveur et du client.

Une fois tous ces éléments mis en place, la configuration base de données, les classes java, les fichiers de mapping, la configuration des destinations, le serveur est enfin prêt à être questionné par l'application cliente et à manipuler les données de la base de données. C'est ce que nous allons voir maintenant.


Coté client

Du côté client, il faut tout d'abord développer 2 classes, les "VO client" qui seront linkés aux "VO serveur" (nos classes Java).

Voici leur code :

GabaritVO.as

///////////////////////////////////////////////////////////
//  GabaritVO.as
//  Macromedia ActionScript Implementation of the Class GabaritVO
//  Created on:      10-avr.-2008 15:51:11
//  Original author: titouille
///////////////////////////////////////////////////////////
 
package com.myapp.app.vo
{
	import mx.collections.ArrayCollection;
 
 
	[Managed]
	[RemoteClass(alias="com.myapp.app.vo.GabaritVO")]
 
	/**
	 * @author titouille
	 * @version 1.0
	 * @created 10-avr.-2008 15:51:11
	 */
	public class GabaritVO
	{
		/**
		 * gabarit type for a day gabarit
		 */
		public static const DAY:String = "day";
		/**
		 * gabarit type for a week gabarit
		 */
		public static const WEEK:String = "week";
		/**
		 * gabarit type for a month gabarit
		 */
		public static const MONTH:String = "month";
 
	    /**
	     * id of the media
	     */
	    private var _id: int;
	    /**
	     * name of the media
	     */
	    private var _name: String = "";
	    /**
	     * description of the media
	     */
	    private var _desc: String = "";
	    /**
	     * type of gabarit
	     */
	    private var _type: String = "";
	    /**
	     * collection of events who forms the gabarit
	     */
	    private var _gabaritItems: ArrayCollection;
 
 
	    /**
	     * constructor
	     */
	    function GabaritVO()
	    {
	    }
	    /**
	     * set data
	     * 
	     * @param id
	     * @param name
	     * @param desc
	     */
	    public function setData( id:uint, name:String, type:String, desc:String=null, gabaritItems:ArrayCollection=null ):void
	    {
	    	this._id = id;
	    	this._name = name;
		this._type = type;
	    	this._desc = desc;
 
	    	if( gabaritItems == null ) this._gabaritItems = new ArrayCollection();
	    	else this._gabaritItems = gabaritItems;
	    }
	    /**
	     * set data
	     * 
	     * @param name
	     * @param desc
	     */
	    public function setNewData( name:String, type:String, desc:String=null, gabaritItems:ArrayCollection=null ):void
	    {
	    	this._id = 0;
	    	this._name = name;
	    	this._type = type;
	    	this._desc = desc;
 
	    	if( gabaritItems == null ) this._gabaritItems = new ArrayCollection();
	    	else this._gabaritItems = gabaritItems;
	    }
 
	    /**
	     * getter for the id
	     * [Bindable]
	     */
	    public function get id():uint
	    {
	    	return this._id;
	    }
 
	    /**
	     * setter for the id
	     * 
	     * @param value
	     */
	    public function set id(value:uint):void
	    {
	    	this._id = value;
	    }

	    /**
	     * getter for the name
	     * [Bindable]
	     */
	    public function get name():String
	    {
	    	return this._name;
	    }
 
	    /**
	     * setter for the name
	     * 
	     * @param value
	     */
	    public function set name(value:String):void
	    {
	    	this._name = value;
	    }
 
	    /**
	     * getter for the description
	     * [Bindable]
	     */
	    public function get desc():String
	    {
	    	return this._desc;
	    }
 
	    /**
	     * setter for the description
	     * 
	     * @param value
	     */
	    public function set desc(value:String):void
	    {
	    	this._desc = value;
	    }
 
	    /**
	     * getter for the type of gabarit
	     * [Bindable]
	     */
	    public function get type():String
	    {
	    	return this._type;
	    }
 
	    /**
	     * setter for the type of gabarit
	     * 
	     * @param value
	     */
	    public function set type(value:String):void
	    {
	    	this._type = value;
	    }
 
	    /**
	     * getter for the events
	     * [Bindable]
	     */
	    public function get gabaritItems():ArrayCollection
	    {
	    	return this._gabaritItems;
	    }
 
	    /**
	     * setter for the events
	     * 
	     * @param value
	     */
	    public function set gabaritItems(value:ArrayCollection):void
	    {
	    	this._gabaritItems = value;
	    }
 
	    /**
	     * add an event to the events array collection
	     * 
	     * @param gevo
	     */
	    public function addGabaritItem(gevo:GabaritItemVO):void
	    {
	    	if( this._gabaritItems == null )
	    	{
	    		this._gabaritItems = new ArrayCollection();
	    	}
	    	this._gabaritItems.addItem( gevo );
	    }
	}//end GabaritVO
}

GabaritItemVO.as

///////////////////////////////////////////////////////////
//  GabaritItemVO.as
//  Macromedia ActionScript Implementation of the Class GabaritItemVO
//  Created on:      10-avr.-2008 15:51:07
//  Original author: titouille
///////////////////////////////////////////////////////////
 
package com.myapp.app.vo
{
 
	[Managed]
	[RemoteClass(alias="com.myapp.app.vo.GabaritItemVO")]
 
	/**
	 * @author titouille
	 * @version 1.0
	 * @created 10-avr.-2008 15:51:07
	 */
	public class GabaritItemVO
	{
	    /**
	     * id of the gabarit event
	     */
	    private var _id: int;
 
	    private var _gabaritid: GabaritVO;
	    /**
	     * day of week of the event
	     */
	    private var _day: uint;
	    /**
	     * time to add the event
	     */
	    private var _hour: Date;
	    /**
	     * duration of the event, in minutes
	     */
	    private var _duration: Date;
 
	    /**
	     * constructor
	     */
	    function GabaritItemVO()
	    {
	    }
	    /**
	     * set data
	     * 
	     * @param id
	     * @param gabaritid
	     * @param day
	     * @param time
	     * @param duration
	     */
	    public function setData( id:int, gabaritid:GabaritVO, day:uint, hour:Date, duration:Date ):void
	    {
	    	this._id = id;
	    	this._gabaritid = gabaritid;
	    	this._day = day;
	    	this._hour = hour;
	    	this._duration = duration;
	    }
	    /**
	     * set data
	     * 
	     * @param gabaritid
	     * @param day
	     * @param time
	     * @param duration
	     */
	    public function setNewData( gabaritid:GabaritVO, day:uint, hour:Date, duration:Date ):void
	    {
	    	this._id = 0;
	    	this._gabaritid = gabaritid;
	    	this._day = day;
	    	this._hour = hour;
	    	this._duration = duration;
	    }
 
	    /**
	     * getter for the id
	     * [Bindable]
	     */
	    public function get id(): uint
	    {
	    	return this._id;
	    }
 
	    /**
	     * setter for the id
	     * 
	     * @param value
	     */
	    public function set id(value:uint): void
	    {
	    	this._id = value;
	    }

	    /**
	     * getter for the gabarit id
	     * [Bindable]
	     */
	    public function get gabaritid(): GabaritVO
	    {
	    	return this._gabaritid;
	    }
 
	    /**
	     * setter for the gabarit id
	     * 
	     * @param value
	     */
	    public function set gabaritid(value:GabaritVO): void
	    {
	    	this._gabaritid = value;
	    }
 
	    /**
	     * getter for the day
	     * [Bindable]
	     */
	    public function get day(): uint
	    {
	    	return this._day;
	    }
 
	    /**
	     * setter for the day of week
	     * 
	     * @param value
	     */
	    public function set day(value:uint): void
	    {
	    	this._day = value;
	    }
 
	    /**
	     * getter for the duration
	     * [Bindable]
	     */
	    public function get duration(): Date
	    {
	    	return this._duration;
	    }
 
	    /**
	     * setter for the duration
	     * 
	     * @param value
	     */
	    public function set duration(value:Date): void
	    {
	    	this._duration = value;
	    }
 
	    /**
	     * getter for the hour
	     * [Bindable]
	     */
	    public function get hour(): Date
	    {
	    	return this._hour;
	    }
 
	    /**
	     * setter for the hour
	     * 
	     * @param value
	     */
	    public function set hour(value:Date): void
	    {
	    	this._hour = value;
	    }
	}//end GabaritEventVO
}

Les éléments importants à noter sont :


  1. la balise de méta-données "Managed" ainsi que la balise de méta-données "RemoteClass" qui font la liaison entre la classe client et la classe serveur, exemple :

    [Managed]
    [RemoteClass(alias="com.myapp.app.vo.GabaritVO")]

    La méta-donnée [Managed] indique à Flex qu'il doit observer les changements opérés sur cet objet, et dépendant de la configuration, renvoyer automatiquement cet objet au serveur pour qu'il prenne en compte les modifications.
    La méta-donnée [RemoteClass] indique quelle est la classe serveur associée à cette classe client.

  2. Le constructeur ne possède aucun paramètre, à l'identique de la classe serveur.
  3. Puisque le constructeur ne permet pas de créer un objet complet dès son instanciation, j'ai créé 2 méthodes "setData" et setNewData permettant de passer directement tous les paramètres nécessaire au remplissage des données. Il est encore à noter que la méthode "setNewData" valorise à 0 le paramètre "id" (l'identifiant de l'objet). Ainsi, lorsqu'il est renvoyé au serveur, grâce à l'attribut unsaved-value du fichier de mapping, hibernate est en mesure de savoir que cet objet n'existe pas dans la base de données, et il sait qu'il doit l'insérer (insert) plutôt que de tenter une mise à jour (update)
  4. Enfin, la variable "gabaritid" de la classe GabaritItemVO est de type GabaritVO, telle que dans la classe Java côté serveur.

Maintenant, il est possible de récupérer les informations via la méthode fill, modifier les données, en supprimer, en créer en manipulant directement l'objet dataProvider et ses objets de type gabaritVO, puis d'un simple appel à la méthode "commit" du dataService, les modifications seront appliquées côté serveur.

Pour finir, voici quelques exemples de commandes créées pour récupérer, manipuler et sauvegarder les données.

Dans l'ordre, on récupère d'abord les données via la commande GetGabaritsCommand, on manipule les données via les autres commandes, et on appelle la méthode SaveGabaritsCommand, qui va exécuter le commit, puis rappeler la méthode GetGabaritsCommand afin de récupérer les données côté serveur et s'assurer que l'application détient bien les données à jour.

GetGabaritsCommand.as

///////////////////////////////////////////////////////////
//  GetGabaritsCommand.as
//  Macromedia ActionScript Implementation of the Class GetGabaritsCommand
//  Created on:      26-mars-2008 11:34:02
//  Original author: titouille
///////////////////////////////////////////////////////////
 
package com.myapp.app.controller.scheduler.gabarits
{
	import com.myapp.app.ApplicationFacade;
	import com.myapp.app.model.GabaritManagerProxy;
	import com.myapp.app.view.SchedulerMediator;
	import com.myapp.app.view.components.Scheduler;
 
	import mx.data.events.DataConflictEvent;
	import mx.data.events.DataServiceFaultEvent;
	import mx.rpc.events.FaultEvent;
	import mx.rpc.events.ResultEvent;
 
	import org.puremvc.as3.interfaces.INotification;
	import org.puremvc.as3.patterns.command.SimpleCommand;
 
	/**
	 * @author titouille
	 * @version 1.0
	 * @created 26-mars-2008 11:34:02
	 */
	public class GetGabaritsCommand extends SimpleCommand
	{
	    /**
	     * execute the command
	     * 
	     * @param note
	     */
	    public override function execute(note:INotification): void
	    {
	    	trace( "execute getGabaritCommand" );
	    	//execute remoting function to get a
	    	//result from database, if possible as
	    	//Value Object (VO) ArrayCollection
 
		// get gabarit proxy
	    	var gmp:GabaritManagerProxy = GabaritManagerProxy( facade.retrieveProxy( GabaritManagerProxy.NAME ) );
 
	    	// set listener for data service
		gmp.dataService.addEventListener( DataServiceFaultEvent.FAULT, this._handleFault );
		gmp.dataService.addEventListener( DataConflictEvent.CONFLICT, this._handleConflict );
		gmp.dataService.addEventListener(ResultEvent.RESULT, this._handleResult );
 
		// get data from server and fill the data provider
		gmp.dataService.fill(gmp.dataProvider,"all.gabarits",[]);
	    }

	    /**
	    * handle when server return a fault
	    * 
	    * @param FaultEvent event
	    */
	    private function _handleFault( event:FaultEvent ):void 
	    {
		trace( "handleFault" );
		trace( event.message.toString() );
	    }
 
	    /**
	    * handle when server return a conflict
	    * 
	    * @param FaultEvent event
	    */
	    private function _handleConflict( event:DataConflictEvent ):void 
	    {
		trace( "handleConflict" );
		trace( event.conflict.message.toString() );
		trace( event.conflict.name.toString() );
		trace( event.conflict.cause.identity );
		trace( event.conflict.errorID );
	    }
 
	    /**
	     * handler when result (or error) returns from server
	     * 
	     * @param ResultEvent event
	     */
	    private function _handleResult( event:ResultEvent ): void
	    {
	    	//get the result from the server.
	    	//If needed, set the result to proxy with
	    	//a formatting function.
	    	//Else, set the result directly on the
	    	//proxy container arrayCollection and
	    	//sendNotification to alert concerned
	    	//mediator(s)
	    	trace( "handleResult" );
 
	    	// get gabarit proxy
	    	var gmp:GabaritManagerProxy = GabaritManagerProxy( facade.retrieveProxy( GabaritManagerProxy.NAME ) );
 
		// remove data service listeners
		gmp.dataService.removeEventListener(DataConflictEvent.CONFLICT, this._handleConflict );
	    	gmp.dataService.removeEventListener(DataServiceFaultEvent.FAULT, this._handleFault );
	    	gmp.dataService.removeEventListener(ResultEvent.RESULT, this._handleResult );
 
	    	// get scheduler
		var schm:SchedulerMediator = SchedulerMediator( facade.retrieveMediator( SchedulerMediator.NAME ) );
	    	var sch:Scheduler = schm.ctrlView;
 
		// bind proxy data provider to gabarit data provider
		sch.gabaritView.dataProvider = gmp.dataProvider;
	    	sch.gabaritView.ctrGabarit.dataProvider = sch.gabaritView.dataProvider;
 
	    	// alert mediator(s)
	    	this.sendNotification( ApplicationFacade.SCHEDULER_READY );
	    }
	}//end GetGabaritsCommand
}

NewGabaritCommand.as (création d'un gabarit vide)

///////////////////////////////////////////////////////////
//  NewGabaritCommand.as
//  Macromedia ActionScript Implementation of the Class NewGabaritCommand
//  Created on:      26-mars-2008 11:34:02
//  Original author: titouille
///////////////////////////////////////////////////////////
 
package com.myapp.app.controller.scheduler.gabarits
{
	import com.myapp.app.ApplicationFacade;
	import com.myapp.app.control.DateManagerBase;
	import com.myapp.app.model.GabaritManagerProxy;
	import com.myapp.app.view.SchedulerMediator;
	import com.myapp.app.vo.GabaritVO;
 
	import org.puremvc.as3.interfaces.INotification;
	import org.puremvc.as3.patterns.command.SimpleCommand;
 
	/**
	 * @author titouille
	 * @version 1.0
	 * @created 26-mars-2008 11:34:02
	 */
	public class NewGabaritCommand extends SimpleCommand
	{
	    /**
	     * execute the command
	     * 
	     * @param note
	     */
	    public override function execute(note:INotification): void
	    {
	    	trace( "execute NewGabaritCommand" );
 
		// get gabarit proxy
	    	var gmp:GabaritManagerProxy = GabaritManagerProxy( facade.retrieveProxy( GabaritManagerProxy.NAME ) );
		var schm:SchedulerMediator = SchedulerMediator( facade.retrieveMediator( SchedulerMediator.NAME ) );
		var dm:DateManagerBase = schm.ctrlView.dateView;
 
		// create new gabarit value object
		var gvo:GabaritVO = new GabaritVO();
		gvo.setNewData( note.getBody() as String, dm.mode );
 
		// add gabarit value object to proxy data provider
		gmp.dataProvider.addItem( gvo );
 
		// save changes
		this.sendNotification( ApplicationFacade.SCHEDULER_SAVEGABARITS );
	    }
	}//end NewGabaritCommand
}

UpdateGabaritCommand.as (mise à jour du nom du gabarit)

///////////////////////////////////////////////////////////
//  UpdateGabaritCommand.as
//  Macromedia ActionScript Implementation of the Class UpdateGabaritCommand
//  Created on:      26-mars-2008 11:34:02
//  Original author: titouille
///////////////////////////////////////////////////////////
 
package com.myapp.app.controller.scheduler.gabarits
{
	import com.myapp.app.ApplicationFacade;
	import com.myapp.app.model.GabaritManagerProxy;
	import com.myapp.app.view.SchedulerMediator;
	import com.myapp.app.vo.GabaritVO;
 
	import org.puremvc.as3.interfaces.INotification;
	import org.puremvc.as3.patterns.command.SimpleCommand;
 
	/**
	 * @author titouille
	 * @version 1.0
	 * @created 26-mars-2008 11:34:02
	 */
	public class UpdateGabaritCommand extends SimpleCommand
	{
	    /**
	     * execute the command
	     * 
	     * @param note
	     */
	    public override function execute(note:INotification): void
	    {
	    	trace( "execute UpdateGabaritCommand" );
 
		// get gabarit proxy
	    	var gmp:GabaritManagerProxy = GabaritManagerProxy( facade.retrieveProxy( GabaritManagerProxy.NAME ) );
		var sch:SchedulerMediator = SchedulerMediator( facade.retrieveMediator( SchedulerMediator.NAME ) );
 
		// update name of gabarit
		var gvo:GabaritVO = sch.ctrlView.gabaritView.ctrGabarit.selectedItem as GabaritVO;
		gvo.name = note.getBody() as String;
 
		// save changes
		this.sendNotification( ApplicationFacade.SCHEDULER_SAVEGABARITS );
	    }
	}//end UpdateGabaritCommand
}

EraseGabaritCommand.as (écrasement des items d'un gabarit existant)

///////////////////////////////////////////////////////////
//  EraseGabaritCommand.as
//  Macromedia ActionScript Implementation of the Class EraseGabaritCommand
//  Created on:      05-may-2008 10:06:02
//  Original author: titouille
///////////////////////////////////////////////////////////
 
package com.myapp.app.controller.scheduler.gabarits
{
	import com.myapp.app.ApplicationFacade;
	import com.myapp.app.control.CalendarManagerBase;
	import com.myapp.app.control.DateManagerBase;
	import com.myapp.app.model.GabaritManagerProxy;
	import com.myapp.app.view.SchedulerMediator;
	import com.myapp.app.vo.GabaritItemVO;
	import com.myapp.app.vo.GabaritVO;
 
	import mx.collections.ArrayCollection;
 
	import org.puremvc.as3.interfaces.INotification;
	import org.puremvc.as3.patterns.command.SimpleCommand;
 
	import qs.calendar.CalendarEvent;
 
	/**
	 * @author titouille
	 * @version 1.0
	 * @created 05-may-2008 10:06:02
	 */
	public class EraseGabaritCommand extends SimpleCommand
	{
	    /**
	     * execute the command
	     * 
	     * @param note
	     */
	    public override function execute(note:INotification): void
	    {
	    	trace( "execute EraseGabaritCommand" );
 
		// get gabarit proxy
	    	var gmp:GabaritManagerProxy = GabaritManagerProxy( facade.retrieveProxy( GabaritManagerProxy.NAME ) );
	    	// get scheduler mediator
		var schm:SchedulerMediator = SchedulerMediator( facade.retrieveMediator( SchedulerMediator.NAME ) );
		// get date manager
		var dmb:DateManagerBase = schm.ctrlView.dateView;
		// get calendar manager
		var cal:CalendarManagerBase = schm.ctrlView.calView;
 
		// get selected gabarit value object
		var gvo:GabaritVO = gmp.dataProvider.getItemAt( note.getBody() as uint ) as GabaritVO;
 
		// add calendar display mode to gabarit and supress gabarit items
		gvo.type = dmb.mode;
		gvo.gabaritItems.removeAll();
 
		// get visible items on calendar
		var a:Array = cal.ccdCal.visibleEvents;
 
		// iterate on each event on calendar to add it on gabarit
		for( var i:uint = 0; i < a.length; i++ )
		{
			var evt:CalendarEvent = a[i] as CalendarEvent;
 
			// get day and duration
			var day:uint = this._getType( dmb.mode, evt );
			var duration:Date = this._getDuration( evt.end.getTime() - evt.start.getTime() );
 
			// create new gabarit item and set data
			var givo:GabaritItemVO = new GabaritItemVO();
			givo.setNewData( gvo, day, new Date( 0, 0, 0, evt.start.getHours(), evt.start.getMinutes() ), duration );
 
			// add item to gabarit
			gvo.addGabaritItem( givo );
		}
 
		// save changes
		this.sendNotification( ApplicationFacade.SCHEDULER_SAVEGABARITS );
	    }

 
	    /**
	    * get type of gabarit to add to database
	    * 
	    * @param String mode of calendar visualisation (day, week or month)
	    * @param CalendarEvent current parsed calendar event
	    */
	    private function _getType( mode:String, evt:CalendarEvent ):uint
	    {
		if( mode == "week" )	return evt.start.getDay();
		else if( mode == "month" ) return evt.start.getDate();
		return 0;
	    }
 
	    /**
	    * get the duration of the event, date format
	    * 
	    * @param uint duration of event
	    */
	    private function _getDuration( duration:uint ):Date
	    {
	    	// get duration in seconds instead of miliseconds
	    	duration = duration / 1000;
 
	    	// get seconds
	    	var seconds:uint = duration % 60;
	    	duration = (duration - seconds) / 60;
	    	// get minutes
	    	var minutes:uint = duration % 60;
	    	// get hours
	    	var hours:uint = (duration - minutes) / 60;
 
	    	return new Date( 0, 0, 0, hours, minutes, seconds, 0 );
	    }
	}//end EraseGabaritCommand
}

CreateGabaritCommand.as (création d'un gabarit et de ses items)

///////////////////////////////////////////////////////////
//  CreateGabaritCommand.as
//  Macromedia ActionScript Implementation of the Class CreateGabaritCommand
//  Created on:      05-may-2008 10:06:02
//  Original author: titouille
///////////////////////////////////////////////////////////
 
package com.myapp.app.controller.scheduler.gabarits
{
	import com.myapp.app.ApplicationFacade;
	import com.myapp.app.control.CalendarManagerBase;
	import com.myapp.app.control.DateManagerBase;
	import com.myapp.app.model.GabaritManagerProxy;
	import com.myapp.app.view.SchedulerMediator;
	import com.myapp.app.vo.GabaritItemVO;
	import com.myapp.app.vo.GabaritVO;
 
	import mx.collections.ArrayCollection;
 
	import org.puremvc.as3.interfaces.INotification;
	import org.puremvc.as3.patterns.command.SimpleCommand;
 
	import qs.calendar.CalendarEvent;
 
	/**
	 * @author titouille
	 * @version 1.0
	 * @created 05-may-2008 10:06:02
	 */
	public class CreateGabaritCommand extends SimpleCommand
	{
	    /**
	     * execute the command
	     * 
	     * @param note
	     */
	    public override function execute(note:INotification): void
	    {
	    	trace( "execute CreateGabaritCommand" );
 
		// get gabarit proxy
	    	var gmp:GabaritManagerProxy = GabaritManagerProxy( facade.retrieveProxy( GabaritManagerProxy.NAME ) );
	    	// get scheduler mediator
		var schm:SchedulerMediator = SchedulerMediator( facade.retrieveMediator( SchedulerMediator.NAME ) );
		// get date manager
		var dmb:DateManagerBase = schm.ctrlView.dateView;
		// get calendar manager
		var cal:CalendarManagerBase = schm.ctrlView.calView;
 
		// create new gabarit value object
		var gvo:GabaritVO = new GabaritVO();
		gvo.setNewData( note.getBody() as String, dmb.mode );
 
		// get visible items on calendar
		var a:Array = cal.ccdCal.visibleEvents;
 
		// iterate on each event on calendar to add it on gabarit
		for( var i:uint = 0; i < a.length; i++ )
		{
			var evt:CalendarEvent = a[i] as CalendarEvent;
 
			// get day and duration
			var day:uint = this._getType( dmb.mode, evt );
			var duration:Date = this._getDuration( evt.end.getTime() - evt.start.getTime() );
 
			// create new gabarit item and set data
			var givo:GabaritItemVO = new GabaritItemVO();
			givo.setNewData( gvo, day, new Date( 0, 0, 0, evt.start.getHours(), evt.start.getMinutes() ), duration );
			// add item to gabarit
			gvo.addGabaritItem( givo );
		}
 
		// add gabarit value object to proxy data provider
		gmp.dataProvider.addItem( gvo );
 
		// save changes
		this.sendNotification( ApplicationFacade.SCHEDULER_SAVEGABARITS );
	    }

 
	    /**
	    * get type of gabarit to add to database
	    * 
	    * @param String mode of calendar visualisation (day, week or month)
	    * @param CalendarEvent current parsed calendar event
	    */
	    private function _getType( mode:String, evt:CalendarEvent ):uint
	    {
		if( mode == "week" )	return evt.start.getDay();
		else if( mode == "month" ) return evt.start.getDate();
		return 0;
	    }
 
	    /**
	    * get the duration of the event, date format
	    * 
	    * @param uint duration of event
	    */
	    private function _getDuration( duration:uint ):Date
	    {
	    	// get duration in seconds instead of miliseconds
	    	duration = duration / 1000;
 
	    	// get seconds
	    	var seconds:uint = duration % 60;
	    	duration = (duration - seconds) / 60;
	    	// get minutes
	    	var minutes:uint = duration % 60;
	    	// get hours
	    	var hours:uint = (duration - minutes) / 60;
 
	    	return new Date( 0, 0, 0, hours, minutes, seconds, 0 );
	    }
	}//end CreateGabaritCommand
}

DeleteGabaritCommand.as (suppression d'un gabarit avec suppression en cascade de ses items)

///////////////////////////////////////////////////////////
//  DeleteGabaritCommand.as
//  Macromedia ActionScript Implementation of the Class DeleteGabaritCommand
//  Created on:      26-mars-2008 11:34:02
//  Original author: titouille
///////////////////////////////////////////////////////////
 
package com.myapp.app.controller.scheduler.gabarits
{
	import com.myapp.app.ApplicationFacade;
	import com.myapp.app.model.GabaritManagerProxy;
	import com.myapp.app.view.SchedulerMediator;
	import com.myapp.app.vo.GabaritVO;
 
	import org.puremvc.as3.interfaces.INotification;
	import org.puremvc.as3.patterns.command.SimpleCommand;
 
	/**
	 * @author titouille
	 * @version 1.0
	 * @created 26-mars-2008 11:34:02
	 */
	public class DeleteGabaritCommand extends SimpleCommand
	{
	    /**
	     * execute the command
	     * 
	     * @param note
	     */
	    public override function execute(note:INotification): void
	    {
	    	trace( "execute DeleteGabaritCommand" );
 
		// get gabarit proxy
	    	var gmp:GabaritManagerProxy = GabaritManagerProxy( facade.retrieveProxy( GabaritManagerProxy.NAME ) );
		var sch:SchedulerMediator = SchedulerMediator( facade.retrieveMediator( SchedulerMediator.NAME ) );
 
		// remove gabarit value object from proxy data provider
		var index:uint = note.getBody() as uint;
		gmp.dataProvider.removeItemAt( index );
 
		// save changes
		this.sendNotification( ApplicationFacade.SCHEDULER_SAVEGABARITS );
	    }
	}//end DeleteGabaritCommand
}

SaveGabaritCommand.as (sauvegarde des modifications)

///////////////////////////////////////////////////////////
//  SaveGabaritsCommand.as
//  Macromedia ActionScript Implementation of the Class SaveGabaritsCommand
//  Created on:      26-mars-2008 11:34:02
//  Original author: titouille
///////////////////////////////////////////////////////////
 
package com.myapp.app.controller.scheduler.gabarits
{
	import com.myapp.app.ApplicationFacade;
	import com.myapp.app.model.GabaritManagerProxy;
	import com.myapp.app.vo.GabaritVO;
 
	import mx.data.events.DataConflictEvent;
	import mx.data.events.DataServiceFaultEvent;
	import mx.rpc.events.FaultEvent;
	import mx.rpc.events.ResultEvent;
 
	import org.puremvc.as3.interfaces.INotification;
	import org.puremvc.as3.patterns.command.SimpleCommand;
 
	/**
	 * @author titouille
	 * @version 1.0
	 * @created 26-mars-2008 11:34:02
	 */
	public class SaveGabaritsCommand extends SimpleCommand
	{
	    /**
	     * execute the command
	     * 
	     * @param note
	     */
	    public override function execute(note:INotification): void
	    {
	    	trace( "execute SaveGabaritsCommand" );
	    	//execute remoting function to get a
	    	//result from database, if possible as
	    	//Value Object (VO)
 
	    	var gmp:GabaritManagerProxy = GabaritManagerProxy( facade.retrieveProxy( GabaritManagerProxy.NAME ) );
 
		gmp.dataService.addEventListener( DataServiceFaultEvent.FAULT, handleFault );
		gmp.dataService.addEventListener( DataConflictEvent.CONFLICT, handleConflict );
		gmp.dataService.addEventListener( ResultEvent.RESULT, handleResult );
 
 
		gmp.dataService.commit();
	    }
 
	    private function handleFault( e:FaultEvent ):void 
	    {
		trace( "handleFault" );
		trace( e.message.toString() );
	    }
	    private function handleConflict( e:DataConflictEvent ):void 
	    {
		trace( "handleConflict" );
		trace( e.conflict.message.toString() );
		trace( e.conflict.name.toString() );
		trace( e.conflict.cause.identity );
		trace( e.conflict.errorID );
	    }
 
	    /**
	     * handler when result (or error) returns from server
	     * 
	     * @param result
	     */
	    private function handleResult( e:ResultEvent ): void
	    {
	    	trace( "handleResult" );
	    	var gmp:GabaritManagerProxy = GabaritManagerProxy( facade.retrieveProxy( GabaritManagerProxy.NAME ) );
 
	    	gmp.dataService.removeEventListener( DataConflictEvent.CONFLICT, handleConflict );
	    	gmp.dataService.removeEventListener( DataServiceFaultEvent.FAULT, handleFault );
	    	gmp.dataService.removeEventListener( ResultEvent.RESULT, handleResult );
 
		sendNotification( ApplicationFacade.SCHEDULER_GETGABARITS );
	    }
	}//end SaveGabaritsCommand
}

Comme vous pouvez le constater, les seuls appels au DataService sont effectués soit pour récupérer les donnés (méthode fill() dans GetGabaritsCommand.as) soit pour envoyer les modifications au serveur (méthode commit() dans SaveGabaritsCommand.as)

Voilà pour une première approche de LCDS. Je pense que c'est déjà un bon début pour aller de l'avant avec cette nouvelle technologie très prometteuse Smile




Salut, super demo, mais je

Salut, super demo, mais je ne voit pas la classe GabaritManagerProxy?

Je cherche desesperement un exemple simple d'utilisation de LCDS avec pureMVC.
Tout ce que je veux comprendre c'est l'apport des mediators et comment il interagissent avec la vue d'une part.
D'autre part l'interaction entre pureMVC et LCDS. Dans CairnGorm on accede au data via un EnterpriseServiceLocator.getRemoteObject, c'est quoi l'equivalent?

Tu saurait pas ou trouver un petit tuto (qui marche) ?

Merci pour toute info, je ne sait vraiment plus ou chercher!

Portrait de titouille

fonctionnement

Hello,

Concernant PureMVC, c'est un peu difficile d'expliquer ça de manière rapide et simple, mais en substance, ça fonctionne de la manière suivante :

il y a 3 éléments : les proxy, les médiateurs et les commandes, qui correspondent respectivement aux modèles, vues et contrôleurs (M-V-C)

Le proxy ne sert qu'a très peu de choses, simplement à stocker les données qui seront utilisées par l'application. Dans mon cas, un proxy contient simplement un ArrayCollection et un DataService. Le dataService servant à interroger le serveur, tandis que l'ArrayCollection à stocker la liste des éléments retournés par le serveur. Dans la commande GetGabaritsCommand, tu peux voir que je fais appel à la méthode fill du dataService en lui passant le tableau (dataProvider).

Le médiateur sert à faire la liaison avec la partie visuelle. A priori, elle est à l'écoute des évènements déclenchés par la partie visuelle (déclenchement à partir d'évènements Flex, dispatchEvent), peut déclencher des notifications de type alerte ou commande, et être à l'écoute de notification de type alerte.

Les commandes, enfin, font le gros du boulot, ce sont elles qui s'occupent de lier les différents éléments ensemble (proxy et médiator).

Dans mon cas, j'ai un composant Tree sur la partie visuelle. Le médiateur déclenche une notification de type commande pour exécuter GetGabaritsCommand. La commande va récupérer le proxy, et faire appel à la méthode "fill" du dataService en lui passant le dataProvider. Au retour de cette commande (_handleResult), le dataProvider est rempli des données récupérées depuis le serveur. La commande récupère ensuite le Mediator et lie au composant Tree le dataProvider du proxy. Elle déclenche enfin une commande de type alerte qui sera interceptée par le médiateur pour lui indiquer que le traitement est terminé.

Si j'ai un peu de temps, je mettrai un exemple un peu plus concret avec du code complet pour illustrer mes propos.

Je te laisse déjà avec ça, j'espère que ça te permettra de mieux comprendre comment intégrer les différentes choses Wink