web-dev-qa-db-fra.com

Requêtes dynamiques dans JPA Spring Data

Je recherche une solution pour créer des requêtes de manière dynamique à l'aide de Spring Data JPA. J'ai un GameController qui a un terminal/service de service RESTful qui prend 4 paramètres facultatifs: genre, plate-forme, année, titre. L'API ne peut être transmis à aucun de ceux-ci, à tous les 4 et à toutes les combinaisons entre les deux. Si aucun paramètre n'est transmis, sa valeur par défaut est null. J'ai besoin d'une méthode dans le référentiel qui permet de créer la requête appropriée et, idéalement, d'autoriser toujours la pagination Spring Data JPA, bien que je ne sache pas si cela est possible.

J'ai trouvé cet article mais cela ne semble pas être ce dont j'ai besoin à moins que je ne sois mal compris. http://spring.io/blog/2011/04/26/advanced-spring-data-jpa-specifications-and-querydsl/

Je sais que JPA dispose d’une API Query Criteria, mais je ne sais vraiment pas comment l’implémenter.

Je me rends compte que je pourrais créer une méthode pour chaque scénario possible, mais cela semble être une très mauvaise pratique et beaucoup de code inutile. 

GameRepository:

package net.jkratz.igdb.repository;

import net.jkratz.igdb.model.Game;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface GameRepository extends JpaRepository<Game, Long> {

    @Query("select g from Game g, GamePlatformMap gpm, Platform p where g = gpm.game and gpm.platform = p and p.id = :platform")
    Page<Game> getGamesByPlatform(@Param("platform") Long platformId, Pageable pageable);

    @Query("select g from Game g where g.title like :title")
    Page<Game> getGamesByTitle(@Param("title") String title, Pageable pageable);

    @Query("select g from Game g, GameGenreMap ggm, Genre ge where g = ggm.game and ggm.genre = ge and ge.id = :genreId")
    Page<Game> getGamesByGenre(@Param("genre") Long genreId, Pageable pageable);
}
15
greyfox

Je dirais que l’utilisation de QueryDSL est une façon de faire ce que vous voulez.

Par exemple, j'ai un référentiel défini comme ci-dessous:

public interface UserRepository extends PagingAndSortingRepository<User, Long>, QueryDslPredicateExecutor<User> {

    public Page<User> findAll(Predicate predicate, Pageable p);
}

Je peux appeler cette méthode avec n'importe quelle combinaison de paramètres, comme ci-dessous:

public class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;

    @Test
    public void testFindByGender() {
        List<User> users = userRepository.findAll(QUser.user.gender.eq(Gender.M));
        Assert.assertEquals(4, users.size());

        users = userRepository.findAll(QUser.user.gender.eq(Gender.F));
        Assert.assertEquals(2, users.size());
    }

    @Test
    public void testFindByCity() {

        List<User> users = userRepository.findAll(QUser.user.address.town.eq("Edinburgh"));
        Assert.assertEquals(2, users.size());

        users = userRepository.findAll(QUser.user.address.town.eq("Stirling"));
        Assert.assertEquals(1, users.size());
    }

    @Test
    public void testFindByGenderAndCity() {
        List<User> users = userRepository.findAll(QUser.user.address.town.eq("Glasgow").and(QUser.user.gender.eq(Gender.M)));
        Assert.assertEquals(2, users.size());

        users = userRepository.findAll(QUser.user.address.town.eq("Glasgow").and(QUser.user.gender.eq(Gender.F)));
        Assert.assertEquals(1, users.size());
    }
}
13
Alan Hay

Pour ceux qui utilisent Kotlin (et Spring Data JPA), nous venons d'ouvrir une source bibliothèque DSL de spécification KPA JPA } qui vous permet de créer des requêtes dynamiques sécurisées pour un référentiel JPA.

