it-roy-ru.com

Динамические запросы в весенних данных JPA

Я ищу решение для динамического построения запросов с использованием Spring Data JPA. У меня есть GameController, который имеет конечную точку/игры RESTful, которая принимает 4 необязательных параметра: жанр, платформа, год, название. API не может быть передан ни одному из них, всем 4 и каждой комбинации между ними. Если какой-либо параметр не передан, по умолчанию он равен нулю. Мне нужен метод в репозитории, который будет создавать соответствующий запрос и в идеале также разрешать Spring Data JPA Paging, хотя я не уверен, возможно ли это.

Я нашел эту статью, но это, кажется, не то, что мне нужно, если я не понимаю. http://spring.io/blog/2011/04/26/advanced-spring-data-jpa-specifications-and-querydsl/

Я знаю, что у JPA есть API Query Criteria API, но я действительно не знаю, как это реализовать.

Я понимаю, что мог бы создать метод для каждого возможного сценария, но это кажется очень плохой практикой и большим количеством ненужного кода. 

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

Я бы сказал, что использование 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());
    }
}
13
Alan Hay

Для тех, кто использует Kotlin (и Spring Data JPA), мы просто открыли библиотеку спецификации DSL Kotlin JPA , которая позволяет создавать безопасные для типов динамические запросы для репозитория JPA.

Он использует JpaSpecificationExecutor Spring Data (то есть запросы критериев JPA), но без необходимости какого-либо шаблонного или сгенерированной метамодели.

readme содержит более подробную информацию о том, как он работает внутри, но вот соответствующие примеры кода для быстрого введения.

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
James Bassett

У меня есть решение для этого. Я написал некоторый код для расширения Spring-Data-JPA.

Я называю это spring-data-jpa-extra

spring-data-jpa-extra решает три задачи:

  1. поддержка динамических собственных запросов, таких как mybatis
  2. тип возврата может быть любым
  3. нет кода, просто sql

Можешь попробовать : )

1
stormning