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

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.

Related

JPQL Query Sort by average rating then box office value for movie

Ok so I have two JPA Entities a Movie and a MovieRating. Similar to the below
#Entity
#Table(name = Movie.TABLE_NAME)
public class Movie {
static final String TABLE_NAME = "Movies";
#Id
#Column(name = "IMDB_ID")
private String imdbID;
#Column(name = "BOX_OFFICE_TAKINGS")
private int boxOfficeTakings;
#OneToMany(mappedBy = "movie", cascade = CascadeType.ALL, orphanRemoval = true)
private List<MovieRating> ratings = new ArrayList<>();
// Getters and setters
}
And
#Entity
#Table(name = MovieRating.TABLE_NAME)
public class MovieRating {
static final String TABLE_NAME = "MovieRatings";
#Id
#GeneratedValue(generator = "UUID")
#GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
private UUID id;
private int rating;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "IMDB_ID")
private Movie movie;
//Getters and setters
}
I've written a #Query in the JPA Repository that should return results ordered by average rating descending and then box office value descending eg.
#Query("SELECT m FROM Movie m LEFT JOIN m.ratings r GROUP BY m ORDER BY AVG(r.rating) desc, m.boxOfficeTakings desc")
List<Movie> findTop10OrderedByBoxOfficeTakings(Pageable pageable);
So I'd expect for example a rated movie with a lower box office value to appear above an unrated movie with a higher box office value.
So I've written a test around this and it seems to work like I'd expect
#Test
public void testGetTop10ReturnsRatedMovieAboveUnratedMovieWithHigherValue() {
Movie movie1 = createMovieWithRatings(new Movie("tt000001", "The Godfather part 1", 2010, 268500000), null);
Movie movie2 = createMovieWithRatings(new Movie("tt000002", "The Godfather part 2", 2010, 93000000),
Arrays.asList(new MovieRating(10)));
movieRepository.saveAll(Arrays.asList(movie1, movie2));
List<MovieResponse> top10 = movieService.getTop10OrderedByBoxOfficeTakings();
List<String> expectedMovieIds = Arrays.asList(movie2.getImdbID(), movie1.getImdbID());
List<String> actualMovieIds = top10.stream().map(movie -> movie.getImdbID()).collect(Collectors.toList());
Assertions.assertEquals(expectedMovieIds, actualMovieIds);
}
private Movie createMovieWithRatings(Movie movie, List<MovieRating> ratings) {
if (ratings != null) {
ratings.forEach(rating -> movie.addRating(rating));
}
return movie;
}
However in practice when the code is running the order is movie1, and then movie2. Movie2 only moves above movie1 when it receives more than 1 rating. Why?
Maybe because you are doing a LEFT JOIN and with LEFT JOIN anything you try to join on if doesn't have a value, in your case "The Godfather part 1" doesn't have any ratings so comes as null. try putting a null check in the query itself and if it is null replace with 0, so now it can calculate the AVG for each movie. That might help.

JPQL: How to join via #ElementCollection with #MapKeyJoinColumn

