web-dev-qa-db-fra.com

Spring Boot & JPA: Implémentation de requêtes de recherche avec des critères facultatifs et à distance

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!

5
smeeb

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

12
Bhushan Uniyal

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'.

4
Cepr0

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.

3
Liping Huang

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

0
LetsBeFrank