JPA dynamic criteria-api query - jpa

I wonder if there is a generic way to use the criteria api in combination with a little more complex model?
I have an entity class that has one-to-one relationships to other entities. My service wrapper that does the database query via the criteria api gets the parameters from front end to figure out pagination, sorting and filtering.
Entities
#Entity
public class Person implements Serializable {
#Id
private Long id;
private String name;
private String givenName;
#Temporal(TemporalType.DATE)
private Date birthdate;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "INFORMATION_ID")
private Information information;
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "ADDRESS_ID")
private Address address;
...
}
#Entity
public class Information implements Serializable {
#Id
private Long id;
private String detail;
...
}
#Entity
public class Address implements Serializable {
#Id
private Long id;
private String street;
private String city;
...
}
Service
#Stateless
public class PersonService {
#PersistenceContext(unitName = "ProblemGenericDatatableFilterPU")
private EntityManager em;
public List<Person> findAllPersons222(int first, int pageSize, String sortField, SortOrder sortOrder, Map<String, Object> filters) {
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<Person> criteriaQuery = builder.createQuery(Person.class);
Root<Person> rootPerson = criteriaQuery.from(Person.class);
Join<Person, Information> joinPersonInformation = rootPerson.join(Person_.information);
Join<Person, Address> joinPersonAddress = rootPerson.join(Person_.address);
// select
criteriaQuery.select(rootPerson);
// filter
List<Predicate> allPredicates = new ArrayList<>();
for(Entry<String, Object> currentEntry : filters.entrySet()) {
Predicate currentPredicate;
if(currentEntry.getKey().startsWith("information_")) {
currentPredicate = builder.like(
builder.lower(joinPersonInformation.<String>get(currentEntry.getKey())),
builder.lower(builder.literal(String.valueOf(currentEntry.getValue())))
);
}
else if(currentEntry.getKey().startsWith("address_")) {
currentPredicate = builder.like(
builder.lower(joinPersonAddress.<String>get(currentEntry.getKey())),
builder.lower(builder.literal(String.valueOf(currentEntry.getValue())))
);
}
else {
currentPredicate = builder.like(
builder.lower(rootPerson.<String>get(currentEntry.getKey())),
builder.lower(builder.literal(String.valueOf(currentEntry.getValue())))
);
}
allPredicates.add(currentPredicate);
}
criteriaQuery.where(builder.and(allPredicates.toArray(new Predicate[0])));
// order
if(sortField != null && !sortField.isEmpty()) {
Order orderBy;
if(sortField.startsWith("information_")) {
orderBy = (sortOrder == SortOrder.DESCENDING
? builder.desc(joinPersonInformation.get(sortField))
: builder.asc(joinPersonInformation.get(sortField)));
}
else if(sortField.startsWith("address_")) {
orderBy = (sortOrder == SortOrder.DESCENDING
? builder.desc(joinPersonAddress.get(sortField))
: builder.asc(joinPersonAddress.get(sortField)));
}
else {
orderBy = (sortOrder == SortOrder.DESCENDING
? builder.desc(rootPerson.get(sortField))
: builder.asc(rootPerson.get(sortField)));
}
criteriaQuery.orderBy(orderBy);
}
Query query = em.createQuery(criteriaQuery);
// pagination
query.setFirstResult(first);
query.setMaxResults(pageSize);
return query.getResultList();
}
}
I need to do a distinction of cases for filtering and sorting depending on the root/join on which I am accessing the property. Plus I need to use a naming convention in the facelet. The same goes for the count-query except for sorting.
Now I ask myself whether there is some "dot-notation" or anything which makes the case dispensable. In e. g. native SQL I would do something like create a subquery and select all alias values from the inner projection (select * from (select person.name as name, address.street as street, ...) where name = ... and street like ...).
I would be grateful for any advice.

