I have performance problems with the group by feature in Hibernate. Consider this 2 classes:
public class Project
{
...
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name="user")
private User user;
...
}
public class User
{
...
private Integer id;
private String name;
...
}
Now I try to get a list of all Users assigned to the project. But this query is uselessly slow (more than 100'000 project entries, a lot of joins):
Session session = this.sessionFactory.openSession();
String SQL="SELECT user.id as id, user.name as name FROM Project p GROUP BY p.user.id";
Query q = session.createQuery(SQL);
q.setResultTransformer(Criteria.ALIAS_TO_ENTITY_MAP);
List<Object> list = q.list();
session.close();
I try to change the query this way, but this is not working either because the variable user is an Object (but this would work as a native SQL query):
SELECT id, name FROM User WHERE id IN(SELECT user FROM Project GROUP BY user)
Any other ideas? Thank you in advance!
Try creating an index from the foreign key column.
#javax.persistence.Table(name = "project")
#Table(appliesTo = "project" ,indexes = #Index(columnNames = "user", name = "user_index"))
#Entity
public class Project
{
..
Update
columnNames has been depricated. Use columnList instead.
#Index(columnList = "user", name = "user_index")
Hope this helps.
I doubt this has to do with Hibernate being slow. Most probably the SQL query is slow if run directly on the database as well.
One good practice is to create indices whenever you have a foreign key in your table. In your case create an index for user_id on your project table and run the query once more.
If you want to get all users assigned to the project you don't need group by. Do something like this
select user from Project project inner join project.user user where project.id = :projectId
Related
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 class ReleaseNote saved in the table RELEASE_NOTES and a table RELEASE_NOTE_USER_READ with the columns RELEASE_NOTE_ID and USER_ID where I want to save which users have already read a releaseNote, so I don't show it again.
Since we are using a microservice architekture I don't have a user object in that microservice and don't want to add one. Although it won't be a problem to add a field usersRead to the ReleaseNote which contains the userId of all the users who have read the releaseNote.
I need to perform two actions:
Pass a list of releaseNoteIds to be saved in the table for a given userId, so I can keep track of the releaseNotes a user has read.
Select all releaseNotes a user has not read. The SQL-Statement for this would look like:
SELECT * FROM RELEASE_NOTES WHERE ID NOT IN (SELECT RELEASE_NOTE_ID FROM RELEASE_NOTE_USER_READ WHERE USER_ID = :userId)
How can I achieve this using the EntityManager or Hibernate specific functionality?
Found the solution myself. Add field Set usersRead to ReleaseNote and annotate like the following:
#ElementCollection
#CollectionTable(name = "RELEASE_NOTES_USER_READ",
joinColumns = #JoinColumn(name = "RELEASE_NOTE_ID"))
#Column(name = "USER_ID", table = "RELEASE_NOTES_USER_READ")
private Set<Integer> usersRead = new HashSet<>();
I need to select products based on tags, here are the tables
products: productId, name, description, price, etc
tags: tagId, name
product_tags: productId, tagId
and I have 2 classes Product and Tag and relation is specified in Product class
#OneToMany(cascade = CascadeType.DETACH)
#JoinTable(
name = "product_tags",
joinColumns = #JoinColumn(name = "productId"),
inverseJoinColumns = #JoinColumn(name = "tagId")
)
public List getTags() {
return tags;
}
public void setTags(List tags) {
this.tags = tags;
}
Please note I only want to select products and not tags. following works fine
Criteria cri = getSession().createCriteria(Product.class);
cri.setFirstResult(index);
cri.setMaxResults(limit);
return cri.list();
As I am trying to get list for pagination, so I need total number of pages that can be retrieved by getting totalRecords/recordPerPage
Criteria cri = getSession().createCriteria(Product.class);
//add any required filter to criteria
//e.g cri.add(Restrictions.like("name", keyword, MatchMode.ANYWHERE));
//********** Following code is in utility function ******************/
//Get total number of records matching criteria
cri.setProjection(Projections.rowCount());
Long totalRecords = (Long)cri.uniqueResult();
//Get paginated records
cri.setProjection(null);// This is evil but works
cri.setFirstResult(index);
cri.setMaxResults(limit);
paginatedRecords = cri.list();
Question 1: Is it possible to set some thing like cri.setProjection(Product.class) instead of setting it null, I am aware that I can create a projection list and add all the column of product class but that seems overkill + the common part is in utility function and I found no way to retrieve the previous projection. cri.getProject()
Why I need another method because cri.setProjection(null) fails when I apply join, because it will project all the column of products, tags, product_tags. which cannot be casted to List
Get all products that have associated tag ids as (4,5,6)
cri.createAlias("tags", "t");
cri.add(Restrictions.in("t.tagId", new Integer[]{4,5,6}));
Here is the issued query
select
this_.productId as productId1_9_1_,
this_.categoryId as category6_9_1_,
this_.description as descript8_9_1_,
this_.name as name13_9_1_,
tags3_.productId as productId1_9_,
t1_.tagId as tagId2_10_,
t1_.tagId as tagId1_11_0_,
t1_.name as name4_11_0_,
from
products this_
inner join
product_tags tags3_
on this_.productId=tags3_.productId
inner join
tags t1_
on tags3_.tagId=t1_.tagId
where t1_.tagId in (4,5,6) limit 25
I have found a work-around for this as follows
cri.setProjection(null)
criteria.setResultTransformer(CriteriaSpecification.DISTINCT_ROOT_ENTITY)
This will fix type cast exception.
Question 2: The back-end query is still the same, it join and project all the columns of all involved tables. ((Yakkh dirty)), Why the same query?, I am expecting projections on Product class only
I have a problem with hibernate native sql join query. My query is below and works on Mysql db.
SELECT c.cart_id, u.name, u.surname, c.totalPrice
FROM sandbox.cart c JOIN
sandbox.user u
ON u.id = c.placedBy
I am using hibernate in code and encountered an exception
java.sql.SQLException: Column 'id' not found.
com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1055)
com.mysql.jdbc.SQLError.createSQLException(SQLError.java:956)
com.mysql.jdbc.SQLError.createSQLException(SQLError.java:926)
com.mysql.jdbc.ResultSetImpl.findColumn(ResultSetImpl.java:1093)
Query in code here
Session session = hibernateUtil.getSessionFactory().getCurrentSession();
SQLQuery query = session.createSQLQuery(ORDER_PER_USER_QUERY);
query.addEntity(OrderPerUser.class);
return query.list();
Table column name
Cart
| cart_id | placedBy | totalPrice
User
| id | email | name | surname
My mapped class is
#Entity
public class OrderPerUser {
#Id
private long id;
private String name;
private String surName;
private long cartId;
private double totalPrice; }
You need to remove the line:
query.addEntity(OrderPerUser.class);
After that, you need to rewrite the code and map your object manually, because your OrderPerUser is not an entity:
Session session = hibernateUtil.getSessionFactory().getCurrentSession();
SQLQuery query = session.createSQLQuery(ORDER_PER_USER_QUERY);
List<OrderPerUser> returnList new ArrayList<>();
for(Object[] row : query.list()){
OrderPerUser orderPerUserObj = new OrderPerUser();
oderPerUserObj.setCartId(Long.parseLong(row[0].toString()));
//put other properties here
returnList.add(orderPerUserObj);
}
return returnList;
Edit1: Now I see that you added the mapped class, but OrderPerUser should not be an entity in your case, but a regular DTO. An entity requires an ID, but you can't select the ID in this case, because OrderPerUser is not part of a table, it is just some selected data that you want in your memory and not in the database. So you should make your OrderPerUser a regular data transfer object.
Please read about entities, data transfer objects, data access objects to see what each object should do.
My guess is that your OrderPerUser class which you try to use for collecting the result is expecting a column with name id, and you have no such column in your query...
Try using the query:
SELECT u.id, c.cart_id, u.name, u.surname, c.totalPrice
FROM sandbox.cart c
JOIN sandbox.user u ON u.id = c.placedBy
I have 3 classes, I am trying to get a list of all the events of an eventhost that a user is subscribed to. I am probably thinking way too complicated but I have very little experience with JPA/HQL.
User class
#ManyToMany
#JoinTable(name = "Subscriptions", joinColumns = #JoinColumn(name = "user_id", referencedColumnName = "id") , inverseJoinColumns = #JoinColumn(name = "event_host_id", referencedColumnName = "id") )
private List<EventHost> subscriptions;
EventHost class
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn(name = "event_host_id", referencedColumnName = "id", updatable = true)
private List<Event> events;
I tried using this query, but it tells me that subscriptions is not mapped, which it is not since it's not a java class.
String hql = "SELECT o FROM Event WHERE event_host_id IN (SELECT a FROM EventHost WHERE id IN(SELECT b FROM User WHERE = + " + userid + "))";
I know injecting the userid like this is bad practice, I'm just doing it for testing purposes.
Please ask if you need something more, I would really like to understand how to write a query for this.
This question should really be HQL with two join tables, but I'll let you change it. Since its HQL, or JPA, it's database independent.
Anyway, any time you see a OneToMany or ManyToMany relationship you have a join table and so you should be thinking joins. It's always a good idea to look at the sql create table statements to see what's going on. In this case your user_subscriptions join table is:
create table user_subscriptions (user_id integer not null, subscriptions_id integer not null)
and your event_host_events join table is this:
create table event_host_events (event_host_id integer not null, events_id integer not null)
Nothing new there. When you're trying to get something new working that you don't intuitively understand, break it down into things you can do. For example, you can execute two queries, getting a Users subscriptions first, and then getting the Events for those subscriptions:
Query query = session.createQuery("select u.subscriptions from User u where name = :name");
query.setParameter("name", name);
List<EventHost> subscriptions = query.list();
List<Event> events = new ArrayList<Event>();
Query query2 = session.createQuery("select s.events from EventHost s where id = :id");
for (EventHost s: subscriptions ) {
query2.setParameter("id", s.getId());
events.addAll( query2.list());
}
Not elegant, but it works. Then, keeping join in mind, figure out how to make one statement out of the two of them.
Query query = session.createQuery("select s.events from User u join u.subscriptions s where u.name = :name)");
query.setParameter("name", name);
return query.list();
The join will use an inner join by default, so you're ok there. The JPA provider will auto-magically join your three Entity tables and two Join Tables for you:
select
event4_.id as id1_2_
from user user0_
inner join user_subscriptions subscripti1_ on user0_.id=subscripti1_.user_id
inner join event_host eventhost2_ on subscripti1_.subscriptions_id=eventhost2_.id
inner join event_host_events events3_ on eventhost2_.id=events3_.event_host_id
inner join event event4_ on events3_.events_id=event4_.id
where user0_.name=?
Aren't you glad you don't have to write that query?