Il utilise la variable JpaSpecificationExecutor de Spring Data (c'est-à-dire les requêtes de critères JPA), mais sans recourir à un modèle standard ou à un métamodèle généré.

Le readme a plus de détails sur son fonctionnement en interne, mais voici les exemples de code pertinents pour une introduction rapide.

import au.com.console.jpaspecificationsdsl.*   // 1. Import Kotlin magic

////
// 2. Declare JPA Entities
@Entity
data class TvShow(
    @Id
    @GeneratedValue
    val id: Int = 0,
    val name: String = "",
    val synopsis: String = "",
    val availableOnNetflix: Boolean = false,
    val releaseDate: String? = null,
    @OneToMany(cascade = arrayOf(javax.persistence.CascadeType.ALL))
    val starRatings: Set<StarRating> = emptySet())

@Entity
data class StarRating(
    @Id
    @GeneratedValue
    val id: Int = 0,
    val stars: Int = 0)


////
// 3. Declare JPA Repository with JpaSpecificationExecutor
@Repository
interface TvShowRepository : CrudRepository<TvShow, Int>, JpaSpecificationExecutor<TvShow>


////
// 4. Kotlin Properties are now usable to create fluent specifications
@Service
class MyService @Inject constructor(val tvShowRepo: TvShowRepository) {
   fun findShowsReleasedIn2010NotOnNetflix(): List<TvShow> {
     return tvShowRepo.findAll(TvShow::availableOnNetflix.isFalse() and TvShow::releaseDate.equal("2010"))
   }

   /* Fall back to spring API with some extra helpers for more complex join queries */
   fun findShowsWithComplexQuery(): List<TvShow> {
       return tvShowRepo.findAll(where { equal(it.join(TvShow::starRatings).get(StarRating::stars), 2) })
   }
}

Pour les requêtes plus complexes et dynamiques, il est recommandé de créer des fonctions qui utilisent le DSL pour rendre les requêtes plus lisibles (comme vous le feriez pour QueryDSL) et permettre leur composition dans des requêtes dynamiques complexes.

fun hasName(name: String?): Specifications<TvShow>? = name?.let {
    TvShow::name.equal(it)
}

fun availableOnNetflix(available: Boolean?): Specifications<TvShow>? = available?.let {
    TvShow::availableOnNetflix.equal(it)
}

fun hasKeywordIn(keywords: List<String>?): Specifications<TvShow>? = keywords?.let {
    or(keywords.map { hasKeyword(it) })
}

fun hasKeyword(keyword: String?): Specifications<TvShow>? = keyword?.let {
    TvShow::synopsis.like("%$keyword%")
}

Ces fonctions peuvent être combinées avec and() et or() pour des requêtes imbriquées complexes:

val shows = tvShowRepo.findAll(
        or(
                and(
                        availableOnNetflix(false),
                        hasKeywordIn(listOf("Jimmy"))
                ),
                and(
                        availableOnNetflix(true),
                        or(
                                hasKeyword("killer"),
                                hasKeyword("monster")
                        )
                )
        )
)

Ou ils peuvent être combinés avec un DTO de requête de couche de service et une fonction d'extension de mappage

/**
 * A TV show query DTO - typically used at the service layer.
 */
data class TvShowQuery(
        val name: String? = null,
        val availableOnNetflix: Boolean? = null,
        val keywords: List<String> = listOf()
)

/**
 * A single TvShowQuery is equivalent to an AND of all supplied criteria.
 * Note: any criteria that is null will be ignored (not included in the query).
 */
fun TvShowQuery.toSpecification(): Specifications<TvShow> = and(
        hasName(name),
        availableOnNetflix(availableOnNetflix),
        hasKeywordIn(keywords)
)

pour les requêtes dynamiques puissantes:

val query = TvShowQuery(availableOnNetflix = false, keywords = listOf("Rick", "Jimmy"))
val shows = tvShowRepo.findAll(query.toSpecification())

JpaSpecificationExecutor prend en charge la pagination afin que vous puissiez réaliser des requêtes dynamiques paginables, adaptées au type!

2
James Bassett

J'ai une solution pour cela. J'ai écrit un code pour étendre le printemps-data-jpa.

Je l'appelle spring-data-jpa-extra

spring-data-jpa-extra vient résoudre trois problèmes:

  1. support de requête natif dynamique comme mybatis
  2. le type de retour peut être n'importe quoi
  3. pas de code, juste sql

Tu peux l'essayer : )

1
stormning