spring data sort by map-value within a given key - jpa

I would like to sort a Map of by the value. For example I have Person class which has a map of details that are stored in a map with key-value Map<String, String>.
I am using springboot with hibernate5. This is the mapping.
public class Person implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#DocumentId
#Column(name = "personid")
private Integer id;
#Column(name = "name")
private String name;
// this is a collection of person details
#ElementCollection()
#MapKeyColumn(name = "detailkey")
#Column(name = "detailvalue")
#CollectionTable(name = "details", joinColumns = #JoinColumn(name = "personid"))
Map<String, String> details = new HashMap<>();
//getter and setters omitted
So far I am able to retrieve a person with some specific detailskey and specific detail value. So for example a person table in the DB has eyecolor as detail attribute and as value can have "green", "blue", "brown". Note this is not a real example, just for clarity purposes.
So for example I can get the list of persons and sort them by their name, in the controller I can do
Sort sort = new Sort(Sort.Direction.ASC, "name");
and the opposite direction
Sort sort = new Sort(Sort.Direction.DESC, "name");
Pageable pageable = new PageRequest(1, 10, sort);
pageResult = personRepository.findAll(
"eyecolor", "green", pageable
);
and this one will return the list of persons that have "eyecolor" as green. So far so good and this is working as expected. Now I would like to define a sorting on the detailvalue.
For example I would like to get a list of person sorted by their eyecolor. So first I should have the persons that have "blue", "brown", "green".
how can the Sort be specified in this case ?
In standard SQL it would be something like this:
SELECT p.* from persons p LEFT JOIN details d ON
p.personid = d.personid AND p.detailkey='eyercolor' ORDER BY
p.detailvalue ASC;

