samedi 28 mai 2011

Spring Data JPA et Querydsl

SpringSource a récemment lancé le projet Spring-Data, un ensemble de sous projets ayant pour but de nous aider à manipuler plus facilement nos données.

Nous allons nous attarder sur Spring-Data-JPA, qui fournit un ensemble d'automatisations pour JPA.


Repositories

Les repositories sont des interfaces pour lesquelles nous n'avons pas à fournir d'implémentation. Ces interfaces seront implémentées par le framework lui même au démarrage du contexte Spring.

On considère la classe Person :

@Entity
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String firstName;
    private String lastName;
    private Integer age;
    private String city;
 //getters & setters 
}

Si on écrit par exemple :

public interface PersonRepository extends JpaRepository<Person, Long> {
    List<Person> findByCity(String city);    
}

Spring implémentera une requête en se basant sur le nom de la méthode, pour rechercher des personnes par la ville où elles habitent.

Pour utiliser plusieurs paramètres on utilise le mot clé "And" :

List<Person> findByFirstNameAndLastName(String firstName, String lastName);

On peut utiliser tout un tas d'autres mots clés dans le nom des méthodes pour construire nos requêtes :

  • And
  • Or
  • GreaterThan
  • LessThan
  • Like
  • IsNull
  • OrderBy
  • ...
Par exemple :

List<Person> findByAgeOrderByLastnameDesc(Integer age);

Les repositories permettent aussi de persister les entités dans la base :

personRepository.save(person);

Ces fonctionnalités sont directement reprises depuis Hades, les 2 frameworks partageant le même spec leader.

Si on veut écrire directement une requête JPQL, on peut utiliser l'annotation @Query :

@Query("ma super requête ici")
List<Person> findWithMyCustomQuery(Customer customer);

La requête sera exécutée lorsque qu'on appellera la méthode findWithMyCustomQuery.

Named queries

Pour utiliser une NamedQuery, il suffit que le nom de la méthode du repository suive le même nom que la NamedQuery. Dans ce cas de figure, le framework n'essaiera pas de générer une requête en suivant le nom de la méthode.
Par exemple la méthode findByNameWithMyFilters sera mappée sur la requête nommée "Person.findByNameWithMyFilters".

Entité :
@Entity
@NamedQuery(name = "Person.findByNameWithMyFilters", 
  query = "select p from Person p where filter1 = ? and filter2 = ?")
public class Person {...}

Repository :
Person findByNameWithMyFilters(String filter1, String filter2);
 

Ces repositories nous facilitent bien la vie en générant pour nous les requêtes pour un grand nombre de cas courants. Un détail qui a son importance : toutes les méthodes des repositories sont transactionnelles par défaut.

Querydsl

Querydsl est une librairie qui apporte des API pour requêter des entités de manière "typesafe" et avec une syntaxe épurée. Spring-Data supporte cette librairie pour simplifier l'écriture de prédicats en Java.

Voici quelques exemples de requêtes, avec l'utilisation des mots clés eq (equals), like et gt (greater than) :

private static QPerson person = QPerson.person;

public static BooleanExpression fromCity(String city) {
 return person.city.like(city);
}

public static BooleanExpression byName(String firstName, String lastName) {
 return person.firstName.eq(firstName).and(person.lastName.eq(lastName));
}

public static BooleanExpression adults() {
 return person.age.gt(18);
}

J'aime beaucoup cette syntaxe, elle est vraiment fluide, facile à écrire et lisible. Il est bien sur possible de gérer des cas plus complexes, je vous laisse consulter la doc de Querydsl pour en savoir plus.

Pour exécuter une requête utilisant un de ces prédicats, il suffit d'écrire :

personRepository.findAll(Person.fromCity("maVille"))

ou

personRepository.findAll(Person.adults())

Pour que tout cela fonctionne, notre repository doit implémenter QuerydslPredicateExecutor. On doit également définir ce plugin dans le build maven, qui se chargera des générer les classes Q* pour chacune des entités du projet :


<plugin>
        <groupId>com.mysema.maven</groupId>
      <artifactId>maven-apt-plugin</artifactId>
      <version>1.0</version>
      <executions>
          <execution>
              <phase>generate-sources</phase>
              <goals>
                  <goal>process</goal>
              </goals>
              <configuration>
                  <outputDirectory>target/generated-sources</outputDirectory>
                  <processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
              </configuration>
          </execution>
      </executions>
  </plugin>

N.B : Il est possible d'écrire nos prédicats avec l'api criteria de JPA 2 au lieu de Querydsl si on le souhaite, en utilisant la classe org.springframework.data.jpa.domain.Specification à la place de nos BooleanExpression.

Voyons comment utiliser nos repositories et nos prédicats :

@Test
  public void testPersistence() {

      Person p = new Person();
      p.setFirstName("loic");
      p.setLastName("descotte");
      p.setCity("Grenoble");
      p.setAge(29);

      Person p2 = new Person();
      p2.setFirstName("john");
      p2.setLastName("doe");
      p2.setAge(17);
      p2.setCity("NYC");

      //save
      personRepository.save(p);
      personRepository.save(p2);

      persons = personRepository.findAll();
      assertEquals(persons.size(),2);

      persons = (List) personRepository.findAll(Person.fromCity("%Gre%"));
      assertEquals(persons.size(),1);
      assertEquals(persons.get(0).getFirstName(),"loic");

      persons = (List) personRepository.findByFirstNameAndLastName("loic", "descotte");
      assertEquals(persons.size(),1);
      assertEquals(persons.get(0).getCity(),"Grenoble");

      persons = (List) personRepository.findAll(Person.adults());
      assertEquals(persons.size(),1);
      assertEquals(persons.get(0).getFirstName(),"loic");
  }


Dans cet exemple le code client est une classe de test, mais on peut très bien appeler notre repository depuis un service REST, depuis la couche graphique, etc. (voir le code source de l'exemple en téléchargement à la fin de cet article).

On voit dans ces exemples qu'on a complètement modifié notre façon d'architecturer notre application Spring: on peut si on le souhaite se passer des couche "services" ou "business" qui contiennent habituellement la logique de notre application.
En effet Spring-Data et ses repositories ont un bon gout de DDD (Domain Driven Design). Ceux qui ont essayé Play connaissent déjà cette approche.
Le code métier de notre application peut être recentré sur le modèle (objets du domaine métier). A nous la vrai conception objet, fini les modèles anémiques sans intelligence dont les seules méthodes sont des getters et des setters.
Comme vous le voyez dans cet exemple, on a même la possibilité de placer nos prédicats d'accès à la base de données dans la classe Personne.

Pour conclure ce nouveau framework est vraiment prometteur et apporte pas mal de nouveautés et de facilités aux applications codées en environnement Spring.
Le seul bémol que j'ai vu étant (comme souvent avec Spring et Maven) le temps nécessaire pour mettre au point la configuration XML et trouver les bonnes dépendances (sans compter les conflits avec Jersey, etc.)
Mais comme je suis sympa vous pouvez tout recopier dans le code source des exemples de l'article : code source de l'exemple.

Je vous conseille de jeter également un oeil aux autres projets de Spring-Data, tout aussi intéressants et qui adressent le monde NoSQL (bases orientées graphes, documents, clé/valeur...).