Dans l'étude précédente, nous avons passé pas mal de temps à prendre connaissance des beans entités. Le gros avantage de ce type de bean c'est qu'il permet de s'occuper automatiquement de la sauvegarde dans une base de données alors que nous travaillons avec des objets. Ainsi, nous sommes en présence d'une base de données objet plutôt qu'une base de données relationnelle.
Par contre, lorsque nous désirons retrouver des informations, nous les retrouvons certes toujours sous la forme d'un objet, ce qui est vraiment super, mais nous sommes systématiquement obligé de passer par la clé primaire, ce qui est cette fois-ci plutôt limitatif. Ce serait plus judicieux de proproser des requêtes de recherche qui soient adaptées à ce que nous désirons. Il serait par exemple souhaitable d'effectuer une recherche par rapport au nom de la personne plutôt que de passer systématiquement par un identifiant qui, très souvent, est un numéro anonyme.
Comment allons nous procéder ?
Depuis longtemps déjà, SQL est le langage de référence pour exécuter des requêtes sur les bases de données relationnelles. Certes performant et standardisé, celui-ci ne correspond plus aux langages de développement orientés objets.
Le mapping Objet / Relationnel a vu le jour, comme nous l'avons vu, au travers des beans entités. Se dotant de dérivés de SQL qui permettent d'exécuter des requêtes sur les objets, ces systèmes sont très vite devenus référents en la matière. C'est le cas d'EJB-QL qui permet d'utiliser directement les beans entités dans les requêtes.
EJB-QL (Entreprise JavaBean Query Language) est une spécification de langage de requêtes. Intégré au système EJB, ce langage de requêtes présente plusieurs avantages.
La finalité de ce langage est en quelque sorte identique à celle du gestionnaire de persistance, qui offre, en effet, la possibilité d'effectuer des requêtes de type INSERT, UPDATE ou encore DELETE.
Afin de travailler directement avec les entités, EJB-QL s'appui sur un schéma abstrait des beans entités. Le schéma abstrait est une représentation interne des entités et de leurs relations. Toutefois, même si ce terme semble complexe à première vue, nous l'assimilons couramment au nom du bean entité.
Avec la spécification EJB 3, nous utilisons généralement les annotations. Nous pouvons spécifier le nom de référence par l'attribut name de l'annotation @Entity :
@Entity(name = "UnePhoto")
public class Photo {
Toutefois, cette attribut name sur l'annotation @Entity, n'est pas absolument indispensable. En effet, par défaut, le nom de la classe du bean entité est utilisé comme nom de référence.
Ainsi si nous pouvons proposer l'écriture suivante :
@Entity
public class Photo {
Le nom de référence devient Photo.
Le terme abstrait distingue ce schéma du schéma physique de la base de données. Dans une base de données relationnelle, le schéma physique correspond à la structure des tables et des colonnes. En EJB-QL, vous travaillez avec les schémas abstraits et non avec des tables de votre schéma physique.
En résumé, il faut retenir qu'il s'agit d'une représentation abstraite des beans entités, utilisé par EJB-QL pour naviguer entre les propriétés persistantes et relationnelles.
Avant de rentrer dans les détails innombrables de ce langage, voici le premier exemple simple qui vous permettra de comprendre plus facilement la suite des explications.
Je me sers toujours du projet d'archivage des photos numériques qui me permet de stocker les fichiers photos dans la même machine que le serveur d'applications. Je rappelle que j'utilise également ce serveur pour enregistrer un certain nombre d'informations relatives aux photos stockées. Ainsi, je propose de rajouter une méthode getPhotos() au bean session EditionPhotoBean qui permet de retourner l'ensemble des photos archivées. Voici donc ce que nous pouvons écrire :
@Stateless public class EditionPhotoBean implements EditionPhotoLocal { @PersistenceContext(unitName = "Photos-ejbPU") EntityManager persistance; private final String répertoire = "J:/Photos/"; ... public List<Photo> getPhotos() { String texteRequête = "SELECT photo FROM Photo AS photo"; Query requête = persistance.createQuery(texteRequête); return requête.getResultList(); } }
Cette méthode est très courte et utilise trois concepts différents :
Cette méthode getPhotos() retourne donc l'ensemble des instances du bean entité Photo dans une liste. Celle-ci peut ensuite être parcourue par un foreach. Les détails d'utilisation de ces éléments sont expliqués tout au long de cette partie.
L'API Query est le point essentiel pour la jonction entre l'application et l'exécution des requêtes EJB-QL. Les méthodes de celles-ci sont regroupées dans l'interface javax.persistence.Query :
public interface Query { public List getResultList(); public Object getSingleResult(); public int executeUpdate(); public Query setMaxResults(int max); public Query setFirstResults(int index); public Query setHint(String argument, Object argument); public Query setParameter(String nom, int valeur); public Query setParameter(String nom, Date date, TemporalType type); public Query setParameter(String nom, Calendar calendrier, TemporalType type); public Query setParameter(int position, Object valeur); public Query setParameter(int position, Date date, TemporalType type); public Query setParameter(int position, Calendar calendrier, TemporalType type); public Query setFlushMode(FlushModeType mode); }
L'intérêt principal de cette API est de pouvoir créer des requêtes dynamiques sous forme de simples chaînes de caractères, et non de manière statique dans le descripteur de déploiement. Il existe différents moyens de récupérer un objet de type Query. Les méthodes correspondantes sont regroupées dans l'interface EntityManager.
public interface EntityManager { public Query createQuery(String requêteEJB-QL); public Query createNamedQuery(String requêteNommée); public Query createNativeQuery(String requêteSQL); public Query createNativeQuery(String requêteSQL, Class résultat); public Query createNativeQuery(StringrequêteSQL, String résultat); }
Dans ce chapitre, nous nous intéresserons uniquement à la première méthode createQuery() de cette interface. Nous verrons les autres méthodes, utltérieurement, suivant les chapitres traités.
Revoici l'exemple qui permet de récupérer l'ensemble des photos archivées :
String texteRequête = "SELECT photo FROM Photo AS photo"; Query requête = persistance.createQuery(texteRequête); List<Photo> résultat = requête.getResultList(); ... for (Photo unePhoto : résultat) { System.out.println("Identification : " + unePhoto.getIdentification()); ... }
Nous récupérons ici un objet Query via la méthode createQuery() du gestionnaire d'entités. Il suffit ensuite d'appeler la méthode getResultList() pour exécuter la requête et récupérer ainsi le résultat.
Voici d'ailleurs une description détaillée des différentes méthodes classiques de l'interface Query.
Cette méthode exécute la requête spécifiée et retourne l'ensemble des résultats de celle-ci. Elle permet, par exemple, de récupérer un ensemble be beans entités complet, comme nous l'avons montré dans l'exemple précédent. Il est également possible de spécifier certaines propriétés dans la clause SELECT afin de récupérer seulement certaines valeurs de l'entité, sous forme de tableau.
String texteRequête = "SELECT photo.genre, photo.identification FROM Photo AS photo"; Query requête = persistance.createQuery(texteRequête); List<Object[]> résultat = requête.getResultList(); ... for (Object[] listePropriétés : résultat) { System.out.println("Genre : " + (String)listePropriétés[0]); System.out.println("Identification : " + (String)listePropriétés[1]); }
Dans le cas proposé ci-dessus, la méthode getResultList() retourne une liste de tableau d'objets.
.
Cette méthode exécute la requête spécifiée et retourne un unique résultat. Si le nombre est supérieur à 1, une exception NonUniqueResultException est levée. A l'inverse, si aucun résultat n'est trouvé, une exception de type EntityNotFoundExceptoin est levée.
Cette méthode doit être utilisée si vous êtes sûr que la requête ne retourne qu'un unique résultat.
.
String texteRequête = "SELECT photo FROM Photo AS photo WHERE photo.identification='Mésange bleue'"; Query requête = persistance.createQuery(texteRequête); Photo résultat = requête.getSingleResult();
Dans cet exemple, la propriété identification est unique. De ce fait, nous sommes sûrs que la méthode getSingleResult() ne peut renvoyer plusieurs résultats.
.
Ces deux méthodes définissent respectivement le nombre maximal de résultats et l'index du premier élément à retourner.
Celles-ci permettent de limiter les résultats de la requête. En effet, si la requête tente de retourner plusieurs miliers de lignes, vous risquez de réduire rapidement les performances de votre application. De plus, nous ne travaillons généralement pas avec l'ensemble des résultats d'un seul coup, mais partie par partie.
String texteRequête = "SELECT photo FROM Photo AS photo"; Query requête = persistance.createQuery(texteRequête); requête.setMaxResults(30); requête.setFirstResults(10); List<Photo> résultat = requête.getResultList(); ... for (Photo unePhoto : résultat) { System.out.println("Identification : " + unePhoto.getIdentification()); ... }
Dans cet exemple, nous récupérons au maximum 30 résultats à partir du 10° enregistrement.
.
Ces deux méthodes assignent la valeur passée en argument respectivement au paramètre nom ou au paramètre placé à l'index index. Le nom du paramètre est défini dans la requête via :nomDuParamètre ou via ?numéro.
Les paramètres indexés sont placés via un point d'interrogation suivi d'un entier, dont la valeur commence par 0, et il peut être placé à plusieurs endroits simultanéments.
String texteRequête = "SELECT photo FROM Photo AS photo WHERE photo.id=?0";
Les paramètres nommés sont placés grâce aux deux points suivis d'un identifiant.
.
String texteRequête = "SELECT photo FROM Photo AS photo WHERE photo.id=:photoId";
Nous utilisons, bien entendu, les méthodes setParameter() de l'objet Query pour définir les paramètres à utiliser lors de l'exécution de la requête.
.
String texteRequête = "SELECT photo FROM Photo AS photo WHERE photo.id=?0"; Query requête = persistance.createQuery(texteRequête); requête = requête.setParameter(0, 1183645734090);
Pour la requête basée sur les paramètres nommés, c'est un code similaire.
.
String texteRequête = "SELECT photo FROM Photo AS photo WHERE photo.id=:photoId"; Query requête = persistance.createQuery(texteRequête); requête = requête.setParameter("photoId", 1183645734090);
Il existe également d'autre surcharges de la méthode setParameter(). En voici les détails :
public interface Query { ... public Query setParameter(String name, Date date, TemporalType type); public Query setParameter(String name, Calendar calendrier, TemporalType type); public Query setParameter(int position, Date date, TemporalType type); public Query setParameter(int position, Calendar calendrier, TemporalType type); }
Deux paramètres sont ajoutés, permettant d'y affecter une marque de temps, via Date ou Calendar, ainsi que le type de cette marque, via l'énumération TemporalType (Valeurs possibles : DATE, TIME, TIMESTAMP).
Notez que ces méthodes retourne l'objet Query sur lequel elles travaillent. Cela n'est pas anodin et permet d'appeler ces méthodes en cascade, comme le montre l'exemple suivant :
String texteRequête = "SELECT photo FROM Photo AS photo WHERE photo.id=:photoId AND photo.genre=?0"; Query requête = persistance.createQuery(texteRequête); List<Photo> résultat = requête.setParameter("photoId", 1183645734090).setParameter(0, "Animal").getResultList();
Remarque : le principal avantage d'utiliser de telles requêtes "à paramétrer" est de pouvoir créer des requêtes génériques auxquelles le développeur pourra passer en paramètre tout type d'objet.
Nous avons également la possibilité d'exécuter des requêtes UPDATE (modification) ou DELETE (suppression). Pour cela nous utilisons la méthode executeUpdate().
Celle-ci exécute la requête qui doit être de type UPDATE ou DELETE. Elle retourne le nombre d'enregistrements supprimés ou modifiés.
.
String texteRequête = "DELETE FROM Photo photo WHERE photo.identification = '%s%'"; Query requête = persistance.createQuery(texteRequête); System.out.println(requête.executeUpdate()+" enregistrement(s) supprimé(s)");
Nous supprimons, dans cet exemple, l'ensemble des photos dont l'identification contient un s puis nous affichons le nombre d'enregistrement(s) supprimé(s).
.
Cette méthode permet d'utiliser des opérations spécifiques au fournisseur de persistance (plugins). Il est possible, par exemple de spécifier un timeout personnalisé avec Hibernate
String texteRequête = "SELECT photo FROM Photo AS photo WHERE photo.identification = '%s%'"; Query requête = persistance.createQuery(texteRequête); requête.setHint("org.hibernate.timeout", 300);
Dans cet exemple, le temps maximal d'exécution d'une requête sera de 300 millisecondes.
.
Après avoir pris connaissance avec l'API Query, nous nous intéressons ici plus précisément aux requêtes que vous devez soumettre, pour obtenir le résultat souhaité, tout en respectant le langage EJB-QL. Une requête peut remplir une tâche de sélection (SELECT), modification (UPDATE) ou suppression (DELETE). Une requête EJB-QL est similaire à du SQL, avec les clauses suivantes :
Bien sûr d'autres clauses pourront être rajoutées, comme nous allons le voir, mais celles-ci sont quasi systématiquement utilisées pour récupérer des données.
.
L'opérateur "." sert à naviguer entre les propriétés et les relations des beans entités. Par exemple, pour récupérer l'ensemble des photos du genre animal, voici la quelle est la requête à soumettre :
String texteRequête = "SELECT photo FROM Photo AS photo WHERE photo.genre='animal'";
La requête la plus utilisée est sans doute SELECT, la requête de recherche. L'instruction suivante permet de récupérer l'ensemble des photos :
SELECT photo FROM Photo AS photo
Il existe de nombreuses similitudes avec le langage SQL. Toutefois, il faut bien comprendre que la différence vient du fait qu'EJB-QL est orienté objet. Dans l'exemple précédent, "Photo" correspond au schéma abstrait du bean entité Photo et "photo" correspond à un des objets (liste) qui va être récupéré. Cet objet est souvent considéré comme un alias (comme en SQL).
La clause As est optionnelle, mais cela permet de mieux comprendre la requête. Voici éventuellement, et plus simplement, ce que nous pouvons écrire :
SELECT photo FROM Photo photo
La clause SELECT détermine le type de toutes les valeurs qui vont être retournées. Ainsi, dans cet exemple, il s'agit du type Photo et nous récupérons le bean entité en entier puisque que nous spécifions l'instance de l'objet photo juste à la suite du mot SELECT.
A titre indicatif, nous pouvons également indiquer que la valeur retournée correspond bien à une entité en spécifiant explicitement qu'il s'agit bien d'un objet qui est attendu :
SELECT OBJECT(photo) FROM Photo AS photo
Les exemples précédents travaillent directement avec l'objet Photo, cependant il est parfois pratique de ne récupérer que le genre, l'identification, etc. Avec EJB 3, il est possible de spécifier les propriétés que l'on souhaite récupérer. Il suffit de les spécifier dans la clause SELECT :
SELECT photo.genre, photo.identification FROM Photo AS photo
Lorsque une requête attend plusieurs attributs pour une même entité, le résultat est intégré dans un tableau d'objet (Object[]), et si plusieurs beans sont concernés par cette recherche, c'est toujours la liste qui est utilisée (java.util.List) grâce à la méthode getResultList().
String texteRequête = "SELECT photo.genre, photo.identification FROM Photo AS photo"; Query requête = persistance.createQuery(texteRequête); List<Object[]> résultat = requête.getResultList(); ... for (Object[] listePropriétés : résultat) { System.out.println("Genre : " + (String)listePropriétés[0]); System.out.println("Identification : " + (String)listePropriétés[1]); }
De façon plus simple, nous avons quelquefois besoin de la valeur d'un seul attribut d'un bean entité. Le résultat est toujours récolté au travers de la méthode getResultList(), mais cette fois-ci, le retour est une liste du type correspondant à l'attribut souhaité. Ainsi, si je désire récupérer l'ensemble des clés primaires de mes photos stockées, qui sont en réalité, le nom des fichiers photos souvegardés, voici ce que je dois écrire :
String texteRequête = "SELECT photo.id FROM Photo AS photo"; Query requête = persistance.createQuery(texteRequête); List<Long> résultat = requête.getResultList(); ... for (Long fichier : résultat) { System.out.println("Fichier photo : " + fichier +".jpg"); }
Ces requêtes vont fournir un ensemble d'opérations pour modifier ou supprimer un bean entité. Leur particularité commune est de n'avoir qu'une seule entité dans la clause FROM. Ainsi, si vous souhaitez supprimer ou modifier des enregistrements de plusieurs beans entités, vous devez effectuer plusieurs requêtes. Voici les deux exemples montrant l'utilisation de ces requêtes :
DELETE FROM Photo AS photo WHERE photo.identification = 'Mésange bleue'
UPDATE Photo AS photo WHERE photo.id = 1183645734090
La clause DELETE d'EJB-QL ne supporte pas la suppression en cascade. En effet, même si la relation est configurée avec CascadeType.REMOVE ou CascadeType.ALL, il faudra tout de même écrire manuellement leur retrait de la base de données.
Dans cette requête DELETE, nous risquons de recevoir des exceptions dans le cas où le bean entité Photo possèderait des relations. Nous avons alors trois possibilités :
Dans ce dernier cas, il est possible d'utiliser l'expression ON DELETE CASCADE pour, par exemple, les bases de données de type MySQL version 5 et du moteur de stockage InnoBD.
EJB-QL est très utile pour tout ce qui est recherche. Par contreles requêtes DELETE et UPDATE me semble pas très utiles surtout UPDATE d'ailleurs. Le gestionnaire de persistance se comporte très bien avec les mises à jours des beans entités sans passer par EJB-QL. Toutefois, j'utilise quelquefois la requête DELETE, mais jamais UPDATE.
Les permissions incluses dans la clause WHERE existent pour la plupart dans une forme similaire à celles que nous utilisont avec SQL. Voici une liste résumant celles qui sont le plus couramment utilisées :
7 @Entity 8 public class Utilisateur implements Serializable { 9 private String nom; 10 private String prenom; 11 private String fonction; 12 private Collection<Photo> photos = new ArrayList<Photo>(); ... 24 @OneToMany 25 public Collection<Photo> getPhotos() { return photos; } 26 public void setPhotos(Collection<Photo> photos) { this.photos = photos; } 27 }j'aimerais récupérer l'ensemble des photos qu'un photographe a pris :
La majeure partie des relations utilise des collections. Le parcours de leurs entrées est particulièrement intéressante car il n'existe pas dans la logique relationnelle du SQL. Nous avons pour cela l'opérateur IN. Il doit être placé dans la clause FROM, et permet de déclarer un alias pour les entrées de la collection. Par exemple, nous pouvons récupérer l'ensemble des photos prises par les photographes :
SELECT photo FROM Utilisateur AS photographe , IN(photographe.photos) photo
Nous regroupons ainsi tous les éléments contenus dans la collection photographe.photos, dans l'objet photo. De ce fait, la requête regroupe toutes les photos de tous les photographes.
Les identifiants dans la clause FROM sont déclarés de gauche à droite. Lorsqu'un identifiant est déclaré, nous pouvons l'utiliser dans les déclarations suivantes.
.
La requête suivante permet de récupérer l'ensemble des retouches effectuées sur l'ensemble des photos (il faut imaginer, dans ce cas là, quil existe une collections de retouches pour une photo) :
SELECT retouche FROM Utilisateur AS photographe , IN(photographe.photos) photo , IN(photo.retouches) retouche
Une seconde fonction, ELEMENTS, remplit le même rôle, mais se situe dans la clause SELECT. Voici une requête avec un résultat similaire au premier exemple.
.
SELECT ELEMENTS(photographe.photos) FROM Utilisateur AS photographe
La clause DISTINCT assure que la requête proposée ne retourne aucun duplicata (des doublons ou plus). Dans l'exemple qui suit, je m'assure que dans l'ensemble des photos récupérées, je suis sûr qu'elles sont uniques :
SELECT DISTINCT photo FROM Photo AS photo ORDER BY photo.id DESC
La clause ORDER BY permet de ranger par ordre les résultats d'une requête, à partir d'un ou plusieurs attributs en utilisant l'ordre alphanumérique puis alphabétique. La requête suivante retourne l'ensemble des photos suivant l'ordre de stockage :
SELECT photo FROM Photo AS photo ORDER BY photo.id ASC
ou
SELECT photo FROM Photo AS photo ORDER BY photo.id
Le mot clé optionnel ASC signifie que le classement se fait de façon ascendante. Pour l'effectuer dans l'ordre descendante, il faut utiliser DESC (du plus grand au plus petit). Par défaut, c'est ASC qui est appliqué.
L'exemple suivant retourne les photos, avec en premier, les toutes dernières sauvegardées :
SELECT photo FROM Photo AS photo ORDER BY photo.id DESC
Il est bien entendu possible de combiner ces critères. Ainsi, la requête suivante récupère les photos de la plus petite largeur vers la plus grande et pour une largeur données, le tri se fait ensuite sur la hauteur décroissante :
SELECT photo FROM Photo AS photo ORDER BY photo.largeur , photo.hauteur DESC
EJB-QL dispose d'un certain nombre de fonctions qui permettent de réaliser des traitements sur les chaînes de caractères ou qui effectuent des opérations basiques sur les nombres.
EJB-QL possède sept fonctions sur les traitements de chaînes de caractères ainsi que trois fonctions relatives aux opérations numériques.
Les fonctions de traitement de chaîne sont les suivantes :
Les paramètres début et longueur sont des entiers (int). Vous pouvez utiliser ces fonctions avec la clause WHERE pour affiner votre recherche.
.
la recherche suivant retourne un ensemble de photos dont le genre possède plus de six caractères et dont l'identification comporte le mot mésange :
SELECT photo FROM Photo AS photo WHERE LENGTH(photo.genre) > 6 AND LOCATE(UPPER(photo.identification), 'MESANGE') > -1
Les fonctions arithmétiques dans EJB-QL doivent être appliquées sur des types primitifs ou sur leurs classes enveloppes équivalentes. Voici les trois fonctions de traitement numérique :
EJB-QL possède trois fonctions qui retourne la date courante, l'heure courante ou les deux : CURRENT_DATE, CURRENT_TIME et CURRENT_TIMESTAMP. Voici un exemple qui renvoie toutes photos du jour :
SELECT photo FROM Photo AS photo WHERE photo.instantStockage = CURRENT_DATE
Les fonctions d'agrégation servent à effectuer des opérations sur des ensembles d'éléments.
Par exemple, nous pouvons utiliser la fonction COUNT() pour déterminer le nombre de photos présentes dans le serveur :
SELECT COUNT(photo) FROM Photo AS photo
ou alors le nombre de photos correspondantes aux mésanges :
SELECT COUNT(photo) FROM Photo AS photo WHERE UPPER(photo.identification) LIKE 'MESANGE%'
Il est possible d'utiliser la clause GROUP BY pour appliquer la fonction d'agrégation sur un lot d'enregsitrements.
.
Voici un exemple permettant de retourner le nombre de photo par photographe :
SELECT photographe.nom , COUNT(photographe.photos) AS nombre FROM Utilisateur AS photographe GROUP BY photographe
L'intérêt du GROUP BY dans cette requête est de pouvoir appliquer cette fonction COUNT() sur chaque photographe.
.
La clause HAVING permet de spécifier des critères, comme avec la clause WHERE, sur une colonne générée par une des fonctions d'agrégation.
.
Voici un exemple qui retourne le nom des photographes qui possèdent plus de deux photos stockées dans le serveur :
SELECT photographe.nom , COUNT(photographe.photos) AS nombre FROM Utilisateur AS photographe GROUP BY photographe.nom HAVING nombre > 2
La clause DISTINCT peut également être avantageuse utilisé, avec des fonctions d'agrégation, afin d'éliminer toutes les duplications.
.
SELECT DISTINCT COUNT(photographe.photos) AS nombre FROM Utilisateur AS photographe GROUP BY photographe
Les jointures permettent de manipuler les relations entre les entités. Nous verrons que les fonctionnalités disponibles sont multiples et pourront s'adapter à de nombreux besoins. Pour illustrer cette déclaration, nous présenterons la récupération des retouches associées aux photos correspondantes, ceci de différentes manières. Voici d'abord les deux entités en relation :
@Entity public class Retouche implements Serializable { private long id; private int luminosite; private int contraste; private Photo photo; @Id @GeneratedValue public long getId() { return id; } public void setId(long id) { this.id = id; } @OneToOne(mappedBy="retouche")
public Photo getPhoto() { return photo; }
public void setPhoto(Photo photo) { this.photo = photo; } public int getLuminosite() { return luminosite; } public void setLuminosite(int luminosite) { this.luminosite = luminosite; } public int getContraste() { return contraste; } public void setContraste(int contraste) { this.contraste = contraste; } }
8 @Entity 9 public class Photo implements Serializable { 10 private long id; 11 private Date instantStockage; 12 private String genre; 13 private String identification; 14 private int largeur; 15 private int hauteur; 16 private long poids; 17 private Retouche retouche; 18 19 @OneToOne 20 @JoinColumn(name="RETOUCHE_ID", referencedColumnName="ID") 21 public Retouche getRetouche() { return retouche; } 22 public void setRetouche(Retouche retouche) { this.retouche = retouche; } ... 61 }
Et voici les différentes approches que nous pouvons en faire :
Les résultats retournés sont les mêmes pour chacune des requêtes à quelques exceptions près. En effet, le fonctionnement des jointures est subtil. Lorsqu'une entité Photo ne possède pas de relation avec l'entité Retouche, l'enregistrement ne sera pas retourné !
Afin de renvoyer les enregistrements de l'entité Retouche, même s'ils n'ont pas de relation, il faut effectuer une jointure dite "ouverte". Pour cela, nous utilisons les instructions LEFT OUTER JOIN ou RIGHT OUTER JOIN, également existantes en SQL.
En EJB-QL, l'expression LEFT JOIN permet d'inclure systématiquement la première entité dans le résultat de la requête. RIGHT JOIN, quand à elle, ajoute les résultats de la seconde entité.
Remarque : L'instruction OUTER, utilisée en SQL, est devenue optionnelle.
.
L'exemple suivant utilise une jointure ouverte. Les résultats sont alors différents :
SELECT photo.identification , retouche.contraste FROM Photo AS photo LEFT JOIN photo.retouche AS retouche
En effet, la requête a désormais pris en compte toutes les photos prévues même si aucune retouche n'y est associée.
Nous avons vu précédemment l'utilisation de l'opérateur IN. La jointure de type INNER provoque le même résultat. Elle est cependant plus familière aux développeurs utilisant couramment SQL. Il suffit d'utiliser le mot clé INNER JOIN pour réaliser ce type de jointure.
SELECT photo FROM Utilisateur AS photographe INNER JOIN photographe.photos photo
La requête précédente retourne l'ensemble des photos créées quel que soit l'utilisateur. Il est toutefois plus simple d'utiliser l'opérateur IN. Voici la correspondance utilisant IN :
SELECT photo FROM Utilisateur AS photographe , IN(photographe.photos) photo
EJB-QL supporte nativement le polymorphisme, c'est-à-dire les requêtes portant sur une hiérarchie d'objets.

L'exécution de la requête suivante retourne tous les enregistrement liés à l'entité Photo.
SELECT photo FROM Photo AS photo
La collection de résultat contient des objets, aussi bien de type Photo, que de type Utilisateur ou Retouche.
.
Les sous-requêtes peuvent être placées dans les clauses WHERE et HAVING. Comme leur équivalent SQL, elles permettent d'imbriquer les requêtes. L'exemple suivant sélectionne les utilisateurs ayant plus de trois photos :
SELECT photographe FROM Utilisateur AS photographe WHERE (SELECT COUNT(photo) FROM Photo As photo GROUP BY photographe) > 3
Nous remarquons l'utilisation de l'objet (l'alias) photographe de la requête principale dans la sous-requête. Un des principaux intérêt est de pouvoir se substituer aux jointures, notamment dans les requêtes de type UPDATE et DELETE.
Utilisez les sous-requêtes lorsque vous ne pouvez pas faire autrement. Nous vous conseillons d'étudier, d'abord, l'utilisation de jointures ou d'instructions telles que IN, ELEMENTS... qui sont d'ailleurs obligatoires dans des systèmes ne prenant pas en compte les sous-requêtes.
Lorsqu'une sous-requête est susceptible de retourner plusieurs lignes, il est alors possible de quantifier le résultat devant être retourné. Pour cela, nous utilisons les expressions :
FROM Photo AS photo WHERE 0 < ALL (SELECT retouche.contraste FROM photo.retouche AS retouche)
FROM Photo AS photo WHERE 0 = ANY (SELECT retouche.contraste FROM photo.retouche AS retouche)
L'opérateur EXISTS retourne true si le résultat d'une sous-requête obtient au moins une valeur. Au contraire, si aucune valeur n'est trouvée par la sous-requête, la valeur false est retournée.
FROM Photo AS photo WHERE EXIST (SELECT retouche FROM photo.retouche AS retouche WHERE retouche.contraste = 20)
Afin de justifier l'utilisation d'EJB-QL, je vous propose d'illustrer un certain nombre de requêtes au travers du projet d'archivage de photos que j'ai déjà mis en oeuvre lors de l'étude précédente. Je vais profiter de ces nouvelles compétences pour proposer dans mon application Web, d'une part une classification des photos par genre, et d'autre part une recherche de la ou des photos désirées.

Afin de se remémorer les différents éléments qui font partie de l'application d'entreprise, je vous propose l'architecture globale :

Revoici les beans entités Retouche et Photo que nous avons déjà mis en place dans l'étude précédente. La relation proposée ici entre les deux beans entités est une agrégation.
| photos.Retouche.java |
|---|
package photos; import javax.persistence.*; import java.io.Serializable; @Embeddable public class Retouche implements Serializable { private int luminosite; private int contraste; public int getLuminosite() { return luminosite; } public void setLuminosite(int luminosite) { this.luminosite = luminosite; } public int getContraste() { return contraste; } public void setContraste(int contraste) { this.contraste = contraste; } } |
| photos.Photo.java |
|---|
package photos; import java.io.Serializable; import java.text.DateFormat; import java.util.Date; import javax.persistence.*; @Entity public class Photo implements Serializable { private long id; private Date instantStockage; private String genre; private String identification; private int largeur; private int hauteur; private long poids; private Retouche retouche; @Embedded public Retouche getRetouche() { return retouche; } public void setRetouche(Retouche retouche) { this.retouche = retouche; } @Id public long getId() { return id; } public void setId(long id) { this.id = id; setInstantStockage(new Date(id)); } @Temporal(TemporalType.TIMESTAMP) public Date getInstantStockage() { return instantStockage; } public void setInstantStockage(Date instantStockage) { this.instantStockage = instantStockage; } @Column(length=15, nullable=false) public String getGenre() { return genre; } public void setGenre(String genre) { this.genre = genre; } @Column(nullable=false) public String getIdentification() { return identification; } public void setIdentification(String identification) { this.identification = identification; } public int getLargeur() { return largeur; } public void setLargeur(int largeur) { this.largeur = largeur; } public int getHauteur() { return hauteur; } public void setHauteur(int hauteur) { this.hauteur = hauteur; } public long getPoids() { return poids; } public void setPoids(long poids) { this.poids = poids; } @Transient public String getDateFormat() { return DateFormat.getDateInstance(DateFormat.FULL).format(instantStockage); } @Transient public String getNomFichier() { return id+".jpg"; } } |
Nous allons maintenant rentrer dans le vif du sujet. Effectivement, nous allons nous servir du bean session EditionPhotoBean pour implémenter les différentes requêtes EJB-QL. Nous allons modifier et rajouter des méthodes qui vont délivrer l'ensemble des renseignements nécessaires pour l'application Web.
| photos.EditionPhotoLocal.java |
|---|
.. 8 @Local 9 public interface EditionPhotoLocal { 10 List<Long> liste(); 11 BufferedImage getPhoto(String nom, int largeur) throws IOException; 12 Photo rechercher(String nom); 13 void modifier(Photo photo); 14 void supprimer(long id); 15 List<Photo> getPhotos(); 16 List<Photo> getPhotos(String genre); 17 List<String> getGenres(); 18 long nombreTotalDePhoto(); 19 List<Photo> recherche(String mot); 20 } |
| photos.EditionPhotoBean.java |
|---|
.. 11 @Stateless 12 public class EditionPhotoBean implements EditionPhotoLocal { 13 @PersistenceContext(unitName = "Photos-ejbPU") 14 EntityManager persistance; 15 private final String répertoire = "J:/Photos/"; 16 17 public List<Long> liste() { 18 String texteRequête = "SELECT photo.id FROM Photo AS photo"; 19 Query requête = persistance.createQuery(texteRequête); 20 return requête.getResultList(); 21 } 22 23 public BufferedImage getPhoto(String nom, int largeur) throws IOException { .. 45 } 46 47 public Photo rechercher(String nom) { 48 long identifiant = Long.parseLong(nom.substring(0, nom.length()-4)); 49 return persistance.find(Photo.class, identifiant); 50 } 51 52 public void modifier(Photo photo) { 53 persistance.merge(photo); 54 } 55 56 public void supprimer(long id) { 57 String texteRequête = "DELETE FROM Photo photo WHERE photo.id = :id"; 58 Query requête = persistance.createQuery(texteRequête); 59 requête.setParameter("id", id); 60 requête.executeUpdate(); 61 File fichier = new File(répertoire+id+".jpg"); 62 fichier.delete(); 63 } 64 65 public List<Photo> getPhotos() { 66 String texteRequête = "SELECT photo FROM Photo AS photo ORDER BY photo.genre, photo.identification"; 67 Query requête = persistance.createQuery(texteRequête); 68 return requête.getResultList(); 69 } 70 71 public List<Photo> getPhotos(String genre) { 72 String texteRequête = "SELECT photo FROM Photo AS photo WHERE photo.genre = :genre ORDER BY photo.identification"; 73 Query requête = persistance.createQuery(texteRequête); 74 requête.setParameter("genre", genre); 75 return requête.getResultList(); 76 } 77 78 public List<String> getGenres() { 79 String texteRequête = "SELECT DISTINCT photo.genre FROM Photo AS photo"; 80 Query requête = persistance.createQuery(texteRequête); 81 return requête.getResultList(); 82 } 83 84 public long nombreTotalDePhoto() { 85 String texteRequête = "SELECT COUNT(photo) FROM Photo AS photo"; 86 Query requête = persistance.createQuery(texteRequête); 87 return (Long)requête.getSingleResult(); 88 } 89 90 public List<Photo> recherche(String mot) { 91 String texteRequête = "SELECT photo FROM Photo AS photo WHERE UPPER(photo.identification) LIKE :mot"; 92 Query requête = persistance.createQuery(texteRequête); 93 String motif = "%"+mot+"%"; 94 requête.setParameter("mot", motif.toUpperCase()); 95 return requête.getResultList(); 96 } 97 } |
Dans les sources qui suivent, vous avez toute l'application Web. Nous voyons ainsi comment utiliser ce bean session stateless que nous venons de mettre en oeuvre. Il s'agit d'une structure classique avec la technologie JSF. Je me sers tout simplement de tout ce que nous avons appris dans les études précédentes. Il suffit de revenir sur ces études pour maîtriser les écritures proposées. Je ne ferais aucun commentaire particulier.
| faces-config.xml |
|---|
<?xml version='1.0' encoding='UTF-8'?> <faces-config version="1.2" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee ..."> <managed-bean> <managed-bean-name>photos</managed-bean-name> <managed-bean-class>bean.Photos</managed-bean-class> <managed-bean-scope>session</managed-bean-scope> </managed-bean> </faces-config> |
| bean.Photos.java |
|---|
1 package bean; 2 3 import java.util.*; 4 import javax.ejb.EJB; 5 import javax.faces.component.*; 6 import javax.faces.event.ValueChangeEvent; 7 import javax.faces.model.SelectItem; 8 import photos.*; 9 10 public class Photos { 11 private int largeurVignette = 300; 12 @EJB 13 private EditionPhotoLocal édition; 14 private UIData table; 15 private List<Photo> photos; 16 private SelectItem[] genres; 17 private String genre = "Recherche"; 18 private String recherche; 19 20 public String getRecherche() { 21 return recherche; 22 } 23 24 public void setRecherche(String recherche) { 25 this.recherche = recherche; 26 } 27 28 public void executeRecherche(ValueChangeEvent evt) { 29 recherche = (String)evt.getNewValue(); 30 } 31 32 public SelectItem[] getGenres() { 33 List<String> liste = édition.getGenres(); 34 genres = new SelectItem[liste.size()+2]; 35 genres[0] = new SelectItem("Recherche", "Recherche"); 36 genres[1] = new SelectItem("Tous", "Tous"); 37 for (int i=2; i<genres.length; i++) 38 genres[i] = new SelectItem(liste.get(i-2), liste.get(i-2)); 39 return genres; 40 } 41 42 public String getGenre() { 43 return genre; 44 } 45 46 public void setGenre(String genre) { 47 this.genre = genre; 48 } 49 50 public void changeGenre(ValueChangeEvent evt) { 51 genre = (String)evt.getNewValue(); 52 } 53 54 public UIData getTable() { 55 return table; 56 } 57 58 public void setTable(UIData table) { 59 this.table = table; 60 } 61 62 public int getLargeurVignette() { 63 return largeurVignette; 64 } 65 66 public void setLargeurVignette(int |