The following query worked for me:
SELECT p FROM Person p JOIN p.details d WHERE KEY(d) = 'eyecolor' ORDER BY d
(note that ORDER BY VALUE(d) would fail since VALUE(d) still seems to behave as described here: JPA's Map<KEY, VALUE> query by JPQL failed)
Now, I'm not particularly well versed with Spring Data, but I suppose you should be able to use the above query (without the ORDER BY part) with the #Query annotation on your PersonRepository.findAll method (I'm assuming that's a custom method) and provide the sorting using JpaSort.unsafe("d").

Related

JPA Criteria subquery with tuple

I define two Entity
#Entity
class Template {
#Id
private String id;
private String name;
#OneToMany(fetch=FetchType.LAZY, mappedBy="template")
private List<Edition> editions;
}
class Edition {
#Id
private String id;
private Integer version;
private String state;
#ManyToOne(fetch=FetchType.LAZY)
private Template template;
}
And I want to query each template's newest version and edition's state, so my native sql is:
select
a.id, a.name, b.version, b.state
from
tb_pm_template a,
tb_pm_edition b
where
a.id = b.template_id and
(b.template_id, b.version) in (select template_id, max(version) from tb_pm_edition group by template_id)
The navtive sql work fine. But I want to write in jpa Criteria api way.
So, I try the code below:
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Tuple> maxVersionQuery = cb.createTupleQuery();
Root<Edition> editionRoot = maxVersionQuery.from(Edition.class);
maxVersionQuery.multiselect(
editionRoot.get("template").get("id").alias("templateId"),
cb.max(editionRoot.get("version")).alias("maxVersion")
).groupBy(editionRoot.get("template").get("id"));
List<Tuple> maxVersion = entityManager.createQuery(maxVersionQuery).getResultList();
List<Map<String, Object>> maxVersionResult = new ArrayList<>(maxVersion.size());
for (Tuple tuple: maxVersion) {
Map<String, Object> row = new HashMap<>(2);
for (TupleElement element: tuple.getElements()) {
row.put(element.getAlias(), tuple.get(element.getAlias()));
}
maxVersionResult.add(row);
}
// the maxVersion or maxVersionResult contain the template's newest version info I want, then I want to combine the version state and template name
CriteriaQuery<Tuple> templateQuery = cb.createTupleQuery();
Root<Edition> editionRoot1 = templateQuery.from(Edition.class);
templateQuery.multiselect(
editionRoot1.get("template").get("id").alias("id"),
editionRoot1.get("template").get("name").alias("name"),
editionRoot1.get("version").alias("version"),
editionRoot1.get("state").alias("versioinState")
).where(
// here I don't know how to connect the conditions
// I try the cb.in, but it needs Expression type
// I also try to use the Subquery api, but since I need the subquery return template_id and version, so I define Subquery<Tuple>, but the Subquery instance's select method only take one parameter
// I check the official document in comments, the example only show the aggregate without groupby
);
Is my implementation way wrong? On this basis, I also need to add paging and sorting, so if it is divided into two or more SQL statements, will it affect the paging count?

How to join fetch an attribute of element in collection with QueryDSL JPA

Entities as below:
class A {
Long id;
#ManyToMany
#JoinTable(name = "rel_a_b", joinColumns = #JoinColumn(name = "a_id"), inverseJoinColumns = #JoinColumn(name = "b_id"))
Set<B> bSet;
}
class B {
Long id;
#ManyToMany(mappedBy = "bSet")
Set<A> aSet;
#ManyToMany
#JoinTable(name = "rel_b_c", joinColumns = #JoinColumn(name = "b_id"), inverseJoinColumns = #JoinColumn(name = "c_id"))
Set<C> cSet;
}
class C {
Long id;
#ManyToMany(mappedBy = "cSet")
Set<B> bSet;
}
I need to select A entities and join fetch bSet and cSet in B entity. Using JPA Criteria, codes as below:
final Fetch<A, B> bSetFetch = rootA.fetch("bSet", JoinType.LEFT);
bSetFetch.fetch("cSet", JoinType.LEFT);
are working perfectly, but I can't achieve this with QueryDSL. I tried
final QA a = QA.a;
jpaQuery
.from(a)
.leftJoin(a.bSet, QB.b).fetchJoin()
.leftJoin(QB.b.cSet).fetchJoin()
.select(a)
but it throws exception that
query specified join fetching, but the owner of the fetched association was not present in the select list [FromElement{explicit,not a collection join,fetch join,fetch non-lazy properties,classAlias=b,role=A.b,tableName=`b`,tableAlias=b4_,origin=a a2_,columns={a2_.id ,className=B}}] [select a
from A a
left join fetch a.bSet as b
left join fetch b.cSet]]
, if without fetchJoin(), the results don't include bSet and cSet. Could anyone solve this?
The fetch joins are applied correctly from a QueryDSL point of view. We can also observe this from the fact that the produced JPQL query looks correct.
The limitation here is that Hibernate only allows FETCH JOINS if the owner of the fetch association is projected in the select clause. cSet is an association on B, so you would need to project your b's or omit the fetch join for cSet. For example:
jpaQuery
.from(a)
.leftJoin(a.bSet, QB.b).fetchJoin()
.leftJoin(QB.b.cSet).fetchJoin()
.select(a, b)
Now this will result in duplicate results for a due to the cardinality of bSet. That is just a limitation of fetch joins in Hibernate.
Alternatively, you could consider specifying a fetch graph for the query:
EntityGraph postGraph = em.getEntityGraph("post");
query.setHint("javax.persistence.fetchgraph", postGraph);
For more information on using EntityGraphs see https://www.baeldung.com/jpa-entity-graph

EclipseLink ManyToOne - CriteriaBuilder Generated Query is Wrong

