jeudi 1 septembre 2011

Contrainte d'unicité et clé étrangère dans un XSD

Les XML Schema Definition ou XSD sont apparus en 2001. Il permettent de définir la structure et le type de contenu d'un fichier XML. En Java, on les rencontre régulièrement au coeur des web services dans l'élaboration de WSDL, à la base d'outils de génération automatique de code tels que Castor, lors de l'élaboration de modules spécifiques paramétrables par l'utilisateur (les développeurs s'orienteront plutôt vers les annotations depuis JAVA 5), ...

Les XSD offrent également la possibilité de définir des contraintes d'unicité sur la valeur de certains nœuds et de leurs attributs ainsi que des relations de type clés étrangères ou foreign key. C'est cet aspect que nous allons traité au travers d'un exemple très simple.

Prenons le fichier XML suivant, permettant de décrire une équipe de football :
<equipe 
    nom="Olympique Lyonnais"
    xmlns="http://avianey.blogspot.com" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="http://avianey.blogspot.com my.xsd">
        
    <effectif>
        <joueur nom="Lloris" prenom="Hugo" numeroMaillot="1">
            <poste>gardien</poste>
        </joueur>
        <joueur nom="Bastos" prenom="Michel" numeroMaillot="11">
            <poste>milieu</poste>
        </joueur>
        <joueur nom="Lopez" prenom="Lisandro" numeroMaillot="11">
            <poste>attaquant</poste>
        </joueur>
    </effectif>

    <capitaine numeroMaillot="9"/>

</equipe>
Ce fichier XML est valide au regard du schéma XSD suivant :
<xs:schema 
    xmlns="http://avianey.blogspot.com"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    targetNamespace="http://avianey.blogspot.com" 
    elementFormDefault="qualified">
    
    <xs:element name="equipe">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="effectif" minOccurs="1" maxOccurs="1">
                    <xs:complexType>
                        <xs:sequence>
                            <xs:element name="joueur" minOccurs="1" 
                                        maxOccurs="unbounded" type="joueur"/>
                        </xs:sequence>
                    </xs:complexType>
                </xs:element>
                <xs:element name="capitaine" minOccurs="1" maxOccurs="1">
                    <xs:complexType>
                        <xs:attribute name="numeroMaillot" use="required" type="xs:int"/>
                    </xs:complexType>
                </xs:element>
            </xs:sequence>
            <xs:attribute name="nom" use="required" type="xs:string"/>
        </xs:complexType>
    </xs:element>
    
    <xs:complexType name="joueur">
        <xs:sequence>
            <xs:element name="poste" minOccurs="1" maxOccurs="1">
                <xs:simpleType>
                    <xs:restriction base="xs:string">
                      <xs:enumeration value="gardien"/>
                      <xs:enumeration value="défenseur"/>
                      <xs:enumeration value="milieu"/>
                      <xs:enumeration value="attaquant"/>
                    </xs:restriction>
                </xs:simpleType>
            </xs:element>
        </xs:sequence>
        <xs:attribute name="nom" type="xs:string" use="required"/>
        <xs:attribute name="prenom" type="xs:string" use="required"/>
        <xs:attribute name="numeroMaillot" use="required">
            <xs:simpleType>
                <xs:restriction base="xs:int">
                    <xs:minInclusive value="0"/>
                </xs:restriction>
            </xs:simpleType>
        </xs:attribute>
    </xs:complexType>
    
</xs:schema>
Ce qui n'est pas totalement satisfaisant car notre XML comporte deux joueurs avec un même numéro de maillot et parce que le capitaine est définit par référence au numéro de maillot d'un joueur qui n'existe pas !

Ajout d'une contrainte d'unicité

Nous souhaitons maintenant pouvoir imposer l'unicité des numéros de maillots entre les joueurs d'une équipe. Pour cela il faut ajouter une contrainte d'unicité sur l'attribut numeroMaillot d'une balise <joueur>. La contrainte d'unicité s'ajoute dans le schéma XSD au niveau de la déclaration d'un élément parent de l'élément sur lequel la contrainte d'unicité doit s'appliquer. La balise à utiliser est la balise <unique> se positionne à l'intérieure de la balise <element> :
<xs:unique name="uniciteNumeroMaillot" >      
    <xs:selector xpath=".//tns:joueur" />      
    <xs:field xpath="@numeroMaillot" />    
