====== TP Mappings XML - Relationnel - Objet ====== Ce TP se déroule sur deux séances. Il a pour objectif d'établir les liens entre les modèles semi-structurés (XML), relationnels (SGBDR: Oracle / PostgreSQL) et objet (Java). === Rendu === Le programme (projet maven + script SQL commenté dans un fichier zip) est à rendre pour le jeudi 12 janvier 2012 par mail à [[haytham.elghazel@univ-lyon1.fr?suject=[BDAV-TP3] Rendu|Haytham Elghazel]] avec pour sujet [BDAV-TP3] Rendu. Remarque: lorsque l'énoncé demande de modifier une requête, dont donnera le code de chaque étape, pas seulement de la requête finale. ===== Mapping XML - Relationnel ===== Dans cette partie on mettra en œuvre la génération de documents XML en SQL. Cette partie peut être réalisée sur la base Oracle ou sur une base PostgreSQL, au choix. Les fonctions de génération de XML sont presque les mêmes sur les deux système. Pour la version Oracle, on s'appuiera sur le schéma fourni au [[enseignement:bdav:tp-xml-xquery-oracle|TP1]], pour PostgreSQL sur sa traduction fournie au [[enseignement:bdav:tp-plsql-jdbc|TP2]]. ==== XMLElement ==== La fonction ''XMLElement'' permet de créer un élément XML. L'expression suivante permet de créer un élément nommé ''prix'' contenant un noeud texte dont la valeur est donnée par l'attribut ''prix_vente'' XMLElement(name "prix", prix_vente) On peut en particulier l'utiliser dans une requête: SELECT XMLElement(name "prix", prix_vente) FROM ventes Il est possible de spécifier plusieurs enfants pour un élément en les séparant par des virgules. Il est également possible d'imbriquer les appels à XMLElement afin de construire un morceau de document XML plus complexe. -> Écrire une requête qui pour chaque partie génère un élément XML ”partie” avec un élément ”titre” incluant le titre de la partie et un élément ”contenu” incluant le contenu de la partie. ==== XMLAttributes ==== La fonction ''XMLAttributes'' permet d'ajouter des attributs à un élément. Le nom par défaut utilisé pour chaque attribut XML est le nom de l'attribut SQL dont on a pris la valeur. Il est également possible d'utiliser la notation ''AS un_nom'' afin de changer le nom de l'attribut XML (de manière similaire au nom des colonnes dans un SELECT). L'exemple suivant crée des éléments "''article''" avec un attribut XML "''ident''" donné par l'attribut SQL ''ident'', un attribut XML "''prix''" donné par l'attribut SQL ''prix_vente'' et un contenu texte donné par l'attribut SQL ''description'': XMLElement(name "article", XMLAttributes(ident,prix_vente as "prix"), description) -> Modifier la requête précédente afin d'ajouter un attribut ”id” contenant l'identifiant de la partie. On fera bien attention à la casse des éléments/attributs((qui, au contraire de SQL est importante)). ==== XMLForest ==== La fonction ''XMLForest'' crée pour chacun de ses arguments un élément (dont on peut optionnellement préciser le nom avec un ''AS'') qui contient du texte correspondant à la valeur calculée pour cet argument. Par exemple: XMLElement(name "article", XMLAttributes(ident,prix_vente as "prix"), XMLForest(nom_article as "nom",description) -> Modifier la requête précédente en utilisant cette fonction plutôt que des XMLElement imbriqués dans le XMLElement principal. -> Que peut-on remarquer sur les parties n'ayant pas de titre ? ==== XMLAgg ==== La fonction ''XMLAgg'' est une fonction d'agrégation pour le type XML (donc typiquement à utiliser en conjonction avec un ''GROUP BY''). Son effet est de mettre les unes à la suite des autres les différentes valeurs de l'expression passée en argument. La requête suivante illustre son fonctionnement: SELECT XMLElement(name "departement", XMLAttributes(deptno), XMLAgg(XMLElement(name "employe",ename))) as RESULTAT FROM scott.emp GROUP BY deptno; -> Modifier la requête précédente pour ajouter à chaque élément partie des //éléments// "''auteur''" pour chaque auteur. Cet élément contiendra avec un attribut "''ref''" dont la valeur sera l'identifiant de l'auteur concerné. On supposera que toute partie a au moins un auteur. -> Modifier à nouveau la requête pour que le contenu des éléments auteur soit du texte contenant le nom de l'auteur et pour remplacer l'attribut ''ref'' par un nouvel attribut "''email''" ayant la valeur adéquate. ==== Vues pour les sous-parties ==== -> Créer un vue associant à chaque identifiant de partie la concaténation (via XMLAgg) d'un élément par partie incluse (directement) dans la partie concernée. Ces éléments seront nommés "''sous-partie''" et contiendront seulement un attribut un attribut "''ref''" contenant l'identifiant de la partie concernée. -> Créer suivant la même idée une vue pour créer la liste des auteurs. -> Utiliser ces deux vues pour ajouter dans la requête de génération de XML pour les parties la liste des sous-parties. Il se peut que les parties n'ayant pas de sous parties n'apparaissent pas dans le résultat. On pourra alors utiliser un LEFT((ou un RIGHT)) OUTER JOIN pour les récupérer. -> Créer une vue en utilisant cette requête (penser à ajouter un attribut SQL id pour pouvoir requêter cette vue), puis créer une vue qui à chaque livre (id) associe un élément "''livre''" ayant un élément titre, ainsi qu'un élément "''editeur''" construit sur le modèle des auteurs de parties. Cet élément livre contiendra également l'ensemble des parties composant ce livre((on suppose qu'un livre contient au moins une partie)). -> Donner la DTD correspondant aux documents XML produits par les vues xml sur les parties et les livres (les deux dernières vues). ===== Récupération des données XML en Java ===== Le type Java ''java.sql.SQLXML'' permet de représenter une donnée XML renvoyée dans un attribut du résultat d'une requête JDBC. Le code suivant montre comment récupérer une ''javax.xml.transform.Source'' depuis une résultat d'une requête: ResultSet rs = ... while (rs.next()) { SQLXML xmldata = rs.getSQLXML(...); javax.xml.transform.dom.DOMSource domSource = xmldata.getSource(DOMSource.class); Document document = (Document) domSource.getNode(); // utilisation du document } Pour récupérer une javax.xml.transform.sax.SAXSource, il suffit de remplacer ''DOMSource.class'' par ''SAXSource.class''. -> Reprendre projet du [[enseignement:bdav:tp-plsql-jdbc|TP JDBC]]. Dans la classe ''LivreDAO'', ajouter une méthode ''getLivreXML'' qui utilise la vue précédement crée pour fournir une représentation XML d'un livre dont l'identifiant est fourni en argument: public javax.xml.transform.Source getLivreXML(long livreId) throws SQLException :!: Sous Oracle, le pilote de base ne sais pas bien gérer le type SQLXML, il faut donc s'inspirer du code suivant: ResultSet rs = ... if (rs.next()) { Reader xmldata = rs.getCharacterStream(...); return new StreamSource(xmldata); } else { return null; } et ajouter dans la requête l'utilisation de getClobVal() comme suit (attention aux parenthèses): SELECT (mon_xml).getClobVal() as mon_xml, ... FROM ... ===== Ajout de données XML dans une base relationnelle ===== ==== Fonction de création de livre ==== -> Créer un déclencheur PL/SQL pour numéroter les livres. Ajouter une fonction PL/SQL de création de livre construite sur le principe de la fonction de création de partie. Ajouter à la classe LivreDAO une méthode correspondante à la fonction précédente. :!: Avec Oracle, mieux vaut utiliser des procédures et des CallableStatements, //c.f.// http://java.developpez.com/faq/jdbc/?page=callablestatement :!: -> Procéder de la même manière pour créer une méthode d'ajout de membre. -> Créer une méthode qui permet de changer le nom et l'email d'un membre via une requête UPDATE. ==== Insertion de données XML via SAX ==== Relire les {{:enseignement:bdav:apis-xml.pdf|transparents sur les APIs XML}}. On souhaite dans cette partie pouvoir importer des documents conformes à la DTD du [[enseignement:bdav:tp-xml-xquery-oracle|TP1]]. -> Dans le package ''epul.bdav'', créer une classe ''LivreImportHandler'' qui étend ''org.xml.sax.helpers.DefaultHandler''. Elle possèdera un champ ''dao'' de type ''LivreDAO'' initialisé via un argument du constructeur. Cette classe sera utilisée en conjonction avec un parser SAX qui appellera des méthodes telles que ''startDocument'', ''startElement'', ''endElement'', etc lors de la lecture d'un fichier XML. Elle devra insérer les données du document dans la base relationnelle. Pour cela il faudra retenir des informations via des champs de la classe. Il faudra en particulier traiter: * les livres/parties sans identifiant * les identifiants de membre * les liens implicites partie <-> partie et partie <-> livre. Il sera nécessaire de garder des informations de contexte dans des champs, par exemple le texte contenu dans le dernier élément ''titre'' qui a été lu, l'identifiant de la salle courante, etc. Les exceptions ''SQLException'' seront rattrapées et relancées via: try { ... } catch (SQLException ex) { throw new SAXException(ex); } :!: Les identifiants du documents servent uniquement en interne, on supposera que tous les identifiants des tuples de la base sont générés. Il peut ainsi être nécessaire de maintenir une correspondance id xml -> id sql((via une java.util.Map par exemple)). :!: :!: Les données sur les membres venant après les données sur les livres, il sera nécessaire de mettre à jour leurs informations après les avoir créés avec des informations par défaut (nom et email vides). :!: :!: Les parties pouvant être imbriquées les unes dans les autres, il sera nécessaire d'utiliser une pile((java.util.Stack)) pour connaître la liste des parties ancêtres de la partie courante. On pourra mettre dans un premier temps au point l'import sans partie imbriquées :!: -> Créer une méthode ''importXML'' dans la classe ''LivreDAO'' prenant en argument une ''javax.xml.transform.Source'' et mettant en oeuvre le code suivant pour utiliser le ''LivreImportHandler'': javax.xml.transform.Transformer copy = javax.xml.transform.TransformerFactory.newInstance().newTransformer(); copy.transform(la_source, new javax.xml.transform.sax.SAXResult(new LivreImportHandler(this))); ===== Mappings Relationnel - Objet ===== ==== Mise à jour du projet ==== Ajouter les dépendances vers [[http://hibernate.org|Hibernate]], une implémentation de l'api JPA. Pour cela, ajouter au bon endroit dans le fichier ''pom.xml''((qui se trouve dans //Project Files//)): org.hibernate hibernate-entitymanager 3.6.9.Final org.slf4j slf4j-simple 1.6.1 runtime Ajouter un fichier ''src/main/resources/META-INF/persistence.xml'' ayant le contenu suivant: org.hibernate.ejb.HibernatePersistence org.hibernate.ejb.HibernatePersistence Ajouter les deux classes suivantes dans les //Sources Packages//: Livre package epul.bdav.modele; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.SequenceGenerator; /** * * @author ecoquery */ @Entity public class Livre { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "LIVRE_GEN") @SequenceGenerator(name = "LIVRE_GEN", sequenceName = "livre_seq", allocationSize=1) private int id; @Column(name="titre") private String titre; @ManyToOne @JoinColumn(name = "editeur") private Membre editeur; public int getId() { return id; } public String getTitre() { return titre; } public void setTitre(String titre) { this.titre = titre; } public Membre getEditeur() { return editeur; } public void setEditeur(Membre editeur) { this.editeur = editeur; } } Membre package epul.bdav.modele; import java.util.ArrayList; import java.util.Collection; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.OneToMany; import javax.persistence.SequenceGenerator; /** * * @author ecoquery */ @Entity public class Membre { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "MEMBRE_GEN") @SequenceGenerator(name = "MEMBRE_GEN", sequenceName = "livre_seq", allocationSize=1) private int id; @OneToMany(mappedBy="editeur") private Collection livres = new ArrayList(); } Ainsi que la classe de test suivante, cette fois-ci dans les //Test Packages// package epul.bdav; import javax.persistence.EntityManager; import javax.persistence.Persistence; import junit.framework.TestCase; /** * * @author ecoquery */ public class JPATest extends TestCase { public void testOracle() { EntityManager em = Persistence.createEntityManagerFactory("bdav_pu_oracle").createEntityManager(); em.close(); } public void testPostgreSQL() { EntityManager em = Persistence.createEntityManagerFactory("bdav_pu_postgres").createEntityManager(); em.close(); } } -> Commenter le test inutilisé((i.e. celui correspondant à postgres si vous utilisez oracle et inversement)) et modifier éventuellement le fichier ''persistence.xml'' en changeant le login et le mot de passe le cas échéant. Vérifier que les tests se déroulent bien((cela requiert le schéma fourni, ainsi que la séquence ''livre_seq'')). ==== Première modifications ==== -> Regarder la [[http://docs.oracle.com/javaee/5/api/|javadoc]] pour chaque annotation des classes Livre et Membre. Ajouter un commentaire décrvant brièvement l'effet de chaque annotation. -> Ajouter les accesseurs utiles pour les champs de la classe Membre. Comme ''id'' a sa valeur générée, on aura pas de //setter// dessus. Ajouter les informations manquantes dans la classe Membre (nom et email). -> La cohérence des champs Livre.editeur et Membre.livres n'est actuellement pas garantie. Modifier/ajouter les méthodes nécessaires pour encapsuler ces champs et garantir cette cohérence. :!: L'ORM va injecter sa propre implémentation de Collection((afin de pouvoir être paresseux sur le chargement des livres d'un membre)), il ne faut donc pas créer de nouvelle instance pour le champ livre. Le seul moment où l'on affecte directement la collection est lors de l'initialisation par défaut. Dans la même idée on ne mettra pas de setter sur ''livres'', l'ORM étant capable d'affecter au besoin directement un champ, même si celui-ci est privé. ==== DAO, version ORM ==== -> Créer une classe ''epul.bdav.LivreDAOORM''. Elle contiendra un champ ''EntityManager em'', initialisé via son constructeur. Elle devra passer avec succès le test unitaire suivant: package epul.bdav; import epul.bdav.modele.Livre; import epul.bdav.modele.Membre; import javax.persistence.EntityManager; import javax.persistence.Persistence; import junit.framework.TestCase; /** * * @author ecoquery */ public class LivreDAOORMTest extends TestCase { private EntityManager em; protected EntityManager createEntityManager() { return Persistence.createEntityManagerFactory("bdav_pu_oracle").createEntityManager(); } @Override protected void setUp() throws Exception { if (em == null) { em = createEntityManager(); } em.getTransaction().begin(); } @Override protected void tearDown() throws Exception { em.getTransaction().rollback(); } public void testOperationsVariees() { LivreDAOORM dao = new LivreDAOORM(em); Membre m = dao.createMembre("Sting", "sting@police.com"); assertNotNull(m); Membre m2 = dao.getMembre(m.getId()); assertEquals(m,m2); assertTrue(m==m2); Livre l = dao.createLivre("Synchronicity", m); assertNotNull(l); Livre l2 = dao.getLivre(l.getId()); assertEquals(l,l2); assertTrue(l==l2); assertEquals(1,m.getLivres().size()); // adapter éventuellement l'instruction précédente suivant le niveau // d'encapsulation du champ livres de la classe Membre int lId = l.getId(); dao.supprimeLivre(lId); assertNull(dao.getLivre(lId)); int mId = m.getId(); dao.supprimeMembre(mId); assertNull(dao.getMembre(mId)); m = dao.createMembre("Peter Gabriel", "peter@secretworld.org"); l = dao.createLivre("Live", m); mId = m.getId(); lId = l.getId(); dao.supprimeLivre(l); dao.supprimeMembre(m); assertNull(dao.getLivre(lId)); assertNull(dao.getMembre(mId)); } } ==== Parties ==== -> Créer une classe ''epul.bdav.modele.Partie'' pour représenter les parties de livre. Annoter cette classe pour la persistance. On fera particulièrement attention aux points suivants: * La gestion de l'identifiant est a faire via la séquence ''livre_seq''. * Les associations correspondant aux auteurs et aux sous-parties doivent être prises en compte. * On s'assurera de la cohérence entre les différents champs représentant ces associations. * On ajoutera les méthodes adéquates à ''LivreDAOORM''. Remarque: il est possible de changer la propriété ''hibernate.hbm2ddl.auto'' du fichier ''persistence.xml'' en ''update'', ce qui autorise le framework à changer le schéma. Cela peut être utile afin de comprendre ce qui correspond aux annotations. Cependant votre mapping devra correspondre au schéma de l'énoncé du TP (i.e. le mapping doit fonctionner en ''validate'' avec le schéma relationnel de départ). ===== Mappings XML - Objet ===== Cette partie a pour but d'introduire les mappings XML - Objets par annotation des classes (API JAXB) ==== Mise à jour du projet: annotations XML ==== Modifier les classes Membre et Livre en intégrant les modifications dans le code donné ci-dessous. Dans un premier temps, ajouter une annotation ''@XmlTransient'' sur les champs et les méthodes ''getXXX'' que vous avez ajouté dans ces classes. Membre package epul.bdav.modele; import java.util.ArrayList; import java.util.Collection; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.OneToMany; import javax.persistence.SequenceGenerator; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlID; import javax.xml.bind.annotation.XmlTransient; /** * * @author ecoquery */ @Entity public class Membre { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "MEMBRE_GEN") @SequenceGenerator(name = "MEMBRE_GEN", sequenceName = "livre_seq", allocationSize=1) @XmlTransient // l'id sera transformé en String private int id; @OneToMany(mappedBy="editeur") @XmlTransient private Collection livres = new ArrayList(); @XmlTransient public int getId() { return id; } // Ajouté pour gérer les identifiants hors des ORMs public void setId(int id) { this.id = id; } @XmlTransient public Collection getLivres() { return livres; } // Id sous forme de chaine de caractères @XmlAttribute(name="id") @XmlID private String getStringId() { return String.valueOf(id); } private void setStringId(String newId) { id = Integer.parseInt(newId); } } Livre: package epul.bdav.modele; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.SequenceGenerator; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlIDREF; import javax.xml.bind.annotation.XmlTransient; /** * * @author ecoquery */ @Entity // inutile de mettre @XmlRootElement, sauf si on souhaite pouvoir // lire/écrire un document contenant uniquement un élément livre public class Livre { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "LIVRE_GEN") @SequenceGenerator(name = "LIVRE_GEN", sequenceName = "livre_seq", allocationSize=1) @XmlAttribute private int id; @Column(name="titre") @XmlElement(name="titre") private String titre; @ManyToOne @JoinColumn(name = "editeur") @XmlIDREF @XmlAttribute private Membre editeur; @XmlTransient public int getId() { return id; } // Ajouté pour gérer les identifiants hors des ORMs public void setId(int id) { this.id = id; } @XmlTransient public String getTitre() { return titre; } public void setTitre(String titre) { this.titre = titre; } @XmlTransient public Membre getEditeur() { return editeur; } public void setEditeur(Membre editeur) { this.editeur = editeur; } } Créer une classe ''epul.bdav.modele.ListeLivres'' avec le code suivant: package epul.bdav.modele; import java.util.ArrayList; import java.util.Collection; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlTransient; /** * * @author ecoquery */ @XmlRootElement(name="livres") public class ListeLivres { @XmlElement(name="livre") private Collection livres = new ArrayList(); @XmlElement(name="membre") private Collection membres = new ArrayList(); @XmlTransient // nécessaire car JAXB différencie les accesseurs et les champs public Collection getLivres() { return livres; } public void setLivres(Collection livres) { this.livres = livres; } @XmlTransient // nécessaire car JAXB différencie les accesseurs et les champs public Collection getMembres() { return membres; } public void setMembres(Collection membres) { this.membres = membres; } } Un exemple d'utilisation est donné dans le test unitaire suivant: package epul.bdav; import epul.bdav.modele.ListeLivres; import epul.bdav.modele.Livre; import epul.bdav.modele.Membre; import java.io.File; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import junit.framework.TestCase; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * * @author ecoquery */ public class JAXBTest extends TestCase { private static Logger log = LoggerFactory.getLogger(JAXBTest.class); public void testGenerateAndLoad() throws JAXBException { try { String filename = "target/surefire/JAXB_testGenerateAndLoad.xml"; Membre m = new Membre(); m.setId(1); Livre l = new Livre(); l.setId(1); l.setEditeur(m); l.setTitre("Un livre intéressant"); Livre l2 = new Livre(); l2.setId(2); l2.setEditeur(m); l2.setTitre("Un deuxième livre intéressant"); ListeLivres ll = new ListeLivres(); ll.getLivres().add(l); ll.getLivres().add(l2); ll.getMembres().add(m); // objet pour (dé)sérialiser les objets en XML JAXBContext ctx = JAXBContext.newInstance(ListeLivres.class); File f = new File(filename); File f2 = f.getParentFile(); if (!f2.isDirectory()) { assertTrue(f2.mkdirs()); } ctx.createMarshaller().marshal(ll,f); Object o = ctx.createUnmarshaller().unmarshal(f); assertNotNull(o); assertTrue(o instanceof ListeLivres); } catch (JAXBException e) { log.error("Erreur JAXB: "+e.getMessage(), e); throw e; } } } -> Exécuter ce test et regarder le fichier XML généré. -> Pour chaque nouvelle annotation, lire la [[http://docs.oracle.com/javase/6/docs/api/|documentation]] et ajouter un commentaire contenant une brève explication de son effet. ==== Modifications de la sérialisation XML des classes Membre et Livre ==== -> Annoter les champs ou les accesseurs indiquant le nom et l'email de l'auteur. -> Le champ ''livres'' de la classe Membre n'est pas correctement reconstitué. Déplacer les annotations du champ ''editeur'' sur son accesseur et, au besoin, ajouter le code nécessaire pour mettre à jour au passage le champ ''livres'' du membre. ==== Sérialisation XML des parties ==== -> Annoter la classe Partie, ainsi que (si nécessaire) les champs/accesseurs correspondants dans les classes Membre et Livre. Au final, on souhaite que le XML généré corresponde à la DTD (et au schéma correspondant) du [[enseignement:bdav:tp-xml-xquery-oracle|premier TP]].