Aggregation in Hibernate / JPA - java

Let's say I have an entity such as
#Entity
#Table(name = "t_entity")
Entity {
#Id
#GeneratedValue
#Column(name = "id")
int id;
#Column(name = "name")
String name;
#Column(name = "version")
int version;
#Column(name = "parameter")
String parameter;
}
Now, I need to fetch latest versions of entities having given names and specific parameter value. In plain SQL I'd write something like this:
select id, name, max(version) version, parameter
from entity
where name in ('foo', 'bar') and parameter = 'baz'
group by name, id
But, how do I do that with Hibernate (preferably using Criteria API) ?
UPD:
First of all, as it was stated in comments, my example query is incorrect. The correct version of what I was trying to achieve looks more like this:
select e0.id, e0.name, e0.version, e0.parameter from entity e0
right join (select name, max(version) mv from entity where name in ('foo', 'bar') group by name) e1
on e0.name = e1.name and e0.versioin = e1.mv
where e0.parameter = 'baz'
My best working version so far involves two separate requests:
CriteriaBuilder cb = sessionFactory.getCriteriaBuilder();
CriteriaQuery<Tuple> lvq = cb.createQuery(Tuple.class);
Root<Entity> lvr = lvq.from(Entity.class);
lvq.multiselect(lvr.get("name").alias("name"), cb.max(lvr.get("version")).alias("mv"));
lvq.where(lvr.get("name").in("foo", "bar"));
lvq.groupBy(lvr.get("name"));
List<Tuple> lv = sessionFactory.getCurrentSession().createQuery(lvq).getResultList();
CriteriaQuery<Entity> cq = cb.createQuery(Entity.class);
Root<Entity> root = cq.from(Entity.class);
List<Predicate> lvp = new LinkedList<>();
for (Tuple tuple : lv) {
lvp.add(cb.and(
cb.equal(root.get("version"), tuple.get("mv")),
cb.equal(root.get("name"), tuple.get("name"))));
}
cq.select(root).where(cb.and(
cb.equal(root.get("parameter"),"baz"),
cb.or(lvp.toArray(new Predicate[lvp.size()])));
return sessionFactory.getCurrentSession().createQuery(cq).getResultList();
Btw, probably I was not clear enough about which Criteria API I wanna use, so it's JPA.
Thank you guys for answers, they've pushed forward to create ugly, but at least working solution.
I will appreciate any suggestions how to improve the code for such a task.

createCriteria(Entity.class).add(Restrictions.in("name", new String[] {"foo","bar"})).add(Restrictions.eq("parameter","baz")).add( Projections.groupProperty("name")).add(Projections.groupProperty("id")).list()
Something like this should work. What have you tried?

Criteria criteria = session.createCriteria(Entity.class,"entity");
criteria
.add(Restrictions.in("name", new String[] {"foo","bar"}))
.add(Restrictions.eq("parameter","baz"))
.setProjection(Projections.projectionList()
.add(Projections.property("name"), "name")
.add(Projections.property("id"), "id")
.add(Projections.max("version"))
.add(Projections.groupProperty("name"))
.add(Projections.groupProperty("id"))
.list();
I hope this works

Related

Unwanted Cross Join in JPA CriteriaQuery select in subquery

I'm getting what I think are needless CROSS JOINs when I'm doing a select IN SUBQUERY, which is hurting performance. I'm using Postgres if that makes a difference.
I'm aiming to generate the following query
select a1.first_name from author a1
where a1.last_name = ?
and (a1.id in
(select distinct b.author_id
from book b
where (b.published_on between ? and ?)
group by b.author_id
having count(b.author_id) >= 2))
But I get
select a1.first_name from author a1
where a1.last_name = ?
and (a1.id in
(select distinct b.author_id
from book b
cross join author a2 where b.author_id = a2.id -- <<< I don't want this cross join!
and (b.published_on between ? and ?)
group by b.author_id
having count(b.author_id) >= 2))
Code
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<String> cq = cb.createQuery(Author.class);
Root<Author> authorRoot = cq.from(Author.class);
Subquery<Long> countSubquery = cq.subquery(Long.class);
Root<Book> bookRoot = countSubquery.from(Book.class);
Expression<Long> count = cb.count(bookRoot.get(Book_.author));
countSubquery.select(bookRoot.get(Book_.AUTHOR))
.distinct(true)
.where(cb.between(bookRoot.get(Book_.publishedOn),
LocalDate.of(2021, MARCH, 1),
LocalDate.of(2021, MARCH, 31)))
.groupBy(bookRoot.get(Book_.author))
.having(cb.greaterThanOrEqualTo(count, 2L));
cq.where(
cb.equal(authorRoot.get(Author_.lastName), "Smith"),
cb.in(authorRoot.get(Author_.ID)).value(countSubquery));
cq.select(authorRoot.get(Author_.FIRST_NAME));
TypedQuery<String> query = entityManager.createQuery(cq);
return query.getResultList();
In reality I'm generating the queries from a user driven query builder, this code recreates the exact problem I'm having.
When using the query builder the user could end up with multiple select in subqueries so I need this to perform as well as possible.
I don't see why I should need any join/cross join for my query to work.
Entities
#Entity
public class Author {
#Id
#GeneratedValue
private Long id;
private String firstName;
private String lastName;
#OneToMany(mappedBy = "author", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<Book> books;
}
#Entity
public class Book {
#Id
#GeneratedValue
private Long id;
private String name;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "author_id")
private Author author;
private LocalDate publishedOn;
}
This expression: bookRoot.get(Book_.author) means you're joining Author to Book implicitly.
To get rid of the extra join, you would have to either use a native query, or map Book.author_id once more as a simple column:
#Column(name = "author_id", insertable = false, updatable = false)
private Long authorId;
And use Book_.authorId instead.

JPA select association and use NamedEntityGraph

Our in-house framework built with Java 11, Spring Boot, Hibernate 5 and QueryDSL does a lot of auto-generation of queries. I try to keep everything efficient and load associations only when needed.
When loading full entities, the programmer can declare a NamedEntityGraph to be used. Now there is one case where a query like this is generated:
select user.groups
from User user
where user.id = ?1
Where the Entities in question look like this:
#Entity
#NamedEntityGraph(name = User.ENTITY_GRAPH,
attributeNodes = {
#NamedAttributeNode(User.Fields.permissions),
#NamedAttributeNode(value = User.Fields.groups, subgraph = "user-groups-subgraph")
},
subgraphs = #NamedSubgraph(
name = "user-groups-subgraph",
attributeNodes = {
#NamedAttributeNode(Group.Fields.permissions)
}
))
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Enumerated(EnumType.STRING)
#ElementCollection(targetClass = Permission.class)
#CollectionTable(name = "USERS_PERMISSIONS", joinColumns = #JoinColumn(name = "uid"))
private Set<Permission> permissions = EnumSet.of(Permission.ROLE_USER);
#ManyToMany(fetch = LAZY)
private Set<Group> groups = new HashSet<>();
}
#Entity
#NamedEntityGraph(name = Group.ENTITY_GRAPH,
attributeNodes = {
#NamedAttributeNode(value = Group.Fields.permissions)
})
public class Group {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
#Enumerated(EnumType.STRING)
#ElementCollection(targetClass = Permission.class)
#CollectionTable(
name = "GROUPS_PERMISSIONS",
joinColumns = #JoinColumn(name = "gid")
)
#NonNull
private Set<Permission> permissions = EnumSet.noneOf(Permission.class);
}
When selecting either User or Group directly, the generated query simply applies the provided NamedEntityGraphs. But for the above query the exception is:
org.hibernate.QueryException:
query specified join fetching, but the owner of the fetched association was not present in the select list
[FromElement{explicit,collection join,fetch join,fetch non-lazy properties,classAlias=user,role=foo.bar.User.permissions,tableName={none},tableAlias=permission3_,origin=null,columns={,className=null}}]
I first tried the User graph, but since we are fetching Groups, I tried the Group graph. Same Exception.
Problem is, there is no easy way to add a FETCH JOIN to the generated query, since I don't know which properties of the association should be joined in anyway. I would have to load the Entitygraph, walk it and any subgraph and generated the right join clauses.
Some more details on Query generation:
// QueryDsl 4.3.x Expressions, where propType=Group.class, entityPath=User, assocProperty=groups
final Path<?> expression = Expressions.path(propType, entityPath, assocProperty);
// user.id = ?1
final BooleanExpression predicate = Expressions.predicate(Ops.EQ, idPath, Expressions.constant(rootId));
// QuerydslJpaPredicateExecutor#createQuery from Spring Data JPA
final JPQLQuery<P> query = createQuery(predicate).select(expression).from(path);
// Add Fetch Graph
((AbstractJPAQuery<?, ?>) query).setHint(GraphSemantic.FETCH.getJpaHintName(), entityManager.getEntityGraph(fetchGraph));
EDIT:
I can reproduce this with a simple JPQL Query. It's very strange, if I try to make a typed query, it will select a List of Sets of Group and untyped just a List of Group.
Maybe there is something conceptually wrong - I'm selecting a Collection and I'm trying to apply a fetch join on it. But JPQL doesn't allow a SELECT from a subquery, so I'm not sure what to change..
// em is EntityManager
List gs = em
.createQuery("SELECT u.groups FROM User u WHERE u.id = ?1")
.setParameter(1, user.getId())
.setHint(GraphSemantic.FETCH.getJpaHintName(), em.getEntityGraph(Group.ENTITY_GRAPH))
.getResultList();
Same Exception:
org.hibernate.QueryException: query specified join fetching, but the owner of the fetched association was not present in the select list
So the problem can be distilled down to a resolution problem of the Entit Graphs attributes:
select user.groups
from User user
where user.id = ?1
With the Entity Graph
EntityGraph<Group> eg = em.createEntityGraph(Group.class);
eg.addAttributeNodes(Group.Fields.permissions);
Gives an Exception that shows that Hibernate tries to fetch User.permissions instead of Group.permissions. This is the bug report.
And there is another bug regarding the use of #ElementCollection here.