</xs:unique>
La contrainte d'unicité doit posséder un nom et contient deux fils :
<selector>
Expression XPATH indiquant le chemin des balises sur lesquelles la contrainte d'unicité doit s'appliquer.
<field>
Expression XPATH indiquant le champ ou attribut portant la contrainte. Identifiant de la valeur devant être unique.
Les expressions XPATH doivent être qualifiées afin que la contrainte soit correctement interprétée par la majorité des parseurs et validateurs. Il convient donc de déclarer un namespace préfixé identique au namespace cible (si cela n'est pas déjà le cas) :
<xs:schema 
    xmlns="http://avianey.blogspot.com"
    xmlns:tns="http://avianey.blogspot.com"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    targetNamespace="http://avianey.blogspot.com" 
    elementFormDefault="qualified">

Ajout d'une contrainte de type clé étrangère

Maintenant qu'un même numéro de maillot ne peut pas être attribué à deux joueurs différents, nous désirons nous assurer que le capitaine de l'équipe est bien désigné par un numéro de maillot attribué à un joueur de l'équipe. Une foreign key doit donc être mise en place entre l'attribut numeroMaillot de la balise <capitaine> et celui des balises <joueur>. L'attribut numeroMaillot des balises <joueur> est dans un premier temps déclaré en tant que clé dans le schéma XSD au niveau de la déclaration d'un élément parent des balises <joueur>. La balise à utiliser est la balise <key> se positionne à l'intérieure de la balise <element> :
<xs:key name="numeroMaillotCapitaine">
    <xs:selector xpath=".//tns:joueur" />
    <xs:field xpath="@numeroMaillot" />
</xs:key>
La clé doit posséder un nom et contient deux fils :
<selector>
Expression XPATH indiquant le chemin des balises qui porteront les valeurs de référence.
<field>
Expression XPATH indiquant le champ ou l'attribut portant la valeur de référence.
Au même niveau que la balise <key>, la balise <keyref> permet de spécifier les éléments qui seront contraints par la clé étrangère :
<xs:keyref name="numeroMaillotCapitaineRef" refer="numeroMaillotCapitaine">
    <xs:selector xpath="./tns:capitaine" />
    <xs:field xpath="@numeroMaillot" />
</xs:keyref>
La référence doit posséder un nom et contient deux fils :
<selector>
Expression XPATH indiquant le chemin des balises qui seront contraintes par la clé étrangère.
<field>
Expression XPATH indiquant le champ ou l'attribut dont la valeur devra être identique à une valeur de référence.
Il est important de faire attention au type des données utilisées pour la clé et la référence qui doivent être les mêmes. Un entier déclaré avec le type xs:int ne sera pas considéré comme étant égal à sa représentation sous la forme d'une chaine de caractère de type xs:string. Comme pour la contrainte d'unicité, les balises utilisées dans les chemins XPATH devront elles aussi être préfixées. Au final, notre schéma XSD sera le suivant :
<xs:schema 
    xmlns="http://avianey.blogspot.com"
    xmlns:tns="http://avianey.blogspot.com"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    targetNamespace="http://avianey.blogspot.com" 
    elementFormDefault="qualified">
    
    <xs:element name="equipe">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="effectif" minOccurs="1" maxOccurs="1">
                    <xs:complexType>
                        <xs:sequence>
                            <xs:element name="joueur" minOccurs="1" 
                                        maxOccurs="unbounded" type="joueur"/>
                        </xs:sequence>
                    </xs:complexType>
                </xs:element>
                <xs:element name="capitaine" minOccurs="1" maxOccurs="1">
                    <xs:complexType>
                        <xs:attribute name="numeroMaillot" use="required" type="xs:int"/>
                    </xs:complexType>
                </xs:element>
            </xs:sequence>
            <xs:attribute name="nom" use="required" type="xs:string"/>
        </xs:complexType>
        <xs:unique name="uniciteNumeroMaillot" >      
            <xs:selector xpath=".//tns:joueur" />      
            <xs:field xpath="@numeroMaillot" />    
        </xs:unique>
        <xs:key name="numeroMaillotCapitaine">
            <xs:selector xpath=".//tns:joueur" />
            <xs:field xpath="@numeroMaillot" />
        </xs:key>
        <xs:keyref name="numeroMaillotCapitaineRef" refer="numeroMaillotCapitaine">
            <xs:selector xpath="./tns:capitaine" />
            <xs:field xpath="@numeroMaillot" />
        </xs:keyref>
    </xs:element>
    
    <xs:complexType name="joueur">
        <xs:sequence>
            <xs:element name="poste" minOccurs="1" maxOccurs="1">
                <xs:simpleType>
                    <xs:restriction base="xs:string">
                      <xs:enumeration value="gardien"/>
                      <xs:enumeration value="défenseur"/>
                      <xs:enumeration value="milieu"/>
                      <xs:enumeration value="attaquant"/>
                    </xs:restriction>
                </xs:simpleType>
            </xs:element>
        </xs:sequence>
        <xs:attribute name="nom" type="xs:string" use="required"/>
        <xs:attribute name="prenom" type="xs:string" use="required"/>
        <xs:attribute name="numeroMaillot" use="required">
            <xs:simpleType>
                <xs:restriction base="xs:int">
                    <xs:minInclusive value="0"/>
                </xs:restriction>
            </xs:simpleType>
        </xs:attribute>
    </xs:complexType>
    
</xs:schema>
Le fichier XML initial n'est plus valide. Il faut rectifier le numéro de maillot de Lisandro Lopez pour que chaque joueur dispose de son propre numéro de maillot et que celui du capitaine correspondent à celui d'un joueur :
<equipe 
    nom="Olympique Lyonnais"
    xmlns="http://avianey.blogspot.com" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="http://avianey.blogspot.com unique_key.xsd">
        
    <effectif>
        <joueur nom="Lloris" prenom="Hugo" numeroMaillot="1">
            <poste>gardien</poste>
        </joueur>
        <joueur nom="Bastos" prenom="Michel" numeroMaillot="11">
            <poste>milieu</poste>
        </joueur>
        <joueur nom="Lopez" prenom="Lisandro" numeroMaillot="9">
            <poste>attaquant</poste>
        </joueur>
    </effectif>

    <capitaine numeroMaillot="9"/>

</equipe>
Fork me on GitHub