I have problems creating the correct JPQL query for joining through the following tables:
While between GROUPS and USERS there is a conventional #ManyToMany mapping table, DOCUMENTS_GROUPS is what causes the trouble. As you can see in the following entity, I want the relationship between DOCUMENTS and GROUPS to be mapped as a Map containing the access_mode (which works just fine except for the query):
#Entity
#Table(name = "DOCUMENTS")
#NamedQueries({
#NamedQuery(
name = "Documents.findAccessibleByUser",
query = "SELECT d FROM Document d INNER JOIN d.groups g INNER JOIN KEY(g).members m WHERE m.id = :userId"
)
})
public class Document {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
#ElementCollection
#CollectionTable(name = "DOCUMENTS_GROUPS", joinColumns = {#JoinColumn(name = "document_id")})
#MapKeyJoinColumn(name = "group_id")
#Column(name = "access_mode")
#Enumerated(EnumType.STRING)
private Map<Group, AccessMode> groups = new HashMap<>();
/* ... */
}
With Group being rather normal:
#Entity
#Table(name = "GROUPS")
public class Group {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
#Column(length = 255)
private String name;
#ManyToMany
#JoinTable(name = "USERS_GROUPS", //
joinColumns = {#JoinColumn(name = "group_id")}, //
inverseJoinColumns = {#JoinColumn(name = "user_id")} //
)
private Set<User> members = new HashSet<>();
/* ... */
}
My question is now: How do I need to modify the second JOIN in my JPQL query?
SELECT d FROM Document d
INNER JOIN d.groups g
INNER JOIN KEY(g).members m
WHERE m.id = :userId
is syntactically wrong (unexpected KEY after INNER JOIN).
Of course, I have already tried a plain INNER JOIN g.members m, but since we're dealing with a Map<Group, AccessMode>, this fails with cannot dereference scalar collection element: members.
I was facing the same problem with a simple key-value Map<String, String> like:
#Entity Item.java
#ElementCollection
#MapKeyColumn(name = "name")
#Column(name = "value")
#CollectionTable(indexes = #Index(columnList = "value"))
private Map<String, String> attributes = new HashMap<>();
Joining the attributes was possible:
Query query = em.createQuery("SELECT i FROM Item i INNER JOIN i.attributes attr");
but not querying fields:
Query query = em.createQuery("SELECT i FROM Item i INNER JOIN i.attributes attr WHERE attr.value = 'something'");
I debugged the Hibernate internals and found out that the alias attr is already resolved to the value (e.attributes.value), so the only thing you can do here is:
Query query = em.createQuery("SELECT i FROM Item i INNER JOIN i.attributes attr WHERE attr = 'something'");
But I did not find any documentation or JPQL examples pointing that out. The behaviour is is useless in my case, because I want to have conditions for both key and value. Thats why I migrated to a foreign entity collection with key mapping and composite primary key. Its way more complicated but works as expected.
The composite key entity to prevent single primary keys
#Embeddable
public class ItemAttributeName implements Serializable {
private String name;
#ManyToOne
#JoinColumn(nullable = false)
private Item item;
// Empty default constructor is important
public ItemAttributeName() {
}
public ItemAttributeName(Item item, String name) {
this.item = article;
this.name = name;
}
}
The real attribute entity
#Entity
public class ItemAttribute {
#EmbeddedId
private ItemAttributeName id;
private String value;
// Empty default constructor is important
public ItemAttribute() {
}
public ItemAttribute(Item item, String name) {
this.id = new ItemAttributeName (item, name);
}
public String getValue() {
return value;
}
}
#Entity Item.java
#OneToMany(mappedBy = "id.item",cascade = CascadeType.PERSIST)
#MapKeyColumn(name = "name")
public Map<String, ItemAttribute> attributes = new HashMap<>();
Creating entities:
Item item = new Item ();
ItemAttribute fooAttribute = new ItemAttribute(item, "foo");
fooAttribute.setValue("356");
item.attributes.put("foo", fooAttribute);
Querying entities:
Query query = em.createQuery("SELECT i FROM Item i JOIN i.attributes attr WHERE attr.id.name = 'foo' AND attr.value='bar'");
List<Item> resultList = query.getResultList();
System.out.println(resultList.get(0).attributes.get("foo").getValue());
Prints out: bar

JPA SQLResultSetMapping for SQL Aliases not Working At All?

Native SQL with aliased field names + remapping to receive managed entities is required for more complex queries with joined tables.
However, the mapping of the SQL aliases leads to an exception where the aliased fields cannot be found. Can anybody detect an error in the code below, or is SQLResultSetMapping broken? (The sample below is intentionally simple to allow quick checking)
RDBMS H2, DDL
create table A(
ID INTEGER DEFAULT NOT NULL AUTO_INCREMENT PRIMARY KEY,
VAL VARCHAR(10)
);
insert into A (val) values ('val1');
insert into A (val) values ('val2');
Java class
#Entity
#NamedNativeQuery(name = "queryall",
query="select ID as AID, val from A",
resultSetMapping = "mapping")
#SqlResultSetMapping(name = "mapping",
entities = #EntityResult(
entityClass = A.class,
fields = {#FieldResult(name = "ID", column = "AID")})
)
public class A implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "ID")
private Integer id;
#Column(name = "VAL")
private String val;
public A() {
}
public A(Integer id) {
this.id = id;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getVal() {
return val;
}
public void setVal(String val) {
this.val = val;
}
#Override
public String toString() {
return "entities.A[ id=" + id +", val="+val+ " ]";
}
public static void main(String[] args) {
EntityManagerFactory entityManagerFactory =
Persistence.createEntityManagerFactory("JavaApplication6PU");
EntityManager em = entityManagerFactory.createEntityManager();
Query sqlQuery = em.createNamedQuery("queryall");
List list = sqlQuery.getResultList();
for (Iterator<A> iterator = list.iterator(); iterator.hasNext();) {
a = iterator.next();
System.out.println(String.format("entity %s, managed: %s", a, em.contains(a)));
}
}
}
Execution stops with exception:
[EL Warning]: 2018-01-12 21:45:42.748--UnitOfWork(1823014131)--Exception
[EclipseLink-6044] (Eclipse Persistence Services - 2.5.2.v20140319-9ad6abd):
org.eclipse.persistence.exceptions.QueryException
Exception Description: The primary key read from the row [DatabaseRecord(
A.ID => null
A.VAL => val1)] during the execution of the query was detected to be null.
Primary keys must not contain null.
Query: ResultSetMappingQuery(name="queryall" referenceClass=A sql="select ID as AID, val from A")
This, in other words, means: No mapping has taken place -> aliased fields not found
The same when the mapping is announced in adhoc Queries.
Query sqlQuery = em.createNativeQuery("select ID as AID, val from A","mapping");
If resultClass is used instead of resultSetMapping and no SQL aliases exist, the output is as it should be. (This proves that there is no misspelling of fields or any other error)
#NamedNativeQuery(name = "queryall",
query="select ID, val from A",
resultClass = A.class)
Output:
entity entities.A[ id=1, val=val1 ], managed: true
entity entities.A[ id=2, val=val2 ], managed: true

JPA dynamic criteria-api query

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>

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