I have an Entity with a ManyToOne Relationship to the Primary Key of another entity. When I create a query that references this Foreign Key eclipseLink always creates a join instead of simply accessing the Foreign Key.
I have created a highly simplified example to show my issue:
#Entity
public class House {
#Id
#Column(name = "H_ID")
private long id;
#Column(name = "NAME")
private String name;
#ManyToOne
#JoinColumn(name = "G_ID")
private Garage garage;
}
#Entity
public class Garage{
#Id
#Column(name = "G_ID")
private long id;
#Column(name = "SPACE")
private Integer space;
}
I created a query that should return all houses that either have no garage or have a garage with G_ID = 0 using the CriteriaBuilder.
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<House> query = cb.createQuery(House.class);
Root<House> houseRoot = query.from(House.class);
Path<Long> garageId = houseRoot.get(House_.garage).get(Garage_.id);
query.where(cb.or(cb.equal(garageId , 0), cb.isNull(garageId)));
TypedQuery<House> typedQuery = entityManager.createQuery(query);
List<House> houses = typedQuery.getResultList();
The generated query is:
SELECT h.NAME, h.G_ID FROM HOUSE h, GARAGE g WHERE (((h.G_ID= 0) OR (g.G_ID IS NULL)) AND (g.G_ID = h.G_ID));
I don't understand why
The or condition first references table HOUSE and then GARAGE (instead of HOUSE)
The join is created in the first place.
The correct query should look like this in my understanding:
SELECT h.NAME, h.G_ID FROM HOUSE h WHERE (((h.G_ID= 0) OR (h.G_ID IS NULL));
Or if a join is made it should take into account that the ManyToOne relationship is nullable and therefore do a LEFT OUTER JOIN.
SELECT h.NAME, h.G_ID FROM HOUSE h LEFT OUTER JOIN GARAGE g ON (h.G_ID = g.G_ID ) WHERE (h.G_ID = 0) OR (g.G_ID IS NULL);
(Note both these queries would work correctly in my more complicated setup. I also get the same error when only wanting to retrieve all houses that have no garage.)
How can I achieve this (while still using the CriteriaBuilder and ideally not having to change the DB Model)?
(Please let me know any additional information that might be required, I'm very new to this topic and came across this issue while migrating an existing application.)
-- edit --
I have found a solution to my problem that will result in slightly different behaviour (but in my application that part of the code I had to migrate didn't make much sense in the first place). Instead of using
Path<Long> garageId = houseRoot.get(House_.garage).get(Garage_.id);
I use
Path<Garage> garage = houseRoot.get(House_.garage);
And then as expected table Garage isn't joined anymore. (I assume the code previously must have been some kind of hack to get the desired behaviour from openJPA)
I don't understand why
The or condition first references table HOUSE and then GARAGE (instead of HOUSE)
I believe this is implementation specific; in any case, it shouldn't have any bearing on the results.
The join is created in the first place.
By saying Path<Long> garageId = houseRoot.get(House_.garage).get(Garage_.id) you're basically telling EclipseLink: 'join Garage to House, we're gonna need it'. That you then access Garage_.id (and not, for example, Garage_.space) is inconsequential.
If you don't want the join, simply map the G_ID column one more time as a simple property: #Column(name = "G_ID", insertable = false, updatable = false) private Long garageId. Then refer to House_.garageId in your query.
Or if a join is made it should take into account that the ManyToOne relationship is nullable and therefore do a LEFT OUTER JOIN.
Path.get(...) always defaults to an INNER JOIN. If you want a different join type, use Root.join(..., JoinType.LEFT), i. e. houseRoot.join(House_.garage, JoinType.LEFT).get(Garage_.id).
One solution that results in the same behaviour is:
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<House> query = cb.createQuery(House.class);
Root<House> houseRoot = query.from(House.class);
Path<Garage> garage = houseRoot.get(House_.garage);
Path<Long> garageId = garage.get(Garage_.id);
query.where(cb.or(cb.equal(garageId , 0), cb.isNull(garage)));
TypedQuery<House> typedQuery = entityManager.createQuery(query);
List<House> houses = typedQuery.getResultList();
This results in the following SQL:
SELECT H_ID, NAME, G_ID FROM HOUSE WHERE ((G_ID = 0) OR (G_ID IS NULL));

How replace native order by clause on JPA equivalent?

I use JPA 2.0 criteria builder. I need get data from one table and sort them by column from other. This tables have relations OneToMany:
class Club{
#OneToMany(mappedBy = "club")
private List<Address> addresses;
...
}
class Address{
#JoinColumn(name = "club_id", referencedColumnName = "id")
#ManyToOne(fetch = FetchType.LAZY)
private Club club;
#Column(name = "type")
private Long type;
#Column(name = "full_address")
private String full_address;
...
}
May be several address of some type but I need only one row of this specific address.
I write native queries with subquery, but it's has problem because subquery doesn't use in order clause and in select clause in JPA 2.0.
select c.full_name from club c
ORDER BY (select a.full_address from address a WHERE c.id= a.club_id and a.type=1 LIMIT 1)
select c.full_name, (select a.full_address from address a WHERE a.type=1 AND c.id=a.club_id LIMIT 1) as full_address FROM club c
ORDER BY fullAddress;
How I can replace native order by clause on JPA equivalent?
Thanks!
This native query also resolve problem and it can replace by JPA query
select c.full_name, min(a.full_address) FROM club c LEFT JOIN address a on c.id = a.club_id
where a.id is null or a.type=1 or not exists(SELECT 1 from address aSub WHERE aSub .club_id=c.id AND aSub.type=1)
GROUP BY c.id, c.full_name ORDER BY min(a.full_address);
JPA equivalent
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<ClubItem> query = builder.createQuery(ClubItem.class);
Root<Club> root = query.from(Club.class);
Join<Club, Address> addressJoin = root.join(Club_.address, JoinType.LEFT);
query.select(builder.construct(ClubItem.class, root.get(Club_.id), root.get(Club_.fullName), builder.function("min", String.class, addressJoin.get(Address_.fullAddress))));
Subquery<Address> subquery = query.subquery(Address.class);
Root<Address> addressRoot = subquery.from(Address.class);
subquery.select(addressRoot);
subquery.where(
builder.and(
builder.equal(addressRoot.get(Address_.type), 1),
builder.equal(addressRoot.get(Address_.clubId), root.get(Club_.id))));
query.where(builder.or(builder.isNull(addressJoin), builder.equal(addressJoin.get(Address_.type), builder.literal(new Long(1))),
builder.not(builder.exists(subquery))));
query.groupBy(root.get(Club_.id), root.get(Club_.fullName))
Order order = builder.asc(builder.function("min", String.class, addressJoin.get(Address_.fullAddress)));
query.orderBy(order);
TypedQuery<ClubItem> contentQuery = em.createQuery(query);
It's not terribly elegant, but it gets the job done...
Make your "Club" class implement Comparable. Put the order-by logic into the Comparable. Then use Collections.sort(unsortedList) to get the list into sorted form. There's also a Collections.sort(unsortedList, Comparable) method which could be useful, especially if you are doing a bunch of similar methods that just vary on order-by.

Constraint the number of rows returned by JOIN FETCH in JPA

I'm having a Product object with a list of related products (which are also product objects). The field of related products is annotated like this:
public class Product {
#JoinTable(name = "RELATED_PRODUCT", joinColumns = {
#JoinColumn(name = "PRODUCT_ID", referencedColumnName = "id", nullable = false)}, inverseJoinColumns = {
#JoinColumn(name = "RELATED_PRODUCT_ID", referencedColumnName = "id", nullable = false)})
#ManyToMany(fetch = FetchType.LAZY)
List<Product> relatedProducts;
}
As you can see the list is fetched lazy, which is what I want in most cases. In some cases however, I want the list of related products to be filled immediatly. I created a query for this with a LEFT JOIN FETCH. However, I want only the related products to be added that have a certain rating, let's say a rating of > 3.
I tried the following:
SELECT DISTINCT p FROM Product p LEFT JOIN FETCH p.comparableProducts cp WHERE p.id = :id AND cp.rating > 3 AND CURRENT_DATE BETWEEN p.commenceDate AND p.removeDate
But this doesn't work. It always returns back ALL related products in the database, not just the ones that have a rating above 3. How is this fixable?
The easiest way to solve this problem is to load related products separately instead of trying to fit them into relatedProducts field.
It also makes perfect sense from object oriented point of view. I suppose you have something like "Product page" that contains the selected product and "recommended products". If so, such a page is a separate concept that deserves its own class:
public class ProductPage {
private Product product;
private List<Product> recommendedProducts;
...
}
Then you can fill such a class either by a single query:
SELECT DISTINCT p, cp FROM Product p LEFT JOIN p.comparableProducts cp WHERE p.id = :id AND cp.rating > 3 AND CURRENT_DATE BETWEEN p.commenceDate AND p.removeDate
or by two separate queries.
Unfortunately, this approach doesn't allow you to receive an instance of ProductPage directly from JPA, you need to write conversion code manually.