Intro
Souvent lorsque l’on parle de gérer les règles métiers, on pense à moteur de règle,
Le design pattern Specification est une solution de gestion de vos règles métiers.
Ce pattern a été formalisé par Eric Evans, père du DDD, et Martin Fowler que l’on ne présente plus.
Ce pattern est simple mais très puissant. Il permet de :
- Marquer et identifier les règles métiers
- Les centraliser
- Les réutiliser
- Remplacer les @Query compliqués ( parfois) par des méthodes de specifications permettant de gérer des relations de jointures avancées .
etc ..
Cette solution m’a permis de gérer des cas complexes de requetes de recherches et de filtres en un temps minime et d'une manière structurée prête pour être réutilisée . Je partage ici avec vous mon retour d’expérience, donc laissez vous convaincre !
Cas d'utilisation
En travaillant avec du spring Data repository , j'ai eu besoin d'avoir des requêtes flexibles basées sur certaines des propriétés d'une classe d'entité qui devraient être placées dans la requête si elles étaient présentes ou le fait de filter un resultat par des propriétées d’aurtes entitées liées directement ou indirectement avec l’entité cible . Au lieu de créer une requête pour chaque combinaison de propriétés, ou aussi utiliser des requetes JPQL compliquées , j'ai utilisé l'une des fonctionnalités fournies par Spring-Data: query by specification .
Avant de rentrer dans les détails quelques informations de fond. Ce que je vais montrer est un exemple basé sur JpaRepository utilisant Hibernate comme couche de persistence .
L'astuce est dans l'utilisation de l'interface de spécification en combinaison avec un JpaSpecificationExecutor.
Nous définissons d'abord 3 entités avec quelques propriétés:
@Entity
@Table(name = "JOUEUR")
@Getter
@Setter
@NoArgsConstructor
public class Player {
@Id
@GeneratedValue
private int id;
private String position;
private double salary;
@ManyToMany(mappedBy="players")
private Collection<Team> teams }
@Entity
@Table(name = "EQUIPE")
@Getter
@Setter
@NoArgsConstructor
public class Team {
@Id
@GeneratedValue
private int id;
private String name;
private String city;
@ManyToMany
@JoinTable(
name="PERSISTENCE_ROSTER_TEAM_PLAYER",
joinColumns=
@JoinColumn(name="TEAM_ID", referencedColumnName="ID"),
inverseJoinColumns=
@JoinColumn(name="PLAYER_ID", referencedColumnName="ID")
)
private Collection<Player> players;
@ManyToOne
private League league;}
@Entity
@Table(name = "CHAMPIONNAT")
@Getter
@Setter
@NoArgsConstructor
public class League {
@Id
@GeneratedValue
private int id;
private String name;
private String sport;
private Boolean isPro ;
@OneToMany(mappedBy="league")
private Collection<Team> teams;
}
Afin que les methodes natives de JPA supportent le pattern specification il faut que l’interface repository d’une entité X herite de l’interface JpaSpecificationExecutor
Voici un exemple pour le cas l’entité League :
@Repository
public interface LeagueRepository extends
JpaRepository<League, Integer>, JpaSpecificationExecutor<League> { }
Lors de la creation des methodes de specifications on aura besoin de metamodel des entités créées , pour les générer on a besoin d’ajouter un prosess lors de la compilation dans le pom du package contenant les entitées
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<compilerArguments>
<processor>org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor</processor>
</compilerArguments>
</configuration>
</plugin>
Par exemple on souhaite avoir une liste de championnats avec des critères specifiées , ceux qui font reference à une liste de nom d’equipes données tout en ayant la main de choisir entre championnat professionnel ou non .
static Specification<League> leaguesByTeamsNameAndLevel(Collection<String> teamsName , Boolean isPro) {
return (root, criteriaQuery, criteriaBuilder) -> {
Subquery<Leaque> leagueEntitySubquery = criteriaQuery.subquery(League.class);
Root<League> leagueEntityRoot= leagueEntitySubquery.from(League.class);
Join< League, Team> teamJoin = leagueEntityRoot.join(League_.teams);
leagueEntitySubquery.select(leagueEntityRoot)
.where(criteriaBuilder.and(teamJoin.get(Team_.name).in(teamsName),
criteriaBuilder.equal(leagueEntityRoot.get(League_.isPro) ,isPro)
criteriaBuilde.equal(leagueEntityRoot.get(League_.id), root.get(League_.id))));
return criteriaBuilder.exists(leagueEntitySubquery);
};
}
Essayons maintenant quelque chose de plus complexe , on va chercher la liste des joueurs avec un salaire entre x et y pratiquant dans une équipe ce situant dans une ville A et qui participe à un championnat nomé B
static Specification<Player> playersWithSpecs(String teamCity , String leagueName , Double minSalary ,Double maxSalary) {
return (root, criteriaQuery, criteriaBuilder) -> {
Subquery<Player> playerSubquery = criteriaQuery.subquery(Player.class);
Root<Team> teamRoot =
playerSubquery.from(Team.class);
Join< Team, Player> playerJoin = teamRoot.join(Team_.players);
Join< Team, League> leagueJoin = teamRoot.join(Team_.league);
leagueEntitySubquery.select(leagueEntityRoot)
.where(criteriaBuilder.and(
criteriaBuilder.equal(teamRoot.get(Team_.city),teamCity) ,
criteriaBuilder.equal(leagueJoin.get(League_.name),leagueName),
criteriaBuilder
.between(playerJoin.get(Player_.salary) ,minSalary ,maxSalary);
return
criteriaBuilder.exists(playerSubquery);
};
}
L'exemple des cas d'utilisation qu’on a traité nous fournit la possibilité d’appeler la methode findAll
Ainsi
List<League> championnatSpecList = championnatRepository.findAll(leagueByTeamsNameAndLevel (Arrays.asList(“real Madrid” , “FC Barcelone” , “Juventus” , “PSG”), false))
List<Player> playerSpecList = playerRepository.findAll(playersWithSpecs(“Munich” , “SuperCoupe” , 5700.45 , 9500.34))
root : permet de choisir la route à prendre pour acceder aux tables en relation avec celle qu’on souhaite traiter .
criteriaQuery : utilisé pour creer des requetes et des sous requetes en definissant
d’un table donnée .
criteriaBuilder : c’est ce qui permet de construire la requete finale à partir des specification données .
Conclusion
L'implémentation de référentiel de base prépare pour vous les critères CriteriaQuery, Root et CriteriaBuilder, applique le prédicat créé par la spécification donnée et exécute la requête.. Nous voulions être en mesure de combiner librement les spécifications atomiques pour en créer de nouvelles à la volée. Pour ce faire, nous avons une classe d'assistance Spécifications qui fournit and(...) , or(...) des méthodes pour concaténer les spécifications atomiques. Il y a aussi un where(...) qui fournit du sucre syntaxique pour rendre l'expression plus lisible.
Lorsque nous utilisons le modèle de spécifications, nous déplaçons les règles métier dans des classes de spécifications distinctes. Ces classes de spécifications peuvent être facilement combinées en utilisant des spécifications composites. En général, les spécifications améliorent la réutilisabilité et la maintenabilité. De plus, les spécifications peuvent facilement être testées à l'unité.