Finally I got the time to deal with my problem. I found a solution thats not perfect, but works for me.
As I searched for another problem I came to this article by Leonardo Shikida and found a very handy Path<?> getPath(...) method (I also had a deeper look into the brilliant inheritance relationships in the CriteriaAPI: Path, Root, Join, From, etc). With that in mind I remermbered my former problem and thought for a more gerneric way of this method. So here is what I made of this:
At first I create all the joins I need (i. e. Root<?> and Join<? ?>) and put them in a Map<String, From<?, ?>> where the String is an element on which an attribute is queried in a dotted notation (naming convention and the downside on the complete solution) and the From is the corresponding source.
With the Map I can do filtering and sorting in a more or less generic way.
To make it work the front end needs to use the very same naming convention and pass the filters-Map accordingly (i. e. JSF using primefaces field attribute in p:column).
public List<Person> newFindAllPersons(int first, int pageSize, String sortField, SortOrder sortOrder, Map<String, Object> filters)
{
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<Person> criteriaQuery = builder.createQuery(Person.class);
// setting up the required joins
Root<Person> rootPerson = criteriaQuery.from(Person.class);
Join<Person, Information> joinPersonInformation = rootPerson.join(Person_.information);
Join<Person, Address> joinPersonAddress = rootPerson.join(Person_.address);
Join<Address, Information> joinAddressInformation = joinPersonAddress.join(Address_.information);
// putting all joins into a map with a dot`ted name
Map<String, From<?, ?>> mapFieldToFrom = new HashMap<>();
mapFieldToFrom.put("person", rootPerson);
mapFieldToFrom.put("person.address", joinPersonAddress);
mapFieldToFrom.put("person.information", joinPersonInformation);
mapFieldToFrom.put("person.address.information", joinAddressInformation);
// select
criteriaQuery.select(rootPerson);
// filter
List<Predicate> allPredicates = new ArrayList<>();
for(Entry<String, Object> currentEntry : filters.entrySet())
{
Predicate currentPredicate = builder.like(
builder.lower(getStringPath(currentEntry.getKey(), mapFieldToFrom)),
builder.lower(builder.literal("%" + String.valueOf(currentEntry.getValue()) + "%"))
);
allPredicates.add(currentPredicate);
}
criteriaQuery.where(builder.and(allPredicates.toArray(new Predicate[0])));
// order
if(sortField != null && !sortField.isEmpty())
{
Path<?> actualPath = getStringPath(sortField, mapFieldToFrom);
Order orderBy = (sortOrder == SortOrder.DESCENDING
? builder.desc(actualPath)
: builder.asc(actualPath));
criteriaQuery.orderBy(orderBy);
}
Query query = em.createQuery(criteriaQuery);
// pagination
query.setFirstResult(first);
query.setMaxResults(pageSize);
return query.getResultList();
}
/**
* divides the given field at the last dot and takes <br>
* - the first part as the key in the map to retrieve the From<?, ?> <br>
* - the last part as the name of the column in the entity
*/
private Path<String> getStringPath(String field, Map<String, From<?, ?>> mapFieldToFrom)
{
if(!field.matches(".+\\..+"))
{
throw new IllegalArgumentException("field '" + field + "' needs to be a dotted path (i. e. customer.address.city.zipcode)");
}
String fromPart = field.substring(0, field.lastIndexOf('.'));
String fieldPart = field.substring(field.lastIndexOf('.') + 1);
From<?, ?> actualFrom = mapFieldToFrom.get(fromPart);
if(actualFrom == null)
{
throw new IllegalStateException("the given map does not contain a from or for the value '" + fromPart + "' or is null");
}
return actualFrom.get(fieldPart);
}
Example front end
<p:dataTable>
<!-- mapFieldToFrom.put("person", rootPerson); -->
<p:column field="person.name">
</p:column>
<!-- mapFieldToFrom.put("person.address", joinPersonAddress); -->
<p:column field="person.address.street">
</p:column>
</p:dataTable>

Related

Why doesn't JPA repeat persist method throw an exception?

