Ceci est un SSCCE , montre la recherche, n'est pas une dupe et est sur le sujet !!!
Spring Boot REST et MySQL ici. J'ai l'entité Profile
suivante:
@Entity
@Table(name = "profiles")
public class Profile extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "profile_given_name")
private String givenName;
@Column(name = "profile_surname")
private String surname;
@Column(name = "profile_is_male")
private Integer isMale;
@Column(name = "profile_height_meters", columnDefinition = "DOUBLE")
private BigDecimal heightMeters;
@Column(name = "profile_weight_kilos", columnDefinition = "DOUBLE")
private BigDecimal weightKilos;
@Column(name = "profile_dob")
private Date dob;
// Getters, setters & ctor down here
}
J'ai également une ProfileController
et je souhaite exposer un point de terminaison GET qui fournit un moyen très flexible/robuste de rechercher Profiles
en fonction d'un large éventail de critères:
# Search for women between 1.2 and 1.8 meters tall.
GET /v1/profiles?isMale=0&heightMeters={"gt": 1.2, "lt": 1.8}
# Search for men born after Jan 1, 1990 who weigh less than 100 kg.
GET /v1/profiles?isMale=1&dob={"gt" : "1990-01-01 00:00:00"}&weightKilos={"lt": 100.0}
etc.
Alors voici mon contrôleur:
@RestController
@RequestMapping("/v1/profiles")
public class ProfileResource {
@Autowired
ProfileRepository profileRepository;
@GetMapping
public ResponseEntity<Set<Profile>> searchProfiles(@RequestParam(value = "isMale", required = false) String isMaleVal,
@RequestParam(value = "heightMeters", required = false) String heightMetersVal,
@RequestParam(value = "weightKilos", required = false) String weightKilosVal,
@RequestParam(value = "dob", required = false) String dobVal) {
Integer isMaleVal;
BooleanCriteria isMaleCriteria;
if(isMaleVal != null) {
// Parse the value which could either be "0" for female, "1" for male or something like
// ?isMale={0,1} to indicate
// BooleanCriteria would store which values male, female or both) to include in the search
}
BigDecimal heighMeters;
BigDecimalCriteria heightCriteria;
if(heightMetersVal != null) {
// Parse the value which like in the examples could be something like:
// ?heightMeters={"gt" : "1.0"}
// BigDecimalCriteria stores range information
}
BigDecimal heighMeters;
BigDecimalCriteria weightCriteria;
if(weightKilosVal != null) {
// Parse the value which like in the examples could be something like:
// ?weightKilos={"eq" : "100.5"}
// BigDecimalCriteria stores range information
}
// Ditto for DOB and DateCriteria
// TODO: How to pack all of these "criteria" POJOs into a
// CrudRepository/JPQL query against the "profiles" table?
Set<Profile> profiles = profileRepository.searchProfiles(
isMaleCriteria, heightCriteria, weightCriteria, dobCriteria);
}
}
Ma pensée pour, par exemple, BigDecimalCriteria
serait quelque chose comme:
// Basically it just stores the (validated) search criteria that comes in over the wire
// on the controller method
public class BigDecimalCriteria {
private BigDecimal lowerBound;
private Boolean lowerBoundInclusive;
private BigDecimal upperBound;
private Boolean upperBoundInclusive;
// Getters, setters, ctors, etc.
}
Étant donné que tous ces critères de recherche sont facultatifs (et peuvent donc être null
), je ne sais pas comment écrire la requête JPQL dans ProfileRepository
:
public interface ProfileRepository extends CrudRepository<Profile,Long> {
@Query("???")
public Set<Profile> searchProfiles();
}
Comment puis-je implémenter la @Query(...)
pour ProfileRepository#searchProfiles
de manière à ce que tous mes critères de recherche puissent être recherchés (en fonction de toutes les plages et valeurs de critères autorisées) et que tous les critères soient nuls/facultatifs?
Bien sûr, s'il y a de petites bibliothèques astucieuses ou si Spring Boot/JPA a déjà une solution à cela, je suis tout ouïe!
Vous pouvez réaliser des requêtes complexes avec des spécifications de JpaSpecificationExecutor
dans les données de printemps . L'interface de référentiel doit étendre l'interface JpaSpecificationExecutor<T>
afin que nous puissions spécifier les conditions de nos requêtes de base de données en créant de nouveaux objets Specification<T>
.
L'astuce consiste à utiliser l'interface Specification en combinaison avec une JpaSpecificationExecutor
. En voici un exemple:
@Entity
@Table(name = "person")
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(name = "name")
private String name;
@Column(name = "surname")
private String surname;
@Column(name = "city")
private String city;
@Column(name = "age")
private Integer age;
....
}
Ensuite, nous définissons notre référentiel:
public interface PersonRepository extends JpaRepository<Person, Long>, JpaSpecificationExecutor<Person> {
}
Comme vous pouvez le constater, nous avons étendu une autre interface, la JpaSpecificationExecutor
. Cette interface définit les méthodes permettant d'effectuer la recherche via une classe de spécification.
Nous devons maintenant définir notre spécification qui renverra la Predicate
contenant les contraintes de la requête (dans l'exemple, la PersonSpecification
exécute la requête, sélectionnez * à partir de la personne où name =? Ou (nom =? Et age =?) ):
public class PersonSpecification implements Specification<Person> {
private Person filter;
public PersonSpecification(Person filter) {
super();
this.filter = filter;
}
public Predicate toPredicate(Root<Person> root, CriteriaQuery<?> cq,
CriteriaBuilder cb) {
Predicate p = cb.disjunction();
if (filter.getName() != null) {
p.getExpressions()
.add(cb.equal(root.get("name"), filter.getName()));
}
if (filter.getSurname() != null && filter.getAge() != null) {
p.getExpressions().add(
cb.and(cb.equal(root.get("surname"), filter.getSurname()),
cb.equal(root.get("age"), filter.getAge())));
}
return p;
}
}
Maintenant il est temps de l'utiliser. Le fragment de code suivant montre comment utiliser la spécification que nous venons de créer:
...
Person filter = new Person();
filter.setName("Mario");
filter.setSurname("Verdi");
filter.setAge(25);
Specification<Person> spec = new PersonSpecification(filter);
List<Person> result = repository.findAll(spec);
Ici est un exemple complet présent dans github
Aussi, vous pouvez créer des requêtes complexes en utilisant Spécification
Presque ce dont vous avez besoin est déjà implémenté dans Spring Data avec l’aide des extensions Querydsl et Web support Spring Data.
Vous devez également étendre votre référentiel à partir de QuerydslPredicateExecutor
et, si vous utilisez Spring Data REST , vous pouvez interroger vos données de référentiel directement dans la boîte avec le support de filtrage, pagination et tri de base:
/profiles?isMale=0&heightMeters=1.7&sort=dob,desc&size=10&page=2
Pour implémenter des filtres plus complexes, vous devez étendre votre référentiel à partir de QuerydslBinderCustomizer
et utiliser sa méthode customize
(directement dans votre référentiel).
Par exemple, vous pouvez implémenter le filtre 'entre' pour heightMeters
et 'like' pour surname
:
public interface ProfileRepository extends JpaRepository<Profile, Long>, QuerydslPredicateExecutor<Profile>, QuerydslBinderCustomizer<QProfile> {
@Override
default void customize(QuerydslBindings bindings, QProfile profile) {
bindings.excluding( // used to exclude unnecessary fields from the filter
profile.id,
profile.version,
// ...
);
bindings.bind(profile.heightMeters).all((path, value) -> {
Iterator<? extends BigDecimal> it = value.iterator();
BigDecimal from = it.next();
if (value.size() >= 2) {
BigDecimal to = it.next();
return path.between(from, to)); // between - if you specify heightMeters two times
} else {
return path.goe(from); // or greter than - if you specify heightMeters one time
}
});
bindings.bind(profile.surname).first(StringExpression::containsIgnoreCase);
}
}
Ensuite, vous pouvez interroger vos profils:
/profiles?isMale=0&heightMeters=1.4&heightMeters=1.6&surename=doe
c'est-à-dire - trouvez toutes les femelles dont la hauteur est comprise entre 1,4 et 1,6 mètre et le nom contient «biche».
Si vous n'utilisez pas Spring Data REST, vous pouvez implémenter votre propre méthode de contrôleur de repos avec prise en charge de QueryDSL:
@RestController
@RequestMapping("/profiles")
public class ProfileController {
@Autowired private ProfileRepository profileRepo;
@GetMapping
public ResponseEntity<?> getAll(@QuerydslPredicate(root = Profile.class, bindings = ProfileRepository.class) Predicate predicate, Pageable pageable) {
Page<Profile> profiles = profileRepo.findAll(predicate, pageable);
return ResponseEntity.ok(profiles);
}
}
Remarque: n'oubliez pas d'ajouter une dépendance QueryDSL à votre projet:
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<scope>provided</scope>
</dependency>
<build>
<plugins>
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/annotations</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Puis compilez votre projet (par exemple mvn compile
) pour lui permettre de créer des classes 'Q'.
La réponse est plutôt claire et vous pouvez utiliser le requête par exemple au printemps.
et plus encore, vous n'avez pas besoin de répertorier toutes les propriétés Profile
dans votre contrôleur, vous prenez simplement la valeur Profile
comme paramètre, spring s'en chargera.
Et comme vous voulez valider les paramètres de la demande, voici comment intégrer plus facilement le validateur de beans, prenons comme exemple "GivenName". ajoutez la NotNull
dans l'entité et ajoutez @Valid
dans le contrôleur, au cas où le "nom donné" ne se trouverait pas dans les paramètres de la requête, vous obtiendrez la réponse "Requête incorrecte".
Voici les codes de travail:
@Entity
@Table(name = "profiles")
public class Profile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "profile_given_name")
@NotNull
private String givenName;
@Column(name = "profile_surname")
private String surname;
@Column(name = "profile_is_male")
private Integer isMale;
@Column(name = "profile_height_meters", columnDefinition = "DOUBLE")
private BigDecimal heightMeters;
@Column(name = "profile_weight_kilos", columnDefinition = "DOUBLE")
private BigDecimal weightKilos;
@Column(name = "profile_dob")
private Date dob;
}
ProfileResource
@RestController
@RequestMapping("/v1/profiles")
public class ProfileResource {
@Autowired
ProfileRepository profileRepository;
@GetMapping
public ResponseEntity<List<Profile>> searchProfiles(@Valid Profile profile) {
List<Profile> all = profileRepository.findAll(Example.of(profile));
return ResponseEntity.ok(all);
}
}
ProfileRepository
public interface ProfileRepository extends JpaRepository<Profile, Long> {
}
Puis envoyez la méthode HTTP GET /v1/profiles?isMale=0
comme vous le souhaitiez.
Consultez «requête par exemple» dans les données de printemps. Semble correspondre à la facture pour ce dont vous avez besoin ...
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#query-by-example