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
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.
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.
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
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));
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));
[...]