Product product = new Product();
product.setName( "foo" );
product.setPrice(BigDecimal.valueOf( 4.5 ) );
pm.create( product ); // pm delegates calls to an entity manager object using persist method and tx is immediately commited after the call
List<Product> products = pm.findAllProducts();
products.stream().forEach( System.out::println ); // New product is listed too.
pm.create( product ); // Causes no exception! But, as per API, it should.
products = pm.findAllProducts(); // Fetch successful
products.stream().forEach( System.out::println ); // No difference from first print.
As per persistence API, if an entity alredy exists, persist(called from pm.create) throw's EntityExistsException, but its not happening as per code.
Pesistence provider(PP) - EclipseLink.
Why is PP ignoring repeat persist?
In what circumstances does a PP choose to throw an exception?
EDIT:
Product.java
NOTE:
Excluded getters and setters(for all fields) and toString() for brevity.
I tried my best to format code as per guidelines, but its not happening, please bear.
#Entity #Table(name = "PRODUCTS") #XmlRootElement #NamedQueries({
#NamedQuery(name = "Product.findAll", query = "SELECT p FROM Product p")
, #NamedQuery(name = "Product.findById", query = "SELECT p FROM Product p WHERE p.id = :id")
, #NamedQuery(name = "Product.findByName", query = "SELECT p FROM Product p WHERE p.name like :name")
, #NamedQuery(name = "Product.findByPrice", query = "SELECT p FROM Product p WHERE p.price = :price")
, #NamedQuery(name = "Product.findByBestBefore", query = "SELECT p FROM Product p WHERE p.bestBefore = :bestBefore")
, #NamedQuery(name = "Product.findByVersion", query = "SELECT p FROM Product p WHERE p.version = :version")
, #NamedQuery(name = "Product.findTotal", query = "SELECT count(p.id), sum(p.price) FROM Product p WHERE p.id in :ids" ) })
public class Product implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Basic(optional = false)
#NotNull
#SequenceGenerator( name="pidGen", sequenceName="PID_SEQ", allocationSize=1 )
#GeneratedValue( strategy=SEQUENCE, generator="pidGen" )
private Integer id;
#Basic(optional = false)
#NotNull
#Size(min = 3, max = 40, message="{prod.name}")
private String name;
// #Max(value=?) #Min(value=?)//if you know range of your decimal fields consider using these annotations to enforce field validation
#Basic(optional = false)
#NotNull
#Max( value=1000, message="{prod.price.max}")
#Min( value=1, message="{prod.price.min}")
private BigDecimal price;
#Column(name = "BEST_BEFORE")
#Temporal(TemporalType.DATE)
//private Date bestBefore;
private LocalDate bestBefore;
#Version
private Integer version;
public Product() {
}
public Product(Integer id) {
this.id = id;
}
public Product(Integer id, String name, BigDecimal price) {
this.id = id;
this.name = name;
this.price = price;
}
#Override
public int hashCode() {
int hash = 0;
hash += (id != null ? id.hashCode() : 0);
return hash;
}
#Override
public boolean equals(Object object) {
// TODO: Warning - this method won't work in the case the id fields are not set
if (!(object instanceof Product)) {
return false;
}
Product other = (Product) object;
if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) {
return false;
}
return true;
}
}
As per the JPA Spec:
If X is a new entity, it becomes managed. The entity X will be entered into the database at or
before transaction commit or as a result of the flush operation.
If X is a preexisting managed entity, it is ignored by the persist operation (...)
If X is a detached object, the EntityExistsException may be thrown when the persist
operation is invoked, or the EntityExistsException or another PersistenceException may be thrown at flush or commit time
When you invoke EntityManager.persist(product), product becomes a managed entity (#1). Any subsequent calls to EntityManager.persist(product) are ignored, as described in #2. The final point applies only when you try to invoke persist() on a detached entity.

How to prevent sorting on a given column?

My repository allows for sorting on any column when retrieving a list of users:
public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom {
#Query("SELECT u FROM User u")
public Page<User> all(Pageable page);
}
The trouble is that a user has some properties that cannot offer sorting, like his confirmedEmail property.
#Entity
#Table(name = "user_account")
#SequenceGenerator(name = "id_generator", sequenceName = "user_account_id_seq", allocationSize = 10)
public class User extends AbstractEntity {
#Column(nullable = false)
private String firstname;
#Column(nullable = false)
private String lastname;
#Column(nullable = false, unique = true)
private EmailAddress email;
#Column(nullable = false)
private boolean confirmedEmail;
}
How can I prevent the Pageable argument from sorting on this boolean confirmedEmail property ?
I stumbled upon this issue when I sorted by clicking on the Confirmed header in my Angular data table.
This front-end event triggered the following request:
SELECT u FROM com.thalasoft.user.data.jpa.domain.User u order by u.confirmed asc
I know I can make this data table column not sortable, and I did. But I'd like to also have a server side safety in place.
As a side note, I wonder if I could also provide a default sorting if none is specified in the client request.
UPDATE: I created the utility method:
public static final Sort stripColumnsFromSorting(Sort sort, Set<String> nonSortableColumns) {
return Sort.by(sort.stream().filter(order -> {
return !nonSortableColumns.contains(order.getProperty());
}).collect(Collectors.toList()));
}
which I call like:
Set<String> nonSortableColumns = new HashSet<String>(Arrays.asList("id", "confirmedEmail"));
public ResponseEntity<PagedResources<UserResource>> all(#PageableDefault(sort = { "lastname", "firstname" }, direction = Sort.Direction.ASC) Pageable pageable, Sort sort,
PagedResourcesAssembler<User> pagedResourcesAssembler, UriComponentsBuilder builder) {
sort = CommonUtils.stripColumnsFromSorting(sort, nonSortableColumns);
userService.addSortToPageable(pageable, sort);
But it is still invoking the sorting on the non sortable column:
Invoking 'com.thalasoft.user.rest.controller.UserController.all' with arguments [Page request [number: 0, size 5, sort: confirmedEmail: DESC], confirmedEmail: DESC, org.springframework.data.web.MethodParameterAwarePagedResourcesAssembler#78817e7e, org.springframework.web.servlet.support.ServletUriComponentsBuilder#3e49647a]
I think you can check Pageable argument of your controller method and then remove from it unnecessary fields, something like this:
public ResponseEntity<?> myControllerMethod(..., Pageable page) {
Sort newSort = Sort.by(page.getSort()
.get()
.filter(order -> !order.getProperty().equals("confirmedEmail"))
.collect(Collectors.toList()));
PageRequest newPage = PageRequest.of(page.getPageNumber(), page.getPageSize(), newSort);
// using newPage instead of page...
}
To specify a default order you can use #PageableDefault annotation, for example:
public ResponseEntity<?> myControllerMethod(..., #PageableDefault(sort = "lastname", direction = ASC) Pageable page) {
//...
}

Exception when selecting specific columns using Hibernate and Spring Data JPA

I have a table that has a bytea column (named 'pdf') and I don't want to always select it, specially when I'm returning a list from the database, due to performance issues.
I use native queries with spring data inside the repository to solve these types of situations before (when I used eclipselink), but with Hibernate, if I don't write all the columns in the query, it throws an exception.
For test purposes, I'm trying to select only the id from the User and I still get the exception.
Example: "SELET user.id FROM user WHERE user.id = '1'"
It throws an exception saying that it did not find name in the ResultSet, if I put name in the SQL, it then says age was not found and so on, until I have to write all the columns in the SQL.
Thanks in advance for any help.
What I have tried already:
Updating/Downgrading Hibernate and Spring Data with no luck.
Creating a new entity with only the columns I need, works, but it's a messy solution for me.
Maybe the problem is the combination of the frameworks I use and the way I use them, if someone wants, I could try to upload my whole project structure.
My code:
Entity
#Entity
#Table(name = "user", schema = "portal")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "id")
private Integer id;
#Column(name = "pdf")
private byte[] pdf;
#Column(name = "name")
private String name;
#Column(name = "age")
private Integer age;
public User() {
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public byte[] getPdf() {
return pdf;
}
public void setPdf(byte[] pdf) {
this.pdf = pdf;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
#Override
public int hashCode() {
int hash = 0;
hash += (id != null ? id.hashCode() : 0);
return hash;
}
#Override
public boolean equals(Object object) {
// TODO: Warning - this method won't work in the case the id fields are not set
if (!(object instanceof Anexo)) {
return false;
}
Anexo other = (Anexo) object;
if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) {
return false;
}
return true;
}
#Override
public String toString() {
return "br.gov.to.secad.portal.domain.User[ id=" + id + " ]";
}
}
Service
#Service
#Transactional(readOnly = true)
public class UserService implements Serializable {
private static final long serialVersionUID = 1L;
#Autowired
private IUserRepository userRepository;
public UserService() {
}
public User findOne() {
return userRepository.findOneSQL();
}
}
Repository
public interface IUserRepository extends JpaRepository<User, Serializable>, JpaSpecificationExecutor {
#Query(value = "SELECT user.id FROM user WHERE user.id = '1'", nativeQuery = true)
public User findOneSQL();
}
The exception:
org.postgresql.util.PSQLException: The column name name was not found in this ResultSet.
Solution
The solution is using an array of Object when I want to select anything less than what I've mapped on my Entity class, thats the limitation of Hibernate that I now understand.
So basically, the method will return Object[] and then I can iterate each position and instantiate a new entity of User with these values.
Example:
#Query(value = "SELECT user.id FROM user WHERE user.id = '1'", nativeQuery = true)
public Object[] findOneSQL();
I have faced the same problem, I know it is late but well there is a solution that I found elegant.
By the Spring documentation you can declare an interface and from here take the fields you want, in my case it has been something similar to this.
The interface to minimize the fields:
public interface CountryMinify {
String getName();
String getNameTranslation();
}
And my JpaRepository
public interface PlanetRepository extends JpaRepository<Planet, Long> {
#Query(value = "select p.name_country as name, p.name_country_translation as nameTranslation from vm_planet p where gid = ?1", nativeQuery = true)
CountryMinify findByCode(String codeCountry);
}
Keep in mind that the columns should be called the same as gos getter. For example: column name_country -> AS name and the getter of the interface is getName()
Try this
#Query(value = "SELECT user.id FROM user WHERE user.id = '1'", nativeQuery = true)
Integer findOneSQL();
Call the method like so
Integer user = userRepository.findOneSQL();
Edit 1 :
Since you are using native query you wont be able to use Projections which is a great way of accessing only certain entity fields. There is a JIRA ticket which is still under investigation.
Solution
Return List from your repository like so
#Query(value = "SELECT user.id, user.name FROM user WHERE user.id = '1'", nativeQuery = true)
List<Object[]> findOneSQL();
Iterate over the list of Objects and get your specific columns.
List<Object[]> userNative = userRepository.findOneSQL();
for (Object[] obj : userNative) {
System.out.println("User id : " + obj[0]);
System.out.println("User Name : " + obj[1]);
}

JPA: query for all items in a list where target is also a list

I have two entities:
Engineer - has a set of tags (say skillsets - Java. .NET, etc.)
Tag
An engineer may have any number of tags. And a tag is associated with any number of engineers.
#Entity
public class Engineer implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(nullable = false, length = 12)
private String id;
#Column(length = 100)
private String name;
#OneToMany(fetch = FetchType.EAGER)
List<Tag> tags;
/* Getters and Setters */
}
#Entity
public class Tag implements Serializable {
private static final long serialVersionUID = 1L;
#Id
private int id;
#Column(length = 50)
private String name;
/* Getters and Setters */
}
Now, I want to query for engineers who have a list of tags associated with them. For example - "Query for engineers with tags 'Java', 'Opensource', and 'Banking'". The query should return engineers who have all the tags associated with them.
I am able to query for one tag:
public List<Engineer> searchMindsByTags(String tag) {
Query q = em.createQuery("SELECT e FROM Engineer e WHERE '" + tag
+ "' = ANY(SELECT tag.name FROM e.tags tag)");
return (List<Engineer>) q.getResultList();
}
What I need is:
public List<Engineer> searchMindsByTags(List<String> tags) {
/* Which query goes here? */
}
Is there a single query which can do this job or do we need iterate over the results progressively to filter?
An engineer may have any number of tags. And a tag is associated with any number of engineers.
You entity design contradicts above requirement.
You modelled Engineer having one to many relationship to Tag. Hence on a tag table you would see a foreign key column engineer_id. Ie: a tag is associated to one (and only 1) engineer.
You need to model the relationship as many to many with join tables. Engineer entity class should have Tag collection decorated with #ManyToMany, Tag entity class should have an Engineer collection decorated with #ManyToMany.
Using the Engineer collection on Tag class you can implement your searchMindsByTags functions
First thing that comes to my mind is that you can construct your query with something like this:
public List<String> searchMindsByTags(List<String> tags)
{
String queryString = "SELECT e FROM Engineer e ";
List<Tag> tagobjects=null;
if (tags != null && !tags.isEmpty()) {
Query q = entityManager.createQuery("Select t from tag t where t.name IN :tags");
q.setParameter("tags", tags);
tagobjects = q.getResultList();
queryString += " WHERE ";
for (int i = 0; i < tagobjects.size(); i++) {
if (i != 0) {
queryString += "AND ";
}
queryString += "?" + (i + 1) + " member of e.tags";
}
}
Query q = entityManager.createQuery(queryString);
if (tagobjects != null && !tagobjects.isEmpty()) {
for (int i = 0; i < tagobjects.size(); i++) {
q.setParameter((i + 1), tagobjects.get(i));
}
}
return (List<Engineer>) q.getResultList();
}
Its quick example, needs some testing.
You can either use multiple sub selects, or use a count.
See,
https://en.wikibooks.org/wiki/Java_Persistence/Querying#Subselect.2C_querying_a_to_many_relationship_where_all_of_the_relations_are_in_a_list

How to query for entities by their collection value

I'm using jpa and I have the following entity:
#Entity
#Table(name="favorites_folders")
public class FavoritesFolder {
private static final long serialVersionUID = 1L;
#Id
private String id;
#NotNull
#Size(min = 1, max = 50)
public String name;
#ElementCollection(fetch = FetchType.LAZY)
#CollectionTable(
name="favorites_products",
joinColumns=#JoinColumn(name="folder_id")
)
#Column(name="product_id")
#NotNull
private Set<String> productsIds = new HashSet<String>();
}
What I want to do is to get a set of FavoritesFolder entities that contains the string "favorite-id" in their productsIds member set.
Does anyone know how can it be done in criteria api?
Update:
I'm thinking the following sql should do the trick but I'm not sure how to do it in either JPQL or Criteria API:
select * from favorites_folders join favorites_products on favorites_folders.id = favorites_products.folder_id where favorites_products.product_id = 'favorite-id'
To get a set of FavoritesFolder entities that contains the string "favorite-id" in their productsIds member set using criteria api you should do the following:
CriteriaBuilder cb = em.getCriteriaBuilder(); //em is EntityManager
CriteriaQuery<FavoritesFolder> cq = cb.createQuery(FavoritesFolder.class);
Root<FavoritesFolder> root = cq.from(FavoritesFolder.class);
Expression<Collection<String>> productIds = root.get("productsIds");
Predicate containsFavoritedProduct = cb.isMember("favorite-id", productIds);
cq.where(containsFavoritedProduct);
List<FavoritesFolder> favoritesFolders = em.createQuery(cq).getResultList();
More information on Collections in JPQL and Criteria Queries.
Just another way using IN
#Entity
public class UserCategory implements Serializable {
private static final long serialVersionUID = 8261676013650495854L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#ElementCollection
private List<String> categoryName;
(...)
}
Then you can write a Criteria query like
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<UserCategory> q = cb.createQuery(UserCategory.class);
Root<UserCategory> root = q.from(UserCategory.class);
Predicate predicate = cb.conjunction();
Predicate p1 = cb.equal(root.get(UserCategory_.targetSiteType), siteType.getName());
Predicate p2 = root.get(UserCategory_.categoryName).in(category);
predicate = cb.and(p1,p2);
q.where(predicate);
TypedQuery<UserCategory> tq = entityManager.createQuery(q);
List<UserCategory> all = tq.getResultList();
if (all == null || all.size() == 0){
return null;
}else if (all.size() > 1){
throw new Exception("Unexpected result - "+all.size());
}else{
return all.get(0);
}
This is my work around that works.
I'm using Springboot 1.5.9. I don't have time to identify the root cause. What I know is such nested property been ignored when get through JacksonMappingAwareSortTranslator.
So what I did to workaround this is not to use Sort object created by resolvers.
Here's my code in Kotlin. Without doing this, the pageable.sort is null and sorting does not work. And my code will create a new PageRequest object that has non-null sort that works.
#RequestMapping("/searchAds", method = arrayOf(RequestMethod.POST))
fun searchAds(
#RequestBody cmd: AdsSearchCommand,
pageable: Pageable,
resourceAssembler: PersistentEntityResourceAssembler,
sort: String? = null
): ResponseEntity<PagedResources<Resource<Ads>>> {
val page = adsService.searchAds(cmd, pageable.repairSortIfNeeded(sort))
resourceAssembler as ResourceAssembler<Ads, Resource<Ads>>
return adsPagedResourcesAssembler.toResource(page, resourceAssembler).toResponseEntity()
}
fun Pageable.repairSortIfNeeded(sort: String?): Pageable {
return if (sort.isNullOrEmpty() || this.sort != null) {
this
} else {
sort as String
val sa = sort.split(",")
val direction = if (sa.size > 1) Sort.Direction.valueOf(sa[1]) else Sort.Direction.ASC
val property = sa[0]
PageRequest(this.pageNumber, this.pageSize, direction, property)
}
}