Parlons un peu des DAO
par
, 12/08/2022 à 13h27 (2326 Affichages)
Introduction
Dernièrement, je travaillais sur un projet qui avait une dette technique importante. Le deuxième projet le plus endetté techniquement sur lequel je suis intervenu.
Ce projet a d'ailleurs eu (littéralement) ma peau.
J'avais des usines à gaz, des librairies (JSF, Hibernate) mal compris et pas utilisées de manière pertinente, SOLID pas utilisé, de l'objet divin, des rôles mal définit, de la coulé de lave...
Mais un point particulier m'a interpelé: Les gens qui avaient fait le projet ne savait pas ce qu'était une DAO (Pour Data Acces Object ou en français objet d'accès aux données https://fr.wikipedia.org/wiki/Objet_...x_donn%C3%A9es ).
En particulier, il y avait confusion entre la DAO et le template method de la DAO. La différence est fine, pourtant, elle est importante.
De fait, un controleur pouvait avoir accès au SQL ou JPQL (en résumé grossier, le SQL de Hibernate JPA) ce qui pour moi est juste une hérésie. Le contrôleur n'a pas à savoir la BDD qui est derrière.
A la décharge, j'ai fais un tour d'Internet sur le sujet et j'ai vu peu de lien qui expliquait vraiment ce qu'est une DAO.
De plus, j'ai souvent vu la confusion entre la DAO et le Template Method DAO. De fait, je me dis que ça serait bien d'apporter ma pierre à l'édifice, et de faire un topo de la question.
Ici, on va donc parler DAO. Mais d'abord, on va faire un rappel de certains principes de programmations, d'UML, de BDD. Ensuite, on implémentera (pour mon projet) des DAO, et on fera aussi au passage, des tests unitaires.
1) SOLID
Premier principe à garder en mémoire, c'est SOLID ( https://fr.wikipedia.org/wiki/SOLID_(informatique) ).
Le S (pour Single responsibility principle soit Principe de responsabilité unique) dit qu'une classe, méthode ou fonction ne doit avoir qu'une seule et unique responsabilité.
Le O (pour Open/closed principle ou Principe ouvert/fermé) dit que mon objet doit être fermé à la modification directe mais ouvert à l'extension.
Le L (pour Liskov substitution principle ou principe de substitution de Liskov https://fr.wikipedia.org/wiki/Princi...tion_de_Liskov ) est plus théorique, mais tout aussi important.
Une instance de type T peut être remplacée par une instance de type G, G étant un sous type de T, sans modifier la cohérence du programme.
Le I (pour Interface segregation principle ou Principe de ségrégation des interfaces) dit qu'il faut préférer plusieurs interfaces spécifiques pour chaque client plutôt qu'une seule interface générale.
Enfin, le D (Dependency inversion principle ou Principe d'inversion de dépendance) dit qu'il faut dépendre des abstraction, pas des implémentations.
Ce dernier principe est à la base des moteurs d'injections de dépendances comme Spring ou les EJB.
2) Masquage de l'information
https://fr.wikipedia.org/wiki/Masqua...%27information
Ce principe explique qu'il faut cacher les détails d'implémentation afin d'offrir à un utilisateur une interface simple à comprendre et à utiliser.
Il va avec le D de SOLID, vu que l'on va injecter des interfaces et non pas des classes ou des classe abstraites.
Il va avec le L, car l'implémentation est un sous type de l'interface.
C'est pour ça qu'il faut définir comme type une List (Interface Java) et non une ArrayList (Une des implémentation).
Ce principe est à mettre en parallèle avec le modèle OSI ( https://fr.wikipedia.org/wiki/Mod%C3%A8le_OSI ) dans les réseaux informatiques.
On offre sur la couche au dessus une interface, soit des méthodes. La couche au dessus ne connait pas le fonctionnement de la couche en dessous, et donc son fonctionnement.
3) Notion d'interface
L'interface est d'abord une notion d'UML (https://laurent-audibert.developpez....classes#L3-2-4). C'est les services extérieurs offert à un utilisateur.
Notez qu'en Java, c'est aussi un type.
Mais en Java, c'est la même chose qu'en UML. Une interface, c'est un service que peut utiliser un utlisateur extérieur.
De plus, implémenter une interface, c'est répondre à un contrat. C'est avoir des méthodes implémentés comme définies par le contrat.
Il va de soit qu'à cause du D de SOLID, et du point 2, un programme ne doit voir que une interface, pas son implémentation.
4)Rappel 1 de la BDD: CRUD
Dans une base de données (sous entendu ici relationnelle, mais ça peut-être No-SQL ou XML), on fait toujours la même chose.
On fait du CRUD ( https://fr.wikipedia.org/wiki/CRUD ) .
On crée en BDD (C pour create soit créer).
On lit en base de donnée (R pour read soit lire).
On met à jour en BDD (U pour update soit mettre à jour).
On efface en BDD (D pour delete soit effacer).
Le CRUD sera vital pour définir une DAO.
5)Rappel 2 pour une BDD: ACID (BDD relationnelles seulemnt)
Une BDD relationnelle a un principe immuable: elle est toujours cohérente.
C'est tout ou rien!
Effectivement, imaginez que vous faites une opération bancaire. Vous déboursé 100€. Mais là, il survient une panne. Ce qui fait que la personne à qui vous payez 100€ ne reçoit pas les 100€.
Mais vous, vous les avez débitez. Je pense que vous ne seriez pas content.
C'est pour ça qu'en BDD, c'est tout ou rien. Si il y a une panne, on revient à l'état initial, l'état stable. On dit que l'on "Rollback".
Les opérations, en BDD, se font dans ce que l'on appelle une transaction. Si tout est validé, on valide le nouvel état stable. On dit que l'on "Commit".
La transaction applique le principe ACID:
A pour atomicité. C'est tout ou rien.
C pour cohérence. Les états visibles en BDD sont toujours cohérents.
I pour isolation. Si on a deux transactions, alors elles sont indépendantes.
D pour durabilité. Une fois que la transaction est validée, c'est pour toujours.
ACID n'est pas nécessaire pour définir une DAO, mais va devenir primordiale lorsque l'on passera à l'implémentation.
6) Définition d'une DAO
On peut enfin définir une DAO.
Par exemple, ceci est une DAO:Une DAO est une interface qui pour un objet persistant, permet de faire du CRUD et d'autres opérations (recherche par nom...)
Par contre, ceci n'est pas, pour moi, une bonne DAO. C'est une classe abstraite et pire, la connexion en BDD est publique, donc visible de l'extérieure. Comme par un contrôleur par exemple...
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13 public interface Dao<T> { Optional<T> get(long id); List<T> getAll(); void save(T t); void update(T t, String[] params); void delete(T t); }
Je rappelle aussi que selon Joshua Bloch ( https://www.amazon.fr/Effective-Java.../dp/0134685997 ), il faut préférer les interfaces aux classes abstraites.
7) Conséquences de cette définition
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38 package com.developpez.dao; import java.sql.Connection; import com.developpez.jdbc.ConnectionPostgreSQL; public abstract class DAO<T> { public Connection connect = ConnectionPostgreSQL.getInstance(); /** * Permet de récupérer un objet via son ID * @param id * @return */ public abstract T find(long id); /** * Permet de créer une entrée dans la base de données * par rapport à un objet * @param obj */ public abstract T create(T obj); /** * Permet de mettre à jour les données d'une entrée dans la base * @param obj */ public abstract T update(T obj); /** * Permet la suppression d'une entrée de la base * @param obj */ public abstract void delete(T obj); } }
C'est pour un objet persistent. C'est à dire qui peut être sauvé en BDD.
On respecte le point 2. L'utilisateur ne voit que les méthodes pour persister l'objet.
Je n'ai pas précisé la BDD (qui peut être MySQL, Oracle, PostgreSQL, ou une No-SQL comme MongoDB, voir des fichiers XML).
Je n'ai pas précisé l'implémentation. Ça peut-être du JDBC pur et dur, ou un ORM (comme Hibernate JPA).
La seule chose sure, c'est que pour un utilisateur (comprendre une classe) à qui j'injecterais la classe, il pourra faire du CRUD.
Et la personne qui doit faire du CRUD n'a pas besoin de savoir comment on fait du CRUD. Il a juste besoin de pouvoir le faire.
8) Mon implémentation dans le projet
Pour commencer, je ne fait pas du CRUD avec n'importe quoi. Je le fais avec des entité (un terme JPA).
En BDD, on a un identifiant. Je définis donc un contrat que respectera toutes les entités:
A partir de là, je définis la DAO:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 package com.calculateur.warhammer.base.entity; /** * Représente une entité en BDD * @author phili * */ public interface IEntity <I>{ /** * * @return L'identifiant en BDD */ I getId(); /** * Set l'id en BDD * @param id L'id à setter */ void setId(I id); }
Je ne vais pas aborder les exceptions (c'est un sujet très complexe), mais pour moi, ça peut bien se passer, mais ça peut aussi mal se passer.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65 package com.calculateur.warhammer.base.dao; import java.util.List; import com.calculateur.warhammer.base.entity.IEntity; import com.calculateur.warhammer.base.exception.DAOException; import com.calculateur.warhammer.base.exception.FunctionnalExeption; /** * DAO pour une entité en BDD * @author phili * * @param <I> Type de l'id de l'entité * @param <E> L'entité */ public interface IDAO <I,E extends IEntity<I>>{ /** * Sauvegarde l'entité en BDD (Création) * @param entity L'entité à sauvegarder * @return L'entité à créer * @throws DAOException Si problèeme en BDD * @throws FunctionnalExeption Si problème à la vérification avant d'insérer en BDD */ E save(E entity)throws DAOException,FunctionnalExeption; /** * Met à jour l'entit en BDD * @param entity L'entité à modifier * @return L'entité modifiée * @throws DAOException Si problème en BDD * @throws FunctionnalExeption Si problème avant de vérifier l'entité */ E update(E entity)throws DAOException,FunctionnalExeption; /** * * @param id Id de l'entité * @return L'id correspondant à l'entité (null sinon) * @throws DAOException Si problème en BDD */ E getById(I id) throws DAOException; /** * * @return La liste des entité présente en BDD * @throws DAOException Si problème en BDD */ List<E> getAll() throws DAOException; /** * Efface en BDD l'entité correspondant à l'identifiant * @param id L'id de l'entité * @throws DAOException Si problème en BDD */ void delete(I id)throws DAOException; /** * * @return Le nombre d'occurence en BDD * @throws DAOException */ Long count()throws DAOException; }
C'est pour ça que je définit dans l'interface les exceptions qui sont susceptibles d'être jetées.
J'en distingue 2:
- LesDAOException qui sont jetées si il y a un problème avec la BDD (elle tombe en panne, mauvais SQL...)
- Les FunctionnalExeption qui sont jetées si l'utilisateur, lors d'une modification ou d'une création, essaye de mettre un objet non cohérent.
9) Des Tests
Bon, il faut tester. Et comme on va à peut près tester la même chose, on va utiliser le Design Patter Template Method (qui là justifie une classe abstraite) pour faire le TU.
Soit:
Notez qu'ici, je n'ai pas définit explicitement la DAO. Je sait juste que j'ai besoin d'une DAO pour faire le test (d'où la méthode getDAO()).
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97 package com.calculateur.warhammer.base.dao; import static org.junit.jupiter.api.Assertions.fail; import java.util.List; import java.util.Locale; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import com.calculateur.warhammer.base.entity.IEntity; import com.calculateur.warhammer.base.exception.FunctionnalExeption; import com.calculateur.warhammer.base.traduction.AbstractTraductionTest; public abstract class AbstractDAOTest <I, E extends IEntity<I>,DAO extends IDAO<I, E>>{ protected final String STRING_TEST = "TEST"; protected final String STRING_TEST2 = "TEST2"; protected final String EMPTY_STRING = ""; protected abstract DAO getDAO(); protected abstract List<E> getErrorsEntities(); protected abstract I getIdWithNothingInDatabase(); protected abstract E getEntityForInsertion(); protected abstract void compareEntities(E base,E aComparer); protected abstract void updateEntity(E toUpdate); protected abstract void assertUpdate(E anEntity); protected abstract void testSupplementaires(E entityTest); @Test public void testDAO() { DAO dao = getDAO(); //Test insertion avec erreur for(E entityWithError:getErrorsEntities()) { try { dao.save(entityWithError); }catch(Exception e) { Assertions.assertThat(e).isInstanceOf(FunctionnalExeption.class); verifieErreurFonctionnelle((FunctionnalExeption)e); } } try { final Long initial = dao.count(); //Sauver E entity = getEntityForInsertion(); entity = dao.save(entity); Long courant = dao.count(); Assertions.assertThat(courant - initial).isEqualTo(1L); //Get By Id E aEntity = dao.getById(entity.getId()); compareEntities(entity, aEntity); E aEntity2 = dao.getById(getIdWithNothingInDatabase()); Assertions.assertThat(aEntity2).isNull(); //Liste tout List<E> entities = dao.getAll(); Assertions.assertThat(entities).hasSize(courant.intValue()); //Test update updateEntity(entity); entity = dao.update(entity); aEntity = dao.getById(entity.getId()); assertUpdate(aEntity); //Test supplémentaire testSupplementaires(aEntity); //Test delete dao.delete(entity.getId()); courant = dao.count(); Assertions.assertThat(initial - courant).isEqualTo(0L); }catch (Exception e) { fail(e.getMessage()); } } private void verifieErreurFonctionnelle(FunctionnalExeption e) { for(Locale locale :AbstractTraductionTest.LOCALES_APPLICATION) { e.traduireLibelles(locale); Assertions.assertThat(e.getErreursTraduites().isEmpty()).isFalse(); } } protected String getToLongString(int size) { StringBuilder sb = new StringBuilder(); while(sb.length() <= size) { sb.append(STRING_TEST); } return sb.toString(); } }
Dans un premier temps, je vais essayé d'insérer des objets en erreur, donnés par List<E> getErrorsEntities().
Puis, je vais essayer de remonter un objet qui ne correspond à rien en BDD (identifiant donné par getIdWithNothingInDatabase()).
Ensuite, j’insère en BDD un objet (donné par E getEntityForInsertion()).
J'essaye de l'avoir par la méthode E getById(I id) de la DAO et je vérifie que j'ai bien le bon objet avec le bon identifiant (méthode compareEntities(E base,E aComparer)).
Je mets à jour l'objet (méthode updateEntity(E toUpdate)), puis je le récupère par identifiant, vérifiant la mise à jour (assertUpdate(E anEntity)).
Enfin, une DAO permettant de faire d'autre truc, j'ai aussi une méthode d test pour ça (testSupplementaires(E entityTest)).
10) Le Template Method DAO
Évidement, les méthodes de base de la DAO vont toujours faire la même chose.
Comme j'ai horreur de la duplication de code, on va donner une implémentation de base pour la DAO.
Pour cela, on va utiliser le Design Pattern Template Method.
Soit:
Notez que dans mon projet, j'utilise JPA. Je fais donc un Template Method avec JPA. Mais on pourrait le faire avec JDBC. Ou SAX/JAXB si on utilise une BDD avec des fichiers XML.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190 package com.calculateur.warhammer.dao.dao; import java.util.List; import javax.inject.Inject; import javax.inject.Named; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.Query; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; import com.calculateur.warhammer.base.dao.IDAO; import com.calculateur.warhammer.base.entity.IEntity; import com.calculateur.warhammer.base.exception.DAOException; import com.calculateur.warhammer.base.exception.FunctionnalExeption; import com.calculateur.warhammer.dao.verification.VerificationUtils; /** * Template Method pour les DAO avec JPA * @author phili * * @param <I> Type de l'id * @param <E> Type de l'entité */ public abstract class AbstractDAO<I,E extends IEntity<I>> implements IDAO<I, E>{ @PersistenceContext protected EntityManager em; @Inject @Named("transactionManager") protected PlatformTransactionManager transactionManager; /** * * @return La classe sous forme de String pour les Query JPQL */ protected abstract String getEntityClass(); /** * * @return Le libellé de l'entité pour les query JPQL */ protected abstract String getEntityLibelle(); /** * * @return Le resource Bundle d'erreur pour les erreurs fonctionnelles */ protected abstract String getErrorBundle(); /** * * @return L'entité sous forme de classe */ protected abstract Class<E> getClassEntity(); @Override public E save(E entity) throws DAOException, FunctionnalExeption { VerificationUtils.verifie(entity, getErrorBundle()); TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition()); try { em.persist(entity); transactionManager.commit(status); return entity; }catch (Exception e) { transactionManager.rollback(status); throw new DAOException(e); } } @Override public E update(E entity) throws DAOException, FunctionnalExeption { VerificationUtils.verifie(entity, getErrorBundle()); TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition()); try { E rEntity = em.merge(entity); transactionManager.commit(status); return rEntity; }catch (Exception e) { transactionManager.rollback(status); throw new DAOException(e); } } @Override public E getById(I id) throws DAOException { try { return em.find(getClassEntity(), id); }catch(Exception e) { throw new DAOException(e); } } @SuppressWarnings("unchecked") @Override public List<E> getAll() throws DAOException { try { Query query = em.createQuery(getJpqlAll()); return query.getResultList(); }catch(Exception e) { throw new DAOException(e); } } /** * * @return Le JPQL pour avoir toutes les entités */ private String getJpqlAll() { StringBuilder sb = new StringBuilder("SELECT "); sb.append(getEntityLibelle()); sb.append(" FROM "); sb.append(getEntityClass()); sb.append(" "); sb.append(getEntityLibelle()); return sb.toString(); } @Override public void delete(I id) throws DAOException { TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition()); try { Query queryDelete = getDeleteQuery(id); queryDelete.executeUpdate(); transactionManager.commit(status); }catch(Exception e) { transactionManager.rollback(status); throw new DAOException(e); } } /** * On laisse la possibilité de surcharger cette méthode au cas où l'id soit complexe (clé sur 2 colonnes par exemple). * @param id L'id de l'entité à supprimer en BDD. * @return La query pour effacer l'entité (le paramètrage est fait. */ protected Query getDeleteQuery(I id) { Query query = em.createQuery(getDeleteJpql()); query.setParameter("id", id); return query; } /** * On laisse la possibilité de surcharger cette méthode au cas où l'id soit complexe (clé sur 2 colonnes par exemple). * @return JPQL pour suprimer une entité */ protected String getDeleteJpql() { StringBuilder sb = new StringBuilder("DELETE "); sb.append(" FROM "); sb.append(getEntityClass()); sb.append(" "); sb.append(getEntityLibelle()); sb.append(" WHERE "); sb.append(getEntityLibelle()); sb.append(".id = : id"); return sb.toString(); } @Override public Long count() throws DAOException { try { Query query = em.createQuery(getJpqlCount()); return (Long) query.getSingleResult(); }catch (Exception e) { throw new DAOException(e); } } /** * * @return Le JPQL pour compter ce qui est en BDD. */ private String getJpqlCount() { StringBuilder sb = new StringBuilder("SELECT DISTINCT(COUNT("); sb.append(getEntityLibelle()); sb.append(")) FROM "); sb.append(getEntityClass()); sb.append(" "); sb.append(getEntityLibelle()); return sb.toString(); } }
Toute exception est catché et rejettée comme DAOException, comme spécifié dans mon contrat.
J'ai aussi injecté (via CDI/Spring) un objet pour les transactions:
On a donc le pattern suivant pour toutes transactions:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4 @Inject @Named("transactionManager") protected PlatformTransactionManager transactionManager;
Au début, on active la transaction:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10 TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition()); try { em.persist(entity); transactionManager.commit(status); return entity; }catch (Exception e) { transactionManager.rollback(status); throw new DAOException(e); }
Si tout s'est bien passé, on "Commit":
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2 TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
En cas d'accident, qui correspond à une exception, on "RollBack" par:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2 transactionManager.commit(status);
J'aurais pu utiliser @transactionnal ( https://docs.spring.io/spring-framew...sactional.html ) mais j'aime bien voir les étapes, et je n'aime pas le masquage induit par @transactionnal. De plus, je veux maitriser l'exception jetée en cas d'erreur.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2 transactionManager.rollback(status);
Comme je fais beaucoup de JPQL, j'ai besoin sous forme de String du nom de la classe (getEntityClass()) et de son éventuel libellé (getEntityLibelle()).
Pour retrouvé par identifiant, j'ai besoin du type de la classe entité (getClassEntity()).
Comme mes erreurs fonctionnelles sont traduites, j'ai aussi besoin du ResourceBundle (getErrorBundle()).
Tout ça, seule la classe fille le sait.
11) Cas 1: Les camps
Dans mon projet (qui consiste à calculer des morts à Warhammer 40K, je rappelle), j'ai des camps (Imperium, Chaos, Xenos), soit:
Je ne fais rien de spécial avec les camps, donc la DAO devient:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76 package com.calculateur.warhammer.entity.entity; import java.util.Set; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.Id; import javax.persistence.OneToMany; import javax.persistence.Table; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; import org.hibernate.validator.constraints.Length; import com.calculateur.warhammer.base.entity.IEntity; @Entity @Table(name = "CAMP") public class CampEntity implements IEntity<Integer>{ @Id @Column(name = "ID_CAMP",nullable = false) @NotNull(message = "id.null") private Integer id; @Column(name = "NOM_CAMP",length = 50,nullable = false) @NotNull(message = "nom.null") @NotEmpty(message = "nom.empty") @Length(max = 50,message = "nom.size") private String nom; @Column(name = "CLE_TRADUCTION_CAMP",length = 50,nullable = false) @NotNull(message = "cle.traduction.null") @NotEmpty(message = "cle.traduction.empty") @Length(max = 50,message = "cle.traduction.size") private String cleTraduction; @OneToMany(mappedBy = "camp",fetch = FetchType.LAZY) private Set<FactionEntity> factions; @Override public Integer getId() { return id; } @Override public void setId(Integer id) { this.id = id; } public String getNom() { return nom; } public void setNom(String nom) { this.nom = nom; } public String getCleTraduction() { return cleTraduction; } public void setCleTraduction(String cleTraduction) { this.cleTraduction = cleTraduction; } public Set<FactionEntity> getFactions() { return factions; } public void setFactions(Set<FactionEntity> factions) { this.factions = factions; } }
Pour le test, on a:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42 package com.calculateur.warhammer.dao.dao; import javax.inject.Named; import com.calculateur.warhammer.entity.entity.CampEntity; /** * DAO de CampEntity * @author phili * */ @Named("campDAO") public class CampDAO extends AbstractDAO<Integer, CampEntity>{ private static final String ENTITY_CLASS = "CampEntity"; private static final String ENTITY_LIBELLE = "campEntity"; private static final String RES = "com.calculateur.warhammer.entity.camp.camp"; @Override protected String getEntityClass() { return ENTITY_CLASS; } @Override protected String getEntityLibelle() { return ENTITY_LIBELLE; } @Override protected String getErrorBundle() { return RES; } @Override protected Class<CampEntity> getClassEntity() { return CampEntity.class; } }
J'utilise Spring et une BDD simple: H2 ( https://www.h2database.com/html/main.html ).
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89 package com.calculateur.warhammer.dao.dao; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; import javax.inject.Named; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit.jupiter.SpringExtension; import com.calculateur.warhammer.base.dao.AbstractDAOTest; import com.calculateur.warhammer.base.dao.IDAO; import com.calculateur.warhammer.dao.builder.CampEntityBuilder; import com.calculateur.warhammer.dao.configuration.ConfigurationTestBDDH2; import com.calculateur.warhammer.entity.entity.CampEntity; @ExtendWith(SpringExtension.class) @SpringBootTest(classes = {ConfigurationTestBDDH2.class}) public class CampDAOTest extends AbstractDAOTest<Integer, CampEntity, IDAO<Integer,CampEntity>>{ private static final Integer ID_INSERTION = 100; private static final Integer ID_NOT_IN_DATABASE = 101; @Inject @Named("campDAO") private IDAO<Integer, CampEntity> campDAO; @Override protected IDAO<Integer, CampEntity> getDAO() { return campDAO; } @Override protected List<CampEntity> getErrorsEntities() { List<CampEntity> entities = new ArrayList<>(); //Id entities.add(CampEntityBuilder.getInstance().addId(null).addNom(STRING_TEST).addCleTraduction(STRING_TEST).build()); //Nom entities.add(CampEntityBuilder.getInstance().addId(ID_INSERTION).addNom(null).addCleTraduction(STRING_TEST).build()); entities.add(CampEntityBuilder.getInstance().addId(ID_INSERTION).addNom(EMPTY_STRING).addCleTraduction(STRING_TEST).build()); entities.add(CampEntityBuilder.getInstance().addId(ID_INSERTION).addNom(getToLongString(50)).addCleTraduction(STRING_TEST).build()); //Clé de traduction entities.add(CampEntityBuilder.getInstance().addId(ID_INSERTION).addNom(STRING_TEST).addCleTraduction(null).build()); entities.add(CampEntityBuilder.getInstance().addId(ID_INSERTION).addNom(STRING_TEST).addCleTraduction(EMPTY_STRING).build()); entities.add(CampEntityBuilder.getInstance().addId(ID_INSERTION).addNom(STRING_TEST).addCleTraduction(getToLongString(50)).build()); return entities; } @Override protected Integer getIdWithNothingInDatabase() { return ID_NOT_IN_DATABASE; } @Override protected CampEntity getEntityForInsertion() { return CampEntityBuilder.getInstance().addId(ID_INSERTION).addNom(STRING_TEST).addCleTraduction(STRING_TEST).build(); } @Override protected void compareEntities(CampEntity base, CampEntity aComparer) { Assertions.assertThat(aComparer.getId()).isEqualTo(base.getId()); } @Override protected void updateEntity(CampEntity toUpdate) { toUpdate.setNom(STRING_TEST2); toUpdate.setCleTraduction(STRING_TEST2); } @Override protected void assertUpdate(CampEntity anEntity) { Assertions.assertThat(anEntity.getNom()).isEqualTo(STRING_TEST2); Assertions.assertThat(anEntity.getCleTraduction()).isEqualTo(STRING_TEST2); } @Override protected void testSupplementaires(CampEntity entityTest) { //Rien } }
Notez que j'ai injecté (via Spring) la BDD et que j'ai injecté l'interface. Le test voit l'interface, pas la classe. Le test ne voit donc pas l'implémentation de la DAO.
12) Cas 2: Les factions
A Warhammer 40K, j'ai aussi des factions (Space-marine, ...).
Soit:
Là, je vais avoir besoin de rechercher les camps par faction (les "autres opérations d'une DAO").
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83 package com.calculateur.warhammer.entity.entity; import javax.persistence.Column; import javax.persistence.EmbeddedId; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.ForeignKey; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.Table; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import com.calculateur.warhammer.base.entity.IEntity; import com.calculateur.warhammer.entity.pk.FactionPK; /** * Entité pour les faction en BDD * * @author phili * */ @Entity @Table(name = "FACTION") public class FactionEntity implements IEntity<FactionPK> { @EmbeddedId @NotNull(message = "faction.pk.null") private FactionPK id; @Column(name = "NOM",nullable = false,length = 50) @NotNull(message="faction.nom.null") @NotEmpty(message="faction.nom.empty") @Size(max = 50, message = "faction.nom.size") private String nom; @Column(name = "CLE_TRADUCTION",nullable = false,length = 50) @NotNull(message="faction.cle.traduction.null") @NotEmpty(message="faction.cle.traduction.empty") @Size(max = 50, message = "faction.cle.traduction.size") private String cleTraduction; @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "ID_CAMP",foreignKey = @ForeignKey(name = "FK_CAMP"),nullable = false,updatable = false,insertable = false) @NotNull(message = "faction.camp.null") private CampEntity camp; @Override public FactionPK getId() { return id; } @Override public void setId(FactionPK id) { this.id = id; } public String getNom() { return nom; } public void setNom(String nom) { this.nom = nom; } public String getCleTraduction() { return cleTraduction; } public void setCleTraduction(String cleTraduction) { this.cleTraduction = cleTraduction; } public CampEntity getCamp() { return camp; } public void setCamp(CampEntity camp) { this.camp = camp; } }
Je vais donc étendre la DAO de base:
Notez qu'ici, j'ai encore une interface, injectable dans diverses classes variées.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 package com.calculateur.warhammer.dao.contrat; import java.util.List; import com.calculateur.warhammer.base.dao.IDAO; import com.calculateur.warhammer.base.exception.DAOException; import com.calculateur.warhammer.entity.entity.FactionEntity; import com.calculateur.warhammer.entity.pk.FactionPK; /** * DAO spéciale pour les factions * @author phili * */ public interface IFactionDAO extends IDAO<FactionPK, FactionEntity>{ /** * * @param idCamp Id du camp * @return La liste des factions pour l'id du camp * @throws DAOException Si erreur lors de la recherche */ List<FactionEntity> getFactions(Integer idCamp)throws DAOException; }
On a l'implémentations suivante, aidée du Template Method:
Notez qu'ici, j'ai une clé composite, ce qui fait que je surcharge le JPQL pour supprimer. Mais ça reste transparent pour l'utilisateur.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130 package com.calculateur.warhammer.dao.dao; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; import javax.inject.Named; import javax.persistence.Query; import com.calculateur.warhammer.base.dao.IDAO; import com.calculateur.warhammer.base.exception.DAOException; import com.calculateur.warhammer.base.exception.FunctionnalExeption; import com.calculateur.warhammer.dao.contrat.IFactionDAO; import com.calculateur.warhammer.dao.verification.VerificationUtils; import com.calculateur.warhammer.entity.entity.CampEntity; import com.calculateur.warhammer.entity.entity.FactionEntity; import com.calculateur.warhammer.entity.pk.FactionPK; @Named("factionDAO") public class FactionDAO extends AbstractDAO<FactionPK, FactionEntity> implements IFactionDAO { private static final String ENTITY_CLASS = "FactionEntity"; private static final String ENTITY_LIBELLE = "factionEntity"; private static final String RES = "com.calculateur.warhammer.entity.faction.faction"; private static final String JPQL_DELETE = "DELETE FROM FactionEntity factionEntity WHERE factionEntity.id.idCamp = :idCamp AND factionEntity.id.idFaction = :idFaction"; private static final String JPQL_SELECT_BY_CAMP = "SELECT DISTINCT(factionEntity) FROM FactionEntity factionEntity WHERE factionEntity.id.idCamp = :idCamp"; @Inject @Named("campDAO") private IDAO<Integer, CampEntity> campDAO; @SuppressWarnings("unchecked") @Override public List<FactionEntity> getFactions(Integer idCamp) throws DAOException { try { Query query = em.createQuery(JPQL_SELECT_BY_CAMP); query.setParameter("idCamp", idCamp); return query.getResultList(); } catch (Exception e) { throw new DAOException(e); } } @Override protected String getEntityClass() { return ENTITY_CLASS; } @Override protected String getEntityLibelle() { return ENTITY_LIBELLE; } @Override protected String getErrorBundle() { return RES; } @Override protected Class<FactionEntity> getClassEntity() { return FactionEntity.class; } // Surcharge à cause de l'id @Override protected Query getDeleteQuery(FactionPK id) { Query query = em.createQuery(JPQL_DELETE); query.setParameter("idCamp", id.getIdCamp()); query.setParameter("idFaction", id.getIdFaction()); return query; } @Override public FactionEntity save(FactionEntity entity) throws DAOException, FunctionnalExeption { verificationSupplementaire(entity); return super.save(entity); } @Override public FactionEntity update(FactionEntity entity) throws DAOException, FunctionnalExeption { verificationSupplementaire(entity); return super.update(entity); } /** * Vérifications supplémentaires sur la clé primaire et le camp. * * @param entity Entité à vérifier * @throws FunctionnalExeption Si erreur fonctionnelle détectée * @throws DAOException Si erreur avec la BDD */ private void verificationSupplementaire(FactionEntity entity) throws FunctionnalExeption, DAOException { List<String> errors = new ArrayList<>(); // Ce ne sera pas vérifié directement, il faut ruser if (entity.getId() != null) { VerificationUtils.verifie(entity.getId(), RES); } if (entity.getId() != null && entity.getId().getIdCamp() != null && entity.getCamp() != null) { verificationCoherenceCamp(entity, entity.getId(), errors); } if (!errors.isEmpty()) { throw new FunctionnalExeption(errors, RES); } } private void verificationCoherenceCamp(FactionEntity entity, FactionPK id, List<String> errors) throws DAOException { if (entity.getCamp().getId() == null) { errors.add("faction.camp.id.null"); } else { verificationCoherenceCleComposee(entity, id, errors); } } private void verificationCoherenceCleComposee(FactionEntity entity, FactionPK id, List<String> errors) throws DAOException { if (id.getIdCamp().intValue() != entity.getCamp().getId().intValue()) { errors.add("faction.camp.id.error"); } CampEntity aCamp = campDAO.getById(entity.getCamp().getId()); if (aCamp == null) { errors.add("faction.camp.camp.null"); } } }
Le TU devient:
Bibliographie
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132 package com.calculateur.warhammer.dao.dao; import static org.junit.jupiter.api.Assertions.fail; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Named; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit.jupiter.SpringExtension; import com.calculateur.warhammer.base.dao.AbstractDAOTest; import com.calculateur.warhammer.base.dao.IDAO; import com.calculateur.warhammer.base.exception.DAOException; import com.calculateur.warhammer.dao.builder.CampEntityBuilder; import com.calculateur.warhammer.dao.builder.FactionEntityBuilder; import com.calculateur.warhammer.dao.builder.pk.FactionPKBuilder; import com.calculateur.warhammer.dao.configuration.ConfigurationTestBDDH2; import com.calculateur.warhammer.dao.contrat.IFactionDAO; import com.calculateur.warhammer.entity.entity.CampEntity; import com.calculateur.warhammer.entity.entity.FactionEntity; import com.calculateur.warhammer.entity.pk.FactionPK; @ExtendWith(SpringExtension.class) @SpringBootTest(classes = {ConfigurationTestBDDH2.class}) public class FactionDAOTest extends AbstractDAOTest<FactionPK, FactionEntity, IFactionDAO>{ private static final Integer ID_CAMP_IMPERIUM = 1; private static final Integer ID_CAMP_CHAOS = 2; private static final Integer ID_NON_UTILISE = 1000; private static final Integer ID_TEST = 8; @Inject @Named("factionDAO") private IFactionDAO factionDAO; @Inject @Named("campDAO") private IDAO<Integer, CampEntity> campDAO; private CampEntity campImperium; @BeforeEach public void beforeTest() throws DAOException{ campImperium = campDAO.getById(ID_CAMP_IMPERIUM); } @Override protected IFactionDAO getDAO() { return factionDAO; } @Override protected List<FactionEntity> getErrorsEntities() { List<FactionEntity> list = new ArrayList<>(); //Id FactionEntityBuilder builder = FactionEntityBuilder.getInstance().addNom(STRING_TEST).addCleTraduction(STRING_TEST).addCamp(campImperium); list.add(builder.addId(null).build()); list.add(builder.addId(FactionPKBuilder.getInstance().addIdCamp(null).addIdFaction(ID_TEST).build()).build()); list.add(builder.addId(FactionPKBuilder.getInstance().addIdCamp(ID_CAMP_IMPERIUM).addIdFaction(null).build()).build()); FactionPK id = FactionPKBuilder.getInstance().addIdCamp(ID_CAMP_IMPERIUM).addIdFaction(ID_TEST).build(); //Nom list.add(FactionEntityBuilder.getInstance().addId(id).addNom(null).addCleTraduction(STRING_TEST).addCamp(campImperium).build()); list.add(FactionEntityBuilder.getInstance().addId(id).addNom(EMPTY_STRING).addCleTraduction(STRING_TEST).addCamp(campImperium).build()); list.add(FactionEntityBuilder.getInstance().addId(id).addNom(getToLongString(50)).addCleTraduction(STRING_TEST).addCamp(campImperium).build()); //Cle traduction list.add(FactionEntityBuilder.getInstance().addId(id).addNom(STRING_TEST).addCleTraduction(null).addCamp(campImperium).build()); list.add(FactionEntityBuilder.getInstance().addId(id).addNom(STRING_TEST).addCleTraduction(EMPTY_STRING).addCamp(campImperium).build()); list.add(FactionEntityBuilder.getInstance().addId(id).addNom(STRING_TEST).addCleTraduction(getToLongString(50)).addCamp(campImperium).build()); //Camp list.add(FactionEntityBuilder.getInstance().addId(id).addNom(STRING_TEST).addCleTraduction(STRING_TEST).addCamp(null).build()); CampEntity aCamp = CampEntityBuilder.getInstance().addId(null).addNom(STRING_TEST).addCleTraduction(STRING_TEST).build(); list.add(FactionEntityBuilder.getInstance().addId(id).addNom(STRING_TEST).addCleTraduction(STRING_TEST).addCamp(aCamp).build()); aCamp = CampEntityBuilder.getInstance().addId(ID_CAMP_CHAOS).addNom(STRING_TEST).addCleTraduction(STRING_TEST).build(); list.add(FactionEntityBuilder.getInstance().addId(id).addNom(STRING_TEST).addCleTraduction(STRING_TEST).addCamp(aCamp).build()); id = FactionPKBuilder.getInstance().addIdCamp(ID_NON_UTILISE).addIdFaction(ID_TEST).build(); aCamp = CampEntityBuilder.getInstance().addId(ID_NON_UTILISE).addNom(STRING_TEST).addCleTraduction(STRING_TEST).build(); list.add(FactionEntityBuilder.getInstance().addId(id).addNom(STRING_TEST).addCleTraduction(STRING_TEST).addCamp(aCamp).build()); return list; } @Override protected FactionPK getIdWithNothingInDatabase() { return FactionPKBuilder.getInstance().addIdCamp(ID_CAMP_IMPERIUM).addIdFaction(ID_NON_UTILISE).build(); } @Override protected FactionEntity getEntityForInsertion() { FactionPK id = FactionPKBuilder.getInstance().addIdCamp(ID_CAMP_IMPERIUM).addIdFaction(ID_TEST).build(); return FactionEntityBuilder.getInstance().addId(id).addNom(STRING_TEST).addCleTraduction(STRING_TEST).addCamp(campImperium).build(); } @Override protected void compareEntities(FactionEntity base, FactionEntity aComparer) { Assertions.assertThat(base.getId().getIdCamp()).isEqualTo(aComparer.getId().getIdCamp()); Assertions.assertThat(base.getId().getIdFaction()).isEqualTo(aComparer.getId().getIdFaction()); } @Override protected void updateEntity(FactionEntity toUpdate) { toUpdate.setNom(STRING_TEST2); toUpdate.setCleTraduction(STRING_TEST2); } @Override protected void assertUpdate(FactionEntity anEntity) { Assertions.assertThat(anEntity.getNom()).isEqualTo(STRING_TEST2); Assertions.assertThat(anEntity.getCleTraduction()).isEqualTo(STRING_TEST2); } @Override protected void testSupplementaires(FactionEntity entityTest) { try { List<FactionEntity> list = factionDAO.getFactions(ID_CAMP_IMPERIUM); List<FactionEntity> aList = list.stream().filter(f -> f.getId().getIdFaction().equals(ID_TEST)).map(f -> f).collect(Collectors.toList()); Assertions.assertThat(aList).isNotEmpty(); Assertions.assertThat(aList).hasSize(1); } catch (DAOException e) { fail(e); } } }
https://www.baeldung.com/java-dao-pattern
https://openclassrooms.com/fr/course...menter-des-dao
https://www.web-dev-qa-db-fra.com/fr...va/1041827090/
https://www.digitalocean.com/communi...design-pattern