JPA CriteriaQuery, querying multiple fields not returning results as expected

I have a basic message board type system I'm developing in a spring based webapp. I'm using spring 4.1.3 and hibernate 4.3.7. I'm attempting to build a "universal" search bar to find posts that contain a specified value in one of many fields. For starters, here is my post domain object:
#Entity
public class Post implements Serializable {
#ManyToMany(fetch = FetchType.EAGER, cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
#JoinTable(
name = "POST_KEYWORD",
joinColumns = {#JoinColumn(name = "POST_ID", referencedColumnName = "POST_ID")},
inverseJoinColumns = {#JoinColumn(name = "KEYWORD_ID", referencedColumnName = "ID")}
)
private Set<Keyword> keywords = new HashSet<>();
The author, category, and keyword objects are all very boring and all basically look like this:
#Entity
public class Keyword implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "keyword_id_seq")
#SequenceGenerator(name = "keyword_id_seq", sequenceName = "keyword_id_seq", allocationSize = 1)
private Long id;
#Column(name = "KEYWORD_VALUE")
private String value;
private int count = 1;
}
The author and category classes don't have the int count, just simply id and value fields.
Here is my PostDao:
public List<Post> universalFind(String value, int maxResults) {
// create a new string that is built for case insensitive "like" sql searches
String upperValue = "%" + value.toUpperCase() + "%";
final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
// build the criteria query and get the path for the category value
CriteriaQuery<Post> postQuery = criteriaBuilder.createQuery(Post.class).distinct(true);
Root<Post> postRoot = postQuery.from(Post.class);
List<Predicate> predicates = new ArrayList<>();
// find titles containing the value
Predicate titlePredicate = criteriaBuilder.like(criteriaBuilder.upper(postRoot.get(Post_.title)), upperValue);
predicates.add(titlePredicate);
// find keywords containing the value
SetJoin<Post, Keyword> postKeywords = postRoot.join(Post_.keywords);
Path<String> keywordPath = postKeywords.get(Keyword_.value);
Predicate keywordPredicate = criteriaBuilder.like(criteriaBuilder.upper(keywordPath), upperValue);
predicates.add(keywordPredicate);
select.where(criteriaBuilder.or(predicates.toArray(new Predicate[]{})));
TypedQuery typedQuery = entityManager.createQuery(select);
List<Post> resultList = typedQuery.getResultList();
return resultList;
}
It all works as expected when all the data is populated in a Post object. If I have two posts with "Test" in the title, and I search for "test", I will get both posts back. However, if I remove all of the keywords from one of the posts, that post no longer shows up in the search results. I can use pgadmin to verify it is in the database.
If I comment out the SetJoin section for the keyword in my DAO, the posts without keywords will suddenly appear in my searches.
Is there some way to prevent this, so that I can have a post without keywords and it will still show up in the results if the search term matches any one of the other fields?
EDIT: I cut this down to the bare minimum Frankenstein code I possibly could

