|
| 1 | +La préparation [de la version 2 de RESThub](http://pullrequest.org/2011/09/07/resthub-2-preview.html) et l’objectif de remplacer [Hades](http://redmine.synyx.org/) par [Spring-data](http://www.springsource.org/spring-data) nous a emmené à étudier le module spring-data-jpa et ses capacités. |
| 2 | + |
| 3 | +## Présentation |
| 4 | + |
| 5 | +Le projet [Spring-data](http://www.springsource.org/spring-data) est un projet visant à simplifier l’utilisation des bases relationnelles et des bases NO SQL (Graph, Key-Value, Document). |
| 6 | +En plus des facilités de manipulation de données offertes par le project, Spring-data supporte le framework [QueryDsl](http://www.querydsl.com/) et ainsi la possibilité de donner [une orientation DDD](http://en.wikipedia.org/wiki/Domain-driven_design) introduit par *Eric Evans* à son travail. Sans rentrant dans les détails, on assiste peut être à la fin de nos modèles métiers anémiques ! |
| 7 | + |
| 8 | +## Cas d’utilisation basique |
| 9 | +Maintenant on rentre dans le vif du sujet avec un projet exemple montrant les possibilités offertes par Spring-data-jpa. |
| 10 | + |
| 11 | +### 1)Objet domain |
| 12 | + @Entity |
| 13 | + public class User { |
| 14 | + @Id |
| 15 | + @GeneratedValue(strategy = GenerationType.AUTO) |
| 16 | + private Long id; |
| 17 | + |
| 18 | + @Column(unique = true, nullable = false) |
| 19 | + private String username; |
| 20 | + |
| 21 | + @Column(nullable = false) |
| 22 | + private Integer age; |
| 23 | + |
| 24 | + //Get et Set |
| 25 | + } |
| 26 | + |
| 27 | +Ici, Pojo classique pour ne pas dire "anémique". Aucune référence à spring-data n'est nécessaire. |
| 28 | + |
| 29 | +### 2)Repository |
| 30 | + public interface UserRepository extends JpaRepository<User, Long> { |
| 31 | + User findByUsername(String username); |
| 32 | + |
| 33 | + List<User> findByUsernameAndAge(String username, Integer age); |
| 34 | + |
| 35 | + Page<User> findByUsernameLike(String username, Pageable pageable); |
| 36 | + |
| 37 | + @Query("SELECT u FROM User u WHERE u.username like ?1") |
| 38 | + Page<User> findByUsernameLikeCusom(String username, Pageable pageable); |
| 39 | + |
| 40 | + List<User> findByAgeBetween(Integer min, Integer max); |
| 41 | + } |
| 42 | + |
| 43 | +Le travail au niveau du repository se limite à l'écriture de l'interface et c'est Spring-data-jpa qui se charge de faire l'implémentation. Les habitués du framework [Hades](http://redmine.synyx.org/) reconnaitrons sans mal ce mode de fonctionnement. |
| 44 | +Pour les autres, plusieurs modes sont disponibles : |
| 45 | + |
| 46 | +* le framework compose automatiquement les requêtes en se basant sur des mots clés (ByXXX, Order, …) (ex : findByUsernameAndAge, ...) [liste de mots clés](http://static.springsource.org/spring-data/data-jpa/docs/1.0.0.RC1/reference/html/#repositories.query-methods.property-expressions) |
| 47 | +* l’utilisateur écrit directement la requête (utilisation de @Query) avec la posibilité d'utiliser des paramètres nommés |
| 48 | + |
| 49 | +A savoir, qu’il est possible de gérer les Pages pour les requêtes qui peuvent ramener beaucoup de résultats. |
| 50 | + |
| 51 | +### 3)Configuration Spring |
| 52 | +Il faut juste indiquer à Spring-data-jpa le package ou se trouve vos repositories qu'il doit gérer |
| 53 | + |
| 54 | + <jpa:repositoriesbase-package="fr.test.repository" /> |
| 55 | + |
| 56 | +### 4)Tests |
| 57 | +Maintenant on passe aux tests unitaires de notre "userRepository" |
| 58 | + public class UserRepositoryTest { |
| 59 | + @Autowired |
| 60 | + private UserRepository userRepositoryImpl; |
| 61 | + |
| 62 | + private User userTest1 = newUser("Test1", 16); |
| 63 | + private User userTest2 = newUser("Test2", 18); |
| 64 | + private User userTest3 = newUser("Toto", 21); |
| 65 | + private List<User> usersTest = new ArrayList<User>(Arrays.asList(userTest1, userTest2, userTest3)); |
| 66 | + |
| 67 | + @Test |
| 68 | + public void testSave() { |
| 69 | + userRepositoryImpl.save(userTest1); |
| 70 | + assertNotNull(userRepositoryImpl.findOne(userTest1.getId())); |
| 71 | + } |
| 72 | + |
| 73 | + @Test |
| 74 | + public void testFindOne() { |
| 75 | + User user = userRepositoryImpl.save(userTest1); |
| 76 | + assertNotNull(userRepositoryImpl.findOne(userTest1.getId())); |
| 77 | + |
| 78 | + user = userRepositoryImpl.findOne(userTest1.getId()); |
| 79 | + assertNotNull(user); |
| 80 | + assertEquals(user.getId(), userTest1.getId()); |
| 81 | + } |
| 82 | + //ETC ... |
| 83 | + } |
| 84 | + |
| 85 | +Rien de spécial, on injecte notre repository et on peut ensuite tester toutes les fonctions. |
| 86 | + |
| 87 | +## 1er bilan : |
| 88 | + |
| 89 | +* Avantages : |
| 90 | + |
| 91 | + * pour ceux qui connaissent Hades, on est très proche du mode de fonctionnement; |
| 92 | + * les fonctions CRUD déjà implémentées; |
| 93 | + * le mode implémentation automatique permet de gagner du temps dans les petits développements. |
| 94 | + |
| 95 | +* Limitations : |
| 96 | + |
| 97 | + * les interfaces peuvent vite devenir confuses avec des FindByXXXandYYY, FindByXXXandYYYOrderBy, ... |
| 98 | + * les requêtes persos ne sont pas vérifiées avant l’exécution (aie aux tests unitaires oubliés) |
| 99 | + |
| 100 | +## Cas d’utilisation avancé |
| 101 | +### 1)Ajouter des comportements au repository |
| 102 | + |
| 103 | + public interface UserRepositoryCustom { |
| 104 | + public boolean customMethod(User user); |
| 105 | + } |
| 106 | + |
| 107 | + @Repository("userRepositoryImpl") |
| 108 | + public class UserRepositoryCustomImpl implements UserRepositoryCustom { |
| 109 | + private static final Logger LOGGER = LoggerFactory.getLogger(UserRepositoryCustom.class); |
| 110 | + |
| 111 | + public boolean customMethod(User user){ |
| 112 | + LOGGER.info("Methode ajoutee au repository : UserRepository"); |
| 113 | + return true; |
| 114 | + } |
| 115 | + } |
| 116 | + |
| 117 | + @Repository("userRepositoryImpl") |
| 118 | + public class UserRepositoryCustomImpl implements UserRepositoryCustom { |
| 119 | + private static final Logger LOGGER = LoggerFactory.getLogger(UserRepositoryCustom.class); |
| 120 | + |
| 121 | + public boolean customMethod(User user){ |
| 122 | + LOGGER.info("Methode ajoutee au repository : UserRepository"); |
| 123 | + return true; |
| 124 | + } |
| 125 | + } |
| 126 | + |
| 127 | +Que du classique : A savoir la déclaration et l'implémentation des comportements que l'on souhaite ajouter à notre repository. Il s'agit d'un bean classique que l'on pourrait injecter dans une classe indépendamment de notre repository. |
| 128 | + |
| 129 | + public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom{ |
| 130 | + User findByUsername(String username); |
| 131 | + |
| 132 | + List<User> findByUsernameAndAge(String username, Integer age); |
| 133 | + |
| 134 | + Page<User> findByUsernameLike(String username, Pageable pageable); |
| 135 | + |
| 136 | + @Query("SELECT u FROM User u WHERE u.username like ?1") |
| 137 | + Page<User> findByUsernameLikeCusom(String username, Pageable pageable); |
| 138 | + |
| 139 | + List<User> findByAgeBetween(Integer min, Integer max); |
| 140 | + } |
| 141 | + |
| 142 | +On rajoute à notre repository "UserRepository" un extends sur notre repository UserRepositoryCustom et hop on profite des fonctionnalités de spring-data-jpa plus celles de notre implémentation de UserRepositoryCustom |
| 143 | + |
| 144 | +A savoir qu'il est possible d'ajouter des comportements "par défaut" à tous les repositories. [(cf la doc de spring-data)]( http://static.springsource.org/spring-data/data-jpa/docs/current/reference/html/#repositories.custom-behaviour-for-all-repositories). |
| 145 | + |
| 146 | + public class UserRepositoryTest { |
| 147 | + //... |
| 148 | + |
| 149 | + @Autowired |
| 150 | + private UserRepository userRepositoryImpl; |
| 151 | + |
| 152 | + @Test |
| 153 | + public void testCustomMethod() { |
| 154 | + boolean result = userRepositoryImpl.customMethod(userTest1); |
| 155 | + assertTrue(result); |
| 156 | + } |
| 157 | + //..... |
| 158 | + } |
| 159 | +Rien de particulier, on teste que notre UserRepository profite bien de la fonction définie dans notre UserRepositoryCustom. |
| 160 | + |
| 161 | +### 2)Utilisation de queryDsl |
| 162 | +QueryDsl est un framework qui permet d'écrire des requêtes type-safe dans un langage humainement compréhensible. |
| 163 | +Grâce à QueryDsl on va pouvoir supprimer une des limites énoncée dans le 1er bilan et éviter pas mal de surprise à l'éxécution. On va même discrètement rajouter un peu de métier dans autre objet domain. |
| 164 | + |
| 165 | +#### 1er étape : Génération des classes Q* |
| 166 | + |
| 167 | +Afin de pouvoir utiliser les classes QXXX (ici QUser) il faut les générer. Il existe un plugin Maven dédié à ce travail. |
| 168 | + |
| 169 | + <plugin> |
| 170 | + <groupId>com.mysema.maven</groupId> |
| 171 | + <artifactId>maven-apt-plugin</artifactId> |
| 172 | + <version>1.0.2</version> |
| 173 | + <executions> |
| 174 | + <execution> |
| 175 | + <phase>generate-sources</phase> |
| 176 | + <goals> |
| 177 | + <goal>process</goal> |
| 178 | + </goals> |
| 179 | + <configuration> |
| 180 | + <outputDirectory>target/generated-sources</outputDirectory> |
| 181 | + <processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor> |
| 182 | + </configuration> |
| 183 | + </execution> |
| 184 | + </executions> |
| 185 | + </plugin> |
| 186 | + |
| 187 | +Rem : Pour les personnes sous Eclipse il faut penser à faire un "update project configuaration". |
| 188 | + |
| 189 | +#### 2ème étape : Utilisation de QueryDsl dans les repositories |
| 190 | + |
| 191 | + public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom, QueryDslPredicateExecutor<User>{ |
| 192 | + User findByUsername(String username); |
| 193 | + |
| 194 | + List<User> findByUsernameAndAge(String username, Integer age); |
| 195 | + |
| 196 | + Page<User> findByUsernameLike(String username, Pageable pageable); |
| 197 | + |
| 198 | + @Query("SELECT u FROM User u WHERE u.username like ?1") |
| 199 | + Page<User> findByUsernameLikeCusom(String username, Pageable pageable); |
| 200 | + |
| 201 | + List<User> findByAgeBetween(Integer min, Integer max); |
| 202 | + } |
| 203 | + |
| 204 | +#### 3ème étape : On teste |
| 205 | + |
| 206 | + public class UserRepositoryTest { |
| 207 | + //... |
| 208 | + |
| 209 | + @Test |
| 210 | + public void testQueryDsl() { |
| 211 | + List<User> users = userRepositoryImpl.save(usersTest); |
| 212 | + users = userRepositoryImpl.findAll(); |
| 213 | + assertNotNull(users); |
| 214 | + assertTrue(users.size() == 3); |
| 215 | + |
| 216 | + users = (List<User>) userRepositoryImpl.findAll(QUser.user.username.like("Test%").and (QUser.user.age.eq(userTest1.getAge()))); |
| 217 | + assertNotNull(users); |
| 218 | + assertTrue(users.size() == 1); |
| 219 | + assertTrue(users.get(0).getId() == userTest1.getId()); |
| 220 | + assertTrue(users.size() == 1); assertTrue(users.get(0).getAge() < 18); |
| 221 | + } |
| 222 | + //.... |
| 223 | + } |
| 224 | + |
| 225 | +Et hop, on peut profiter de tout un langage pour générer ses requêtes type-safe! [voir la document QueryDsl]( http://source.mysema.com/static/querydsl/2.2.0/reference/html). La complétion rajoute vraiment un confort non négligeable. |
| 226 | + |
| 227 | +#### 4ème étape : Enrichissement du modèle avec les prédicats |
| 228 | + |
| 229 | + @Entity |
| 230 | + public class User { |
| 231 | + @Id |
| 232 | + @GeneratedValue(strategy = GenerationType.AUTO) |
| 233 | + private Long id; |
| 234 | + |
| 235 | + @Column(unique = true, nullable = false) |
| 236 | + private String username; |
| 237 | + |
| 238 | + @Column(nullable = false) |
| 239 | + private Integer age; |
| 240 | + |
| 241 | + public static BooleanExpression isMinor() { |
| 242 | + return QUser.user.age.lt(18); |
| 243 | + } |
| 244 | + //GET et SET |
| 245 | + } |
| 246 | + |
| 247 | +Ici, on voit le côté DDD et l'ajout de métier dans le modèle. |
| 248 | + |
| 249 | +#### 5ème étape : On teste les prédicats |
| 250 | + |
| 251 | + public class UserRepositoryTest { |
| 252 | + //... |
| 253 | + @Test |
| 254 | + public void testQueryDsl2() { |
| 255 | + List<User> users = userRepositoryImpl.save(usersTest); |
| 256 | + users = userRepositoryImpl.findAll(); |
| 257 | + assertNotNull(users); |
| 258 | + assertTrue(users.size() == 3); |
| 259 | + |
| 260 | + users = (List<User>) userRepositoryImpl.findAll(User.isMinor()); |
| 261 | + assertNotNull(users); |
| 262 | + assertTrue(users.size() == 1); |
| 263 | + assertTrue(users.get(0).getAge() < 18); |
| 264 | + } |
| 265 | + //... |
| 266 | + } |
| 267 | + |
| 268 | +On peut maintenant utiliser les prédicats prédéfinis pour générer des requêtes. |
| 269 | + |
| 270 | +## 2ème bilan : |
| 271 | +* On retrouve biens les concepts d'Hades et la possibilité d'étendre les repositories afin de rajouter des comportements. |
| 272 | +* L'utilisation du QueryDsl est vraiment intéressante. On peut fabriquer des requêtes type-safe et dans un langue proche de langage courant et on profite de la complétion!. On évite aussi de rajouter toutes les 5secondes une nouvelle méthode dans le repostitory (cela évite d'avoir plusieurs dizaines findByXXXandYYY, ...). |
0 commit comments