Correspondances entre données - Data Binding
Table of Contents
Correspondance entre données : une définition formelle
Une correspondance entre données est un couple de fonctions permettant de traduire d'un ensemble de données à un autre ensemble, et réciproquement.
\[ \begin{array}{rcl} \mathcal{A} & \underset{g}{\overset{f}{\rightleftarrows}} & \mathcal{B} \\ \end{array} \]Remarque : ce terme de correspondance est une traduction non officelle des deux termes utilisés en anglais, "data mapping" d'une part et "data binding" d'autre part, ce dernier n'étant malheureusement pas très explicite tant le nom "binding" est surchargé. Il a l'avantage de coller à la définition mathématique.
Les fonctions de traductions \(f\) et \(g\) peuvent être des foncteurs lorsque les données dans \(\mathcal{A}\) et \(\mathcal{B}\) sont typées : elles opérent alors non seulement sur les données mais aussi sur les types, d'une manière cohérente. Pour simplifier, si \(f\) est la fonction de traduction pour les données, on note \(F\) la fonction de traduction associée pour les types.
\[ \begin{array}{ccc} \begin{array}{c} (d : S) \in \mathcal{A} \\ \hline (f(d) : F(S)) \in \mathcal{B} \end{array} & \quad & \begin{array}{c} (e : T) \in \mathcal{B} \\ \hline (g(e) : G(T)) \in \mathcal{A} \end{array} \end{array} \](Notation : \((x : X)\) signifie que \(x\) a le type \(X\).)
S'il existe une relation de sous-typage pour les types sur \(\mathcal{A}\) et pour les types sur \(\mathcal{B}\), on peut renforcer la correspondance en préservant la relation de sous-typage.
\[ \begin{array}{ccc} \begin{array}{c} (S_1 \leq S_2) \in \mathcal{A} \\ \hline (F(S_1) \leq F(S_2)) \in \mathcal{B} \end{array} & \quad & \begin{array}{c} (T_1 \leq T_2) \in \mathcal{B} \\ \hline (G(T_1) \leq G(T_2)) \in \mathcal{A} \end{array} \end{array} \](Notation : \((X \leq Y)\) signifie que \(X\) est un sous-type de \(Y\).)
Comment définir une correspondance ? Par la suite, on se restreint à la partie "données" des correspondances, mais toutes les définitions valent aussi pour la partie "types".
Tentative de définition 1 : Le couple de fonction \((f : \mathcal{A} \rightarrow \mathcal{B}, g : \mathcal{B} \rightarrow \mathcal{A})\) forme une correspondance si les fonctions \(f\) et \(g\) sont inverses l'une de l'autre.
\[ f ; g = \mathrm{id}_\mathcal{A}, \quad\quad g ; f = \mathrm{id}_\mathcal{B}. \](Notation : \(h ; k\), appelé séquence de \(h\) et \(k\), désigne la composition \(k o h\) de \(k\) par \(h\) et \(\mathrm{id}_X\) est la fonction identité sur \(X\).)
Si de plus les fonctions \(F\) et \(G\) sont inverses l'une de l'autre et préservent la relation de sous-typage, on peut considérer que les deux ensembles de données sont équivalents, au sens d'interchangeables : ils sont isomorphes, via la correspondance.
Cette première définition est très forte. On peut l'affaiblir, en ne supposant plus que \(f\) est une bijection.
Tentative de définition 2 : Le couple de fonction \((f : \mathcal{A} \rightarrow \mathcal{B}, g : \mathcal{B} \rightarrow \mathcal{A})\) forme une correspondance si la fonction \(g\) est un inverse à gauche (relativement à la séquence) de \(f\).
\[ g ; f = \mathrm{id}_\mathcal{B}. \]Ainsi, \(f\) est toujours surjective mais n'est plus nécessairement injective. On peut aller encore plus loin, en ne supposant plus que \(f\) est surjective. C'est la définition la plus générale, que nous retenons.
Définition [ Correspondance ] : Le couple de fonction \((f : \mathcal{A} \rightarrow \mathcal{B}, g : \mathcal{B} \rightarrow \mathcal{A})\) forme une correspondance si la fonction \(g\) est un quasi-inverse à gauche (relativement à la séquence) de \(f\).
\[ f ; g ; f = f. \]Cette dernière notion de correspondance est généralement celle utilisée lorsqu'on essaie de faire correspondre les données d'un langage (de programmation, ou de description de données) à celles d'un autre langage.
Point fondamental : un théorème d'existence.
Théorème [ Existence de correspondances ] : Toute fonction \(f : \mathcal{A} \rightarrow \mathcal{B}\) admet un quasi-inverse à gauche \(g : \mathcal{B} \rightarrow \mathcal{A}\) et peut donc être étendue en une correspondance \((f, g)\) entre \(\mathcal{A}\) et \(\mathcal{B}\).
Démonstration.
Soit \(f : \mathcal{A} \rightarrow \mathcal{B}\). On décompose canoniquement \(f\).
\[ (f : \mathcal{A} \rightarrow \mathcal{B}) = (f/\sim : \mathcal{A} \rightarrow \mathcal{A}/\sim) ; (i : \mathcal{A}/\sim \rightarrow \mathcal{B}). \](Notation : \(\sim\) est la relation d'équivalence définie par \(x \sim y\) si \(f(x) = f(y)\)).
\(i\) étant une injection, elle admet un inverse à droite, noté \(s\).
\[ i ; s = \mathrm{id}_{\mathcal{A}/\sim} \]
Par l'axiome du choix, il existe une fonction \(c : \mathcal{A}/\sim \rightarrow \mathcal{A}\), qui choisit un représentant dans chaque classe d'équivalence. Définissons \(g : \mathcal{B} \rightarrow \mathcal{A}\) par \(g = s ; c\). On vérifie que \(g\) est bien un quasi-inverse de \(f\).
\[ f ; g ; f = f/\sim ; i ; s ; c ; f = f/\sim ; \mathrm{id} ; c ; f = f/\sim ; c ; f = f. \]
Correspondances pour les langages à objets
Pour un langage à objets comme Java, quelques correspondances jouent un rôle important parce qu'elles permettent d'interfacer le langage avec d'autres langages, très utilisés :
- les langages de représentation des bases de données,
- les langages de représentation des documents structurés.
Correspondance avec le modèle relationnel
Cette correspondance est importante dans le sens où elle fait correspondre des objets sauvegardés dans une mémoire volatile avec des tuples appartenant à des tables sauvegardées dans une mémoire persistante, celle d'une base de données.
En Java, elle est spécifiée par une norme, "Java Persistence API", dite JPA, qui fait partie de la spécification JEE. L'implémentation de référence depuis la version 2.0 vient du projet EclipseLink (cf. le composant JPA). Les types et les annotations sont définis dans le paquet javax.persistence.
Bibliographie : cf. chap. 4 à 6, Beginning Java EE 7, d'Antonio Goncalves.
Correspondance avec les documents XML
Cette correspondance est importante pour les services puisque XML est un des deux langages principaux d'échange de données, avec JSON.
En Java, il existe quatre méthodes pour traiter des documents XML.
- Flux d'événements : interface SAX ("simple API for XML") permettant de parcourir un document XML en produisant un flux d'événements (ouverture de balise, lecture d'attributs, lecture de contenu, fermeture de balise)
- Modèle universel : représentation d'un document XML par un modèle objet dit DOM (également utilisé dans les navigateurs pour représenter du HTML)
- Interfaçage avec des langages de manipulation de documents XML (particulièrement avec XSLT, "eXtensible Stylesheet Language Transformations")
- Modèle objet : correspondance fonctorielle entre les objets et les documents, spécifiée par une norme, JAXB (version 2), dont l'implémentation vient du projet Metro, inclus dans le projet Glassfish (serveur d'application JEE d'Oracle)
Les trois premières méthodes sont fournies par un ensemble de bibliothèques, appartenant à JAXP ("Java Architecture for XML Processing") et intégré à la version standard (JSE).
La dernière méthode définit une véritable correspondance entre données, appelée couramment "data binding" : elle est fournie par un ensemble de bibliothèques et outils, appartenant à JAXB ("Java Architecture for XML Binding") et intégré à la version standard (JSE).
Bibliographie : cf. chap. 12, Beginning Java EE 7, d'Antonio Goncalves.
Voici l'essentiel permettant d'utiliser JAXB dans un framework à objets pour développer des services Web. L'usage de JAXB repose principalement sur des annotations qu'on trouvera dans le paquet javax.xml.bind.annotation.
La correspondance définie par JAXB est un foncteur, suivant le schéma suivant. Elle ne préserve pas la relation de sous-typage.
classe <------> schéma objet <------> document
(Par schéma, nous entendons un schéma complet ou partiel.)
Deux outils fournissent la correspondance pour les types, le générateur de schémas, schemagen, et le compilateur, xjc.
shemagen : classe ------> schéma classe <------ schéma : xjc
Le générateur de schémas ne s'applique pas aux interfaces mais seulement aux classes. Il efface les paramètres de types, comme le compilateur Java.
Une classe non annotée produit un schéma partiel, correspondant à un type complexe. Pour obtenir un schéma complet, comportant la déclaration d'un élément racine, il suffit d'annoter la classe par @XmlRootElement, avec la possibilité de modifier le nom de l'élément. Pour modifier le nom du type, utiliser l'annotation @XmlType. Par défaut, le nom de la balise et du type correspondent au nom de la classe (la majuscule initiale devenant une minuscule). Pour obtenir un type anonyme, utiliser un nom vide.
@XmlRootElement(name = "racine") @XmlType(name = "") public class A { ... }
Résultat :
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <xs:schema version="1.0" xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="racine"> <xs:complexType> <xs:sequence> ... </xs:sequence> </xs:complexType> </xs:element> </xs:schema>
La structure du type complexe est définie par les propriétés (au sens des JavaBeans, cf. annexe ci-dessous) déclarées dans la classe, en prenant en compte non seulement les champs privés avec des accesseurs publics (la convention JavaBean) mais aussi les champs publics. Cette sélection peut être modulée en utilisant l'annotation @XmlAccessorType(XmlAccessType.X) , où X, par défaut PUBLIC_MEMBER, peut exprimer les différentes combinaisons pour les membres publics (accesseurs et champs, accesseurs ou champs, ni l'un ni l'autre sauf mention explicite). On peut retirer un membre de la sélection en utilisant l'annotation XmlTransient.
Par défaut, le type complexe est formé par une séquence d'éléments. L'ordre des éléments est modulable : il suffit d'utiliser l'annotation @XmlType et son attribut propOrder.
@XmlType(propOrder={"un", "deux" , "trois"}) public class A { ... // Propriétés un, deux, trois dans un ordre quelconque }
Résultat :
<xs:complexType name="a"> <xs:sequence> <xs:element name="un" type="..."/> <xs:element name="deux" type="..."/> <xs:element name="trois" type="..."/> </xs:sequence> </xs:complexType>
Plutôt qu'une séquence, on peut spécifier un ensemble (pour lequel l'ordre est indifférent) par l'annotation @XmlType(propOrder={}). Pour renommer un élément, utiliser l'annotation @XmlElement(name="nouveauNom"). Enfin, on peut traduire une propriété en un attribut plutôt qu'un élément (à condition que son type soit simple) : il suffit de l'annoter par @XmlAttribute(required=true).
Correspondance avec les documents JSON
Il existe depuis peu une norme (JSON Processing) fournissant deux méthodes de traitement, par flux d'événements (style SAX) et par un modèle universel (style DOM). En revanche, il n'existe pas encore de norme pour un data binding. Elle est en cours d'élaboration et bénéficiera des frameworks existants, comme Jettison ou Jackson.
Bibliographie : cf. chap. 12, Beginning Java EE 7, d'Antonio Goncalves.
Annexe : les JavaBeans
Une classe adhère à la spécifiaction JavaBean si elle vérifie les propriétés suivantes :
- classe possédant un constructeur sans argument,
- classe dont les champs (variables d'instances) sont accessibles via des accesseurs (getter et setter), avec la convention de nommage habituelle (variable x, méthodes getX et setX).
Cette définition est issue d'une spécification ancienne (1997, au début de Java) : cf. la spécification.
Un objet d'une telle classe est appelé un JavaBean (composant Java). Un champ d'un tel objet est appelé une propriété.