Spring Data JPA 中的动态查询

问题描述 投票:0回答:4

我正在寻找一种使用 Spring Data JPA 动态构建查询的解决方案。我有一个 GameController,它有一个 RESTful 服务端点 /games,它有 4 个可选参数:流派、平台、年份、标题。 API 可以不传递任何一个,也可以传递全部 4 个,以及中间的每个组合。如果未传递任何参数,则默认为 null。我需要存储库中的一个方法来构建适当的查询,并且理想情况下仍然允许 Spring Data JPA 分页,尽管我不确定这是否可能。

我找到了这篇文章,但这似乎不是我需要的,除非我误解了。 http://spring.io/blog/2011/04/26/advanced-spring-data-jpa-specifications-and-querydsl/

我知道 JPA 有一个查询条件 API,但真的不知道如何实现它。

我意识到我可以为每种可能的情况创建一个方法,但这似乎是非常糟糕的做法并且有很多不必要的代码。

游戏库:

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);
}
spring hibernate jpa spring-data spring-data-jpa
4个回答
13
投票

我想说使用 QueryDSL 是做你想做的事情的一种方式。

例如,我有一个定义如下的存储库:

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

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

我可以使用任意参数组合来调用此方法,如下所示:

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());
    }
}

4
投票

对于那些使用 Kotlin(和 Spring Data JPA)的人,我们刚刚开源了一个 Kotlin JPA 规范 DSL 库,它可以让您为 JPA 存储库创建类型安全的动态查询。

它使用 Spring Data 的

JpaSpecificationExecutor
(即 JPA 标准查询),但不需要任何样板或生成的元模型。

自述文件提供了有关其内部工作原理的更多详细信息,但这里有相关代码示例以供快速介绍。

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) })
   }
}

对于更复杂和动态的查询,最好创建使用 DSL 的函数来使查询更具可读性(就像对 QueryDSL 所做的那样),并允许在复杂的动态查询中组合它们。

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%")
}

这些函数可以与

and()
or()
结合使用以实现复杂的嵌套查询:

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

或者可以与服务层查询DTO和映射扩展功能结合

/**
 * 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)
)

强大的动态查询:

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

JpaSpecificationExecutor
支持分页,因此可以实现可分页、类型安全、动态查询!


2
投票

我对此有一个解决方案。我编写了一些代码来扩展 spring-data-jpa 。

我称之为spring-data-jpa-extra

spring-data-jpa-extra 来解决三个问题:

  1. 像mybatis一样动态原生查询支持
  2. 返回类型可以是任何类型
  3. 没有代码,只有sql

你可以尝试一下:)


0
投票

对于具有多个参数的 SELECT 的情况,您可以尝试此 QueryDSL 解决方法。

您的存储库:

public interface GameRepository extends JpaRepository<Game, Long>, QueryDslPredicateExecutor<Game> {
}

服务层中的 SELECT 方法:

public List<Game> selectGames(String title, Long platformId, Long genreId, Integer year) {
    BooleanExpression predicate = Expressions.TRUE;
    if (title != null) {
        predicate = predicate.and(QGame.game.title.like(title));
    }
    if (platformId != null) {
        predicate = predicate.and(QGame.game.platformId.eq(platformId));
    }
    if (genreId != null) {
        predicate = predicate.and(QGame.game.genreId.eq(genreId));
    }
    if (year != null) {
        predicate = predicate.and(QGame.game.year.eq(year));
    }
    return (List<Game>) gameRepository.findAll(predicate);
}

这会生成类似 MyBatis Dynamic SQL 的查询。

您可以将控制器端点设置为:

@GetMapping
List<Room> selectRoom(@RequestParam(required = false) String title,
                      @RequestParam(required = false) Long platformId,
                      @RequestParam(required = false) Long genreId,
                      @RequestParam(required = false) Integer year) {
    return gameService.selectGames(title, platformId, genreId, year);
}

现在您可以使用任何参数组合发出 GET 请求。

© www.soinside.com 2019 - 2024. All rights reserved.