Hibernate Criteria conditions or subqueries on OneToMany property values

One component of my application logs the parameters that an User send with an http URL. The mapping is as follow:
public class ActivityLog {
#Column(name = "id")
private Long id;
#OneToMany(mappedBy="activityLog", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
protected List<ActivityLogParameter> activityLogParameters = new ArrayList<ActivityLogParameter>();
}
public class ActivityLogParameter {
#Column(name = "id")
private Long id;
#Column(name = "key", length=10)
protected String key;
#Column(name = "value", length=50)
protected String value;
#ManyToOne(fetch = FetchType.LAZY, cascade={CascadeType.MERGE})
#JoinColumn(name="activity_log_id")
protected ActivityLog activityLog;
}
Let assume with every URL always 2 parameters are being passed: U and L
I need to create a query using hibernate's Criteria (mandatory from specification) so that it returns me all the ActivityLogs where both parameters matche a certain value. i.e.: U=aaa and L=bbb
I tried like this:
Criteria criteria = getCurrentSession().createCriteria(ActivityLog.class, "al");
// create alias
criteria = criteria.createAlias("activityLogParameters", "alp",JoinFragment.LEFT_OUTER_JOIN);
// create transformer to avoid duplicate results
criteria = criteria.setResultTransformer(CriteriaSpecification.DISTINCT_ROOT_ENTITY);
criteria = criteria.setFetchMode("al.activityLogParameters", FetchMode.SELECT);
//filters
criteria = criteria.add(Restrictions.or(Restrictions.eq("alp.value", UValue), Restrictions.ne("alp.key", "L")));
criteria = criteria.add(Restrictions.or(Restrictions.eq("alp.value", LValue), Restrictions.ne("alp.key", "U")));
But here I'm stuck. I tried to add projections like distinct and group by on this but it's not enough to have a correct result.
I'm trying also to use this criteria as a sub criteria, so to count the number of rows for any ActivityLog and keep only the records that have count(*) = 2 (all parameters match the condition) but I can't find how to do it with Subqueries.
Any idea on how to solve the above problem? In SQL I would do something like this:
select activity_log_id from (
select count(*) as ct, activity_log_id
from activity_log_parameter alp inner join activity_log al on alp.activity_log_id=al.id
where (alp.value='visitor' or alp.key<>'U')
and (alp.value='room1' or alp.key<>'L')
group by activity_log_id
) as subq
where subq.ct = 2
Thanks
Solved using a sub query
DetachedCriteria subquery = DetachedCriteria.forClass(ActivityLogParameter.class, "alp")
.createAlias("activityLog", "al",JoinFragment.LEFT_OUTER_JOIN)
.setProjection(Projections.projectionList()
.add(Projections.count("id"), "alpId"));
subquery = subquery.add( Property.forName("al.id").eqProperty("mainAl.id") );
subquery = subquery.add(Restrictions.or(Restrictions.eq("alp.value", UValue), Restrictions.ne("alp.key", "L")));
subquery = subquery.add(Restrictions.or(Restrictions.eq("alp.value", LValue), Restrictions.ne("alp.key", "U")));
Criteria criteria = getCurrentSession().createCriteria(type, "mainAl");
criteria = criteria.add(Subqueries.eq(new Long(2), subquery));

JPA 2.0 CriteriaBuilder help - How do I select the Greatest(max) value that matches a certain where query?

Sorry for this rather basic question, but I have to get some sort of prototype working very quickly and this is my first foray into JPA.
I have a class, System that has a List of Snapshot items, each has a numeric ID, and a SystemID.
How do I query Snapshots to say something like:
select top 1 ID from Snapshots
where Snapshots.SystemID = X
order by Snapshots.ID desc;
I know how to put the where query in, not sure where to put my "greatest" bit.
Thanks!!
public Snapshot GetMostRecentSnapshotByID(int systemID) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<mynamespace.Snapshot> criteria =
cb.createQuery(mynamespace.Snapshot.class);
Root<mynamespace> snapshot = criteria.from(mynamespace.Snapshot.class);
criteria.where(cb.equal(snapshot.get(Snapshot_.systemID), systemID));
//OK -- where does this guy go?
cb.greatest(snapshot.get(Snapshot_.id));
return JPAResultHelper.getSingleResultOrNull(em.createQuery(criteria));
}
Clarification: I have the following (snippet) of my snapshot class
#
Entity
public class Snapshot implements Serializable {
#Id
#GeneratedValue
private int id;
#ManyToOne
#JoinColumn(name = "systemID", nullable = false)
private System system;
Can I query against a numerical id, vs using a System object, to find a particular System's snapshots?
Sorry if that was confusing!
You are a bit confused about jpa working with entities and properties instead of tables and columns; if you are learning I suggest you to first try to implement your query using jpql, something like:
String q = "from Snapshot s where s.systemID = :systemID order by s.id desc";
TypedQuery<Snapshot> query = em.createTypedQuery(q, Snapshot.class);
query.setParameter("systemID", systemID);
return query.getFirstResult();
// return a Snapshot object, get the id with the getter
(it would have been better to map (#OneToMany) Snapshot to System entity instead of using primitive ID)
then you could make a try with CriteriaBuilder (not using metamodel here):
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Object> cq = cb.createQuery();
Root<Snapshot> r = cq.from(Snapshot.class);
cq.where(cb.equal(r.get("systemID"), systemID));
cd.orderBy(cb.desc(r.get("id")));
em.createQuery(cq).geFirsttResult();
if you wanted to make a where...and... (but it's not your case in this question), it would have been:
[...]
Predicate p1 = cb.equal(r.get("systemID"), systemID));
Predicate p2 = cb. /* other predicate */
cb.and(p1,p2);
[...]
EDIT:
Can I query against a numerical id, vs using a System object, to find
a particular System's snapshots?
Sure, you can do it like that (given that System has an #Id property named id):
String q = "from Snapshot s where s.system.id = :systemID order by s.id desc";
[...]
where s.system.id means: property id (integer) of the property system (class System) of s (Snapshot).
Or, if you had the System entity, you could compare directly the objects:
String q = "from Snapshot s where s.system = :system order by s.id desc";
query.setParameter("system", system);
[...]
Using CriteriaBuilder (and metamodel):
Metamodel m = em.getMetamodel();
Root<Snapshot> snapshot = cq.from(Snapshot.class);
Join<Snapshot, System> system = snapshot.join(Snapshot_.system);
cq.where(cb.equal(System_.id, systemID));
[...]

Categories