Динамические запросы в Spring Данные JPA
Я ищу решение для динамической сборки запросов с помощью Spring Data JPA. У меня есть GameController, у которого есть конечная точка/игры службы RESTful, которая принимает 4 необязательных параметра: жанр, платформа, год, название. API может быть передан ни один из них, все 4 и каждая комбинация между ними. Если какой-либо параметр не передан, он по умолчанию имеет значение null. Мне нужен метод в Репозитории, который построит соответствующий запрос и в идеале также позволит Spring Data JPA Paging, хотя я не уверен, что это возможно.
Я нашел эту статью, но это не похоже на то, что мне нужно, если я не понимаю. http://spring.io/blog/2011/04/26/advanced-spring-data-jpa-specifications-and-querydsl/
Я знаю, что JPA имеет 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);
}
Ответы
Ответ 1
Я бы сказал, что использование 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());
}
}
Ответ 2
Для тех, кто использует Kotlin (и Spring Data JPA), мы только что открыли Kotlin JPA Specification DSL library, которая позволяет вы создаете динамические запросы типа безопасного для репозитория JPA.
Он использует Spring Data JpaSpecificationExecutor
(т.е. запросы критериев 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")
)
)
)
)
Или они могут быть объединены с запросом DOT сервисного уровня и функцией расширения отображения
/**
* 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
поддерживает пейджинг, поэтому вы можете создавать динамические запросы с возможностью просмотра, типа и типа!
Ответ 3
У меня есть решение для этого. Я написал код для расширения spring -data-jpa.
Я называю это spring-data-jpa-extra
spring -data-jpa-extra подходит для решения трех проблем:
- поддержка динамических собственных запросов, таких как mybatis
- Тип возврата может быть любым
- нет кода, просто sql
Вы можете попробовать:)