Spring Data Elasticsearch Class Cast exception on Named Query - java

I'm getting the following exception when trying to use a named query with Spring Data Elasticsearch.
ClassCastException: org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl cannot be cast to org.springframework.data.elasticsearch.core.SearchPage
The query I'm trying to make is:
public interface PlayerRepository extends ElasticsearchRepository<PlayerEntity, String> {
#Query("{\"bool\":{\"must\":[{\"terms\":{\"playerNumber.keyword\": ?0}}]}}")
SearchPage<PlayerEntity> fetchPlayers(JSONArray playerNumbers, Pageable pageable);
}
If I do not use the #Query annotation and instead let Spring derive the query from the method name like so:
SearchPage<PlayerEntity> findPlayerEntityByPlayerNumberIn(List<String> playerNumbers, Pageable pageable);
It works as expected. However, the PlayerNumber field is a #MultiField that supports the the field types of Text and Keyword like so:
#Document(indexName = "#{#playersIndexName}")
public class PlayerEntity {
#MultiField(
mainField = #Field(type = Text, name = "playerNumber"),
otherFields = {#InnerField(suffix = "keyword", type = Keyword)})
private String playerNumber;
...
}
And I need to use the keyword mapping here for the query and not the text mapping. As far as I can tell, Spring Data Elasticsearch cannot derive queries from method names on InnerField, which is why I went with the named query approach. But it seems like the using the declared query approach, detailed here, only supports a subset of return types as detailed here
In addition, I need to use the SearchPage return type as well, because there is metadata there that I need to make decisions on.
So I guess there are a couple of questions that come out of this:
Is it possible to use InnerFields in derived query methods? i.e. something like SearchPage<PlayerEntity> findPlayerEntityByPlayerNumber_KeywordIn(List<String> playerNumbers, Pageable pageable);
Is it possible for a named query to return a SearchPage? I think this might be possible with a custom Repository implementation, but if I could get either approach above to work that would be ideal.
Thanks for any help!!
spring-data-elasticsearch version: 4.0.3.RELEASE
spring-boot-starter-parent version: 2.3.3.RELEASE
elasticsearch version: 7.11.1

To answer your second question (Is it possible for a named query to return a SearchPage?): This is a bug that it does not work with #Query annotated methods. I fixed that yesterday for the main, 4.2.x, 4.1.x and 4.0.x branches so it will work when the next service releases are out.
To answer the first one, I will need to do some research and tests before I can say anything about that - it would be great if it would work. I think I' can give more information later this weekend.
Edit/Addition:
The query derivation from the method name is based on the properties of the Java class and is done in the Spring Data base which knows nothing about these inner fields that only exist in Elasticsearch.
But you can use the following custom repository fragment:
public interface CustomPlayerRepository {
SearchPage<PlayerEntity> findPlayerEntityByPlayerNumberKeywordIn(List<String> playerNumbers, Pageable pageable);
}
public class CustomPlayerRepositoryImpl implements CustomPlayerRepository {
private final ElasticsearchOperations operations;
public CustomPlayerRepositoryImpl(ElasticsearchOperations operations) {
this.operations = operations;
}
#Override
public SearchPage<PlayerEntity> findPlayerEntityByPlayerNumberKeywordIn(
List<String> playerNumbers, Pageable pageable) {
var criteriaQuery = new CriteriaQuery(new Criteria("playerNumber.keyword").in(playerNumbers), pageable);
var searchHits = operations.search(criteriaQuery, PlayerEntity.class);
return SearchHitSupport.searchPageFor(searchHits, pageable);
}
}

Related

How to query for all relational objects of an entity in Spring Boot with Hibernate?

I have an entity with the following relationship in my Store entity:
#OneToMany(mappedBy="computer")
#JsonProperty("computers")
public Set<Computer> computers;
In my StoreService:
List<Computers> computers;
Pageable paging = PageRequest.of(page, size);
Page<Computers> pageTuts;
pageTuts = storeRepository.findAllComputers(paging);
computers = pageTuts.getContent();
Map<String, Object> response = new HashMap<>();
response.put("computers", computers);
response.put("current_page", pageTuts.getNumber());
response.put("total_items", pageTuts.getTotalElements());
response.put("total_pages", pageTuts.getTotalPages());
Now I need to somehow in my StoreRepository interface query for all computers that are relational to that entity. How can I do that? I thought I could just add a name like this:
Page<Computers> findAllComputers(Pageable pageable);
Any ideas how to solve this? Do I have to write a custom Query or something? I think this operation should be some kind of standard so it's hard to think that I would need that.
You should create ComputerRepository and add this kind of method to your repository class.
public interface ComputerRepository extends PagingAndSortingRepository<Computer, Integer> {
Page<Computers> findByStore(Store store, Pageable pageable);
}
Here is quick explanation of Pagination and Sorting.
https://www.baeldung.com/spring-data-jpa-pagination-sorting

Return custom collection from Spring Data JPA query method

I would like my custom query method to return a specific Guava collection type (ImmutableSet in my example).
Example:
public interface MyRepository extends CrudRepository<User,UserId> {
#Query("SELECT DISTINCT u FROM User u WHERE... ORDER BY ...")
ImmutableSet<User> findByxxxx();
}
When I try it, I get :
Failed to convert from type [java.util.ArrayList<?>] to type [com.google.common.collect.ImmutableSet<?>]
...
Caused by: java.lang.IllegalArgumentException: Could not instantiate Collection type: com.google.common.collect.ImmutableSet
The documentation does not explicitly list the Guava collection types, so I am not sure if it is impossible without a new release, or if there is some configuration that is possible to make it work.
How can I instruct Spring Data JPA to use that type?
Spring data doesn't support collections from any third-party libraries and probably never do...
CrudRepository exposes an Iterable to you and if you want to materialize to custom collection it's up to you to do that.
By the way If you want your repository returns custom collection types you can decorate the spring data repository with yours. Like this
interface JpaUserRepository extends CrudRepository<User,UserId> {
#Query("SELECT DISTINCT u FROM User u WHERE... ORDER BY ...")
Iterable<User> findByxxxx();
}
public class MyRepository {
private final JpaUserRepository jpaUserRepository;
#Autowired
public MyRepository(final JpaUserRepository jpaUserRepository) {
this.jpaUserRepository = jpaUserRepository;
}
public ImmutableSet<User> findByxxxx() {
return ImmutableSet.copyOf(jpaUserRepository.findByXXX());
}
}

Spring Data JPA - Creating custom query method generator

In Spring Data JPA we can define a repository interface extending Repository and write a custom method.
If this method follows special syntax, Spring Data will generate the method body automatically.
For example (from the documentation):
interface PersonRepository extends Repository<Person, Long> {
List<Person> findByLastname(String lastname);
}
Is there a way to customize the method generation code to introduce new keywords into the syntax?
For example:
Person findExactlyOneById(Long id);
This method would either return the entity or throw a custom exception.
I know I can customize specific repositories as well as the base repository and achieve the effect from the above example, but I'm specifically asking for the automatic method of body generation.
Is there an extension point designed in the framework? Or is the only option to change the source code?
In your case, you can always use CrudRepository.findById(Long id) or JpaRepository.getOne(Long id).
I would suggest inheriting from the JpaRepository class because all types of repositories are included.
You can set nativeQuery = true in the #Query annotation from a Repository class like this:
public static final String FIND_PROJECTS = "SELECT projectId, projectName FROM projects";
#Query(value = FIND_PROJECTS, nativeQuery = true)
public List<Object[]> findProjects();
It's probably worth looking at the Spring data docs as well.
Some more example
1.
public interface UserRepository extends JpaRepository<User, Long> {
#Query(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1", nativeQuery = true)
User findByEmailAddress(String emailAddress);
}

Spring Pageable does not translate #Column name

I have Entity object :
#Entity(name = "table")
public class SomeEntity {
#Id
#Column(name = "id_column_name")
public final BigDecimal entityId;
#Column(name = "table_column_name")
public final String entityFieldName;
}
And I have database view defined like this:
CREATE OR REPLACE FORCE EDITIONABLE VIEW "V_TABLE" ("ID_COLUMN_NAME", "TABLE_COLUMN_NAME", "SOME_OTHER_COLUMN") AS ... (some SQL magic)
And I have repository with custom query:
#RepositoryRestResource
interface SomeEntityRepository extends PagingAndSortingRepository<SomeEntity, BigDecimal> {
#Query(value = "select id_column_name, table_column_name FROM V_TABLE where some_other_column = ?#{#parameter} order by ?#{#pageable}",
countQuery = "SELECT count(*) from V_TABLE v where some_other_column = ?#{#parameter}",
nativeQuery = true)
Page<SomeEntity> findBySomeParameter(#Param("parameter") long parameter, Pageable pageable);
}
Everything works fine when I request standard data with url:
http://localhost:8080/someEntity/search/findBySomeParameter?parameter=25&page=0&size=20
But when I add sorting information it doesn't work:
http://localhost:8080/someEntity/search/findBySomeParameter?parameter=25&page=0&size=20&sort=entityFieldName,asc
will throw following exception (I'm using Oracle database):
Caused by: java.sql.SQLSyntaxErrorException: ORA-00904: "ENTITYFIELDNAME": invalid identifier
It seems like sorting field are not translated with #Column(name), but are inlined into SQL query.
Is there any way to make pageable sort translated, so that it will use not field name but column name?
This article sheds light on the issue. Read from section 3.1 on.
Apparently dynamic sorting is not supported for native queries. Actually, if you change your findBySomeParameter method to take a Sort instead of a Pageable you will get org.springframework.data.jpa.repository.query.InvalidJpaQueryMethodException: Cannot use native queries with dynamic sorting.
Using pageable you don't get the exception, and pagination actually seems to work fine, but dynamic sorting does not substitute the column name as you found. Looks to me like the only solution is to use JPQL instead of native query, which is not a problem as long as the query you need to make is the one you provide. You would need to map the view though to a SomeEntityView class in order to use JPQL.
EDIT
I thought the issue was not documented but it actually is here in the official doc
Spring Data JPA does not currently support dynamic sorting for native queries, because it would have to manipulate the actual query declared, which it cannot do reliably for native SQL. You can, however, use native queries for pagination by specifying the count query yourself, as shown in the following example:
This workaround works for me in SpringBoot 2.4.3:
#PersistenceContext
private EntityManager entityManager;
// an object ptoperty name to a column name adapter
private Pageable adaptSortColumnNames(Pageable pageable) {
if (pageable.getSort().isSorted()) {
SessionFactory sessionFactory;
if (entityManager == null || (sessionFactory = entityManager.getEntityManagerFactory().unwrap(SessionFactory.class)) == null)
return pageable;
AbstractEntityPersister persister = (AbstractEntityPersister) ((MetamodelImplementor) sessionFactory.getMetamodel()).entityPersister(CommentEntity.class);
Sort adaptedSort = pageable.getSort().get().limit(1).map(order -> {
String propertyName = order.getProperty();
String columnName = persister.getPropertyColumnNames(propertyName)[0];
return Sort.by(order.getDirection(), columnName);
}).findFirst().get();
return PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), adaptedSort);
}
return pageable;
}
#GetMapping()
public ResponseEntity<PagedResponse<CommentResponse>> findByTextContainingFts(#RequestParam(value = "text", required = false) String text, Pageable pageable) {
// apply this adapter in controller
pageable = adaptSortColumnNames(pageable);
Page<CommentEntity> page = commentRepository.find(text, pageable);
return ResponseEntity.ok().body(domainMapper.fromPageToPagedResponse(page));
}

How do I filter data in a restful way using Spring?

As the title says.
I basically would love to do requests like
/api/todos/?completed=eq.true&created_at=lt.1486462109399
Is there any ready spring way of achieving such? Something akin to the Page/Pageable mechanism would be great.
If there is none I think I could implement it using Hibernate Criteria Queries & Argument Re-solvers. Basically allowing me to write my controllers like
#GetMapping
public ResponseEntity<Page<TodoDTO>> listAll(Criteria criteria, Pageable pageable)
{
Page<Todo> todos = todoService.listAll(criteria, pageable)
...
}
A custom Argument resolver would be responsible for turning the query string into a Criteria. Not quite sure yet how I would handle it within the service but that's the direction in which I would try to implement this.
Would that be a good approach? Any recommendations? (All assuming there are no ready mechanism for such already).
Your help is much appreciated.
Another option to build a fluent query API is to use a RSQL parser. RSQL is a query language for parametrized filtering of entries in RESTful APIs. Follow this article and your API would be able to handle URLs like:
http://localhost:8080/users?search=firstName==jo*;age<25
Sample controller:
#RestController
#RequestMapping(value = "/users")
public class UserController {
#Autowired
private UserRepository repo;
#GetMapping
public List<User> findAllByRsql(#RequestParam(value = "search") String search) {
Node rootNode = new RSQLParser().parse(search);
Specification<User> spec = rootNode.accept(new CustomRsqlVisitor<User>());
return repo.findAll(spec);
}
}
Gradle dependency:
// https://mvnrepository.com/artifact/cz.jirutka.rsql/rsql-parser
implementation("cz.jirutka.rsql:rsql-parser:2.1.0")
Recommendations from the library author:
I recommend to use separate URI query parameter for sorting, e.g. ?query=name==Flynn&sortBy=name, not to mix it with RSQL expression. The same applies to paging; I recommend URI parameters named limit and offset.
You can build a Search/Filter REST API using Spring Data JPA and Specifications.
Here is a test URL example that the resulting API would be able to handle:
http://localhost:8080/users?search=lastName:doe,age>25
and example controller:
#RestController
#RequestMapping(value = "/users")
public class UserController {
#Autowired
private UserRepository repo;
#GetMapping
public List<User> search(#RequestParam(value = "search") String search) {
UserSpecificationsBuilder builder = new UserSpecificationsBuilder();
Pattern pattern = Pattern.compile("(\w+?)(:|<|>)(\w+?),");
Matcher matcher = pattern.matcher(search + ",");
while (matcher.find()) {
builder.with(matcher.group(1), matcher.group(2), matcher.group(3));
}
Specification<User> spec = builder.build();
return repo.findAll(spec);
}
}
As a good alternative to RQSL, you may use the following library: https://github.com/turkraft/spring-filter
It will let you run search queries such as:
/search?filter= average(ratings) > 4.5 and brand.name in ('audi', 'land rover') and (year > 2018 or km < 50000) and color : 'white' and accidents is empty
As you see in the example, it supports functions, searching over nested fields, nested logics, enums, strings, numbers, booleans, dates, ...
Very simple usage:
#GetMapping(value = "/search")
public Page<Entity> search(#Filter Specification<Entity> spec, Pageable page) {
return repo.findAll(spec, page);
}
No need to configure anything, just import the dependency:
<dependency>
<groupId>com.turkraft</groupId>
<artifactId>spring-filter</artifactId>
<version>1.0.5</version>
</dependency>
There are also examples using MongoDB.
You may also want to try Spring Search:
https://github.com/sipios/spring-search
Example: /cars?search=creationyear:2018 AND price<300000 AND (color:Yellow OR color:Blue) AND options.transmission:Auto

Categories