JPA Query: join a subquery with grouping condition - java

I have an entity that represents a change event for a specific object. Something like:
#Entity
public class Event {
#Id
private String eventId;
private String objectId;
private Instant creationDate;
// other fields, getters, setters
}
There might be several event objects for a specific objectId.
Now I need to query all latest events for a each objectId (those that have max creationDate groping by objectId) .
If it was pure SQL I would write the following query:
SELECT event.*
FROM
event event
JOIN (
SELECT
e.object_id object_id,
MAX(e.creation_date) last_date
FROM event e
GROUP BY e.object_id
) latest_event
ON latest_event.object_id = event.object_id
AND event.creation_date = latest_event.last_date
But the similar join unfortunately doesn't work in JPA query.
Question: How to join a subquery in a JPA query?
Using a native query is not an option in my case, because I use Spring Data JPA repository with pagination functionality which doesn't work for native queries.

#Query(
value = "SELECT e FROM Event e " +
"WHERE e.creationDate = " +
"(SELECT max(e2.creationDate) FROM Event e2 " +
"WHERE e2.objectId = e.objectId)"
)

SELECT *
FROM Event
NATURAL JOIN
(SELECT object_id, MAX(creation_date) AS creation_date
FROM Event
GROUP BY object_id) groupedEvent
if two equal max creation_date be in the same object_id
SELECT ev.*
FROM Event ev
INNER JOIN
(SELECT max(id) AS id
FROM Event e
INNER JOIN
(SELECT object_id, MAX(creation_date) AS last_date
FROM Event GROUP BY object_id
) groupedEvent
ON e.object_id = groupedEvent.object_id
AND e.creation_date = groupedEvent.last_date
GROUP BY e.object_id) dist
ON ev.id = dist.id;
```

Related

The problem that jpa custom query does not take effect

I am designing a 'like' function for a blog system, and I want to judge whether the likes have been added/applied according to the user's login status when querying the content of the homepage.
So I have created a SQL statement to implement the left join to the query to achieve this function. I used the JPA #Query jpql method to customize the query, but the effect is different from what I expected.
When looking at the statement, I found that the jpql statement is indeed executed once, but it is an associated query to another/subsequent jpa query after the initial query was executed, so I think it may be overwritten.
I am using jpa for the first time and they may be thing that I do not fully understand, please feel free to ask any further questions to help me and thank you.
This is my expected sql statement:
SELECT a.*,l.*FROM article a LEFT JOIN user_like_record l ON a.id=l.target_id AND l.target_type=0 AND l.user_id=17
This is my current code:
#Query(value = "SELECT\n" +
"\tarticle\n" +
"FROM\n" +
"\tArticle article LEFT JOIN article.userLikeRecord ON article.userLikeRecord.targetType = 0\n" +
"\t AND article.userLikeRecord.userId = :userId")
Page<Article> findAllLikeStatusb(Long userId, Pageable pageable);
#OneToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "id", referencedColumnName = "target_id", insertable = false, updatable = false)
private UserLikeRecord userLikeRecord;
This is the sql statement generated by the last query of jpa:
Hibernate:
select
user0_.id as id1_9_,
user0_.create_time as create_t2_9_,
user0_.delete_time as delete_t3_9_,
user0_.update_time as update_t4_9_,
user0_.email as email5_9_,
user0_.mobile as mobile6_9_,
user0_.nick_name as nick_nam7_9_,
user0_.openid as openid8_9_,
user0_.receive_like_counts as receive_9_9_,
user0_.unify_uid as unify_u10_9_,
user0_.user_birthday as user_bi11_9_,
user0_.wx_profile as wx_prof12_9_
from
user user0_
where
(
user0_.delete_time is null
)
and user0_.openid=?
Hibernate:
SELECT
a.*,
l.*
FROM
article a
LEFT JOIN
user_like_record l
ON a.id = l.target_id
AND l.target_type=0
AND l.user_id =?
order by
a.create_time desc limit ?
The following sql is executed multiple times
Hibernate:
select
userlikere0_.target_id as target_i1_10_0_,
userlikere0_.create_time as create_t2_10_0_,
userlikere0_.delete_time as delete_t3_10_0_,
userlikere0_.update_time as update_t4_10_0_,
userlikere0_.id as id5_10_0_,
userlikere0_.like_status as like_sta6_10_0_,
userlikere0_.target_type as target_t7_10_0_,
userlikere0_.user_id as user_id8_10_0_
from
user_like_record userlikere0_
where
userlikere0_.target_id=?
and (
userlikere0_.delete_time is null
)

Named query for entity that does not associate with a specific table

I'm using Quarkus and Hibernate / Panache.
For this example, I have 3 tables (table_a, table_b, table_c) that I am joining together using a native query. In the project I'm working on, it's around 5 JOIN tables to retrieve the information I'm looking for.
table_b is purely a mapping / join table for table_a and table_c:
SELECT
a.id,
a.name,
c.login_date
FROM
table_a a
JOIN table_b b ON b.a_id = a.id
JOIN table_c c ON b.c_id = c.id
WHERE
c.login_date > '01-MAY-21'
I'm porting the above to HQL. I've mapped all my #Entity classes with their respective #Table, along with their #Column names. We're good in that department.
SELECT
a.id,
a.name,
c.loginDate
FROM
TableA a
JOIN TableA b ON b.aId = a.id
JOIN TableB c ON b.cId = c.id
WHERE
c.loginDate > '01-MAY-21'
I'm only looking for name and login_date. There is a bunch of other information stored in table_a and table_c that I don't want for this specific query. So I created an entity for this call:
#Entity
#IdClass(LoginDetailsPk.class)
#NamedQuery(
name = "LoginDetails.findFromDate",
query = "FROM TableA a " +
"JOIN TableA b ON b.aId = a.id " +
"JOIN TableB c ON b.cId = c.id " +
"WHERE c.loginDate > '01-MAY-21'"
)
public class LoginDetails extends PanacheEntityBase {
#Id
private int id;
#Id
private String name;
#Id
private String loginDate;
public static List<LoginDetails> findFromDate(String fromDate) {
// Eventually pass fromDate into find()
return find("#LoginDetails.findFromDate").list();
}
}
I'm having a hard time trying to understand why the return even works. When I invoke LoginDetails.findFromDate(...) and store it in a List<LoginDetails>, it works fine. However, when I try to access the list, I get a ClassCastException error.
List<LoginDetails> details = LoginDetails.findFromDate(null);
for(LoginDetails detail : details) { // <------ Throws a class cast exception
//...
}
After debugging, I'm noticing that generic type stored in my List isn't even my LoginDetails class; rather, it's an array of objects (List<Object[]>) with all my #Entities and the irrelevant information I'm not looking for.
I'm lost. Would it make more sense to move back to a native query?
Your HQL is creating a Object[] for every row in the result, because you are not specifying any SELECT, and by default all the objects in the FROM clause are included in that Object array. If you want to return a LoginDetails object you need to create a constructor with all the attributes:
public LoginDetails(int id, String name, String loginDate) {
this.id = id;
this.name = name;
this.loginDate = loginDate;
}
And then change the query to:
query = "SELECT new LoginDetails(a.id, a.name, c.loginDate) "
"FROM TableA a " +
"JOIN TableA b ON b.aId = a.id " +
"JOIN TableB c ON b.cId = c.id " +
"WHERE c.loginDate > '01-MAY-21'"
See https://docs.jboss.org/hibernate/core/3.5/reference/en/html/queryhql.html#queryhql-select

How to load #ElementCollection within custom query?

I want to reduce the amount of querys run by spring. When getting an object with #ElementCollection via SQL I want to get the data for the ElementCollections directly via a JOIN within the quers.
The attribute with the ElementCollection
#ElementCollection(fetch = FetchType.EAGER)
#JoinTable(name = "song_genre_list")
#org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
private List<String> genre;
The Method that uses a custom string:
#Query(
value = "select distinct s.*, g.* from musicdb.songs s left join musicdb.song_genre_list g on s.id = g.song_id where s.name like ?1 or s.artist like ?1",
nativeQuery = true)
List<Song> searchSong(String title);
How I would imagine defining a query that also loads this element collection:
SELECT DISTINCT s.*, g.* FROM musicdb.songs s LEFT JOIN musicdb.song_genre_list g ON s.id = g.song_id WHERE s.name LIKE ?1 OR s.artist LIKE ?1
What Spring currently does (loading the genres for 3 songs with more querys):
Hibernate: select distinct s.*, g.* from musicdb.songs s left join musicdb.song_genre_list g on s.id = g.song_id where s.name like ?1 or s.artist like ?1
Hibernate: select genre0_.song_id as song_id1_4_0_, genre0_.genre as genre2_4_0_ from musicdb.song_genre_list genre0_ where genre0_.song_id=?
Hibernate: select genre0_.song_id as song_id1_4_0_, genre0_.genre as genre2_4_0_ from musicdb.song_genre_list genre0_ where genre0_.song_id=?
Hibernate: select genre0_.song_id as song_id1_4_0_, genre0_.genre as genre2_4_0_ from musicdb.song_genre_list genre0_ where genre0_.song_id=?
What I want Spring to do:
Hibernate: select distinct s.*, g.* from musicdb.songs s left join musicdb.song_genre_list g on s.id = g.song_id where s.name like ?1 or s.artist like ?1
The required Data for the ElementCollection is already included with the join. How can I tell spring to import that Data?
FetchType.EAGER will already load the genres for you, so you dont need to write a join in your original query. But for that hibernate hibernate uses separate queries by default. To change this add the annotation '#Fetch(FetchMode.JOIN)' on the genre field

JPQL include elementCollection map in select statement

I have an #Entity class Company with several attributes, referencing a companies Table in my db. One of them represents a Map companyProperties where the companies table is extended by a company_properties table, and the properties are saved in key-value format.
#Entity
#Table(name = "companies")
public class Company extends AbstractEntity {
private static final String TABLE_NAME = "companies";
#Id
#GeneratedValue(generator = TABLE_NAME + SEQUENCE_SUFFIX)
#SequenceGenerator(name = TABLE_NAME + SEQUENCE_SUFFIX, sequenceName = TABLE_NAME + SEQUENCE_SUFFIX, allocationSize = SEQUENCE_ALLOCATION_SIZE)
private Long id;
//some attributes
#ElementCollection
#CollectionTable(name = "company_properties", joinColumns = #JoinColumn(name = "companyid"))
#MapKeyColumn(name = "propname")
#Column(name = "propvalue")
private Map<String, String> companyProperties;
//getters and setters
}
The entity manager is able to perform properly find clauses
Company company = entityManager.find(Company.class, companyId);
However, I am not able to perform JPQL Queries in this entity and retrieve the Map accordingly. Since the object is big, I just need to select some of the attributes in my entity class. I also do not want to filter by companyProperties but to retrieve all of them coming with the proper assigned companyid Foreign Key. What I have tried to do is the following:
TypedQuery<Company> query = entityManager.createQuery("SELECT c.id, c.name, c.companyProperties " +
"FROM Company as c where c.id = :id", Company.class);
query.setParameter("id", companyId);
Company result = query.getSingleResult();
The error I get is:
java.lang.IllegalArgumentException: An exception occurred while creating a query in EntityManager:
Exception Description: Problem compiling [SELECT c.id, c.name, c.companyProperties FROM Company as c where c.id = :id]. [21, 40] The state field path 'c.companyProperties' cannot be resolved to a collection type.
org.eclipse.persistence.internal.jpa.EntityManagerImpl.createQuery(EntityManagerImpl.java:1616)
org.eclipse.persistence.internal.jpa.EntityManagerImpl.createQuery(EntityManagerImpl.java:1636)
com.sun.enterprise.container.common.impl.EntityManagerWrapper.createQuery(EntityManagerWrapper.java:476)
Trying to do it with joins (the furthest point I got was with
Query query = entityManager.createQuery("SELECT c.id, c.name, p " +
"FROM Company c LEFT JOIN c.companyProperties p where c.id = :id");
does not give me either the correct results (it only returns the value of the property and not a list of them with key-value).
How can I define the right query to do this?
Your JPA syntax looks off to me. In your first query you were selecting individual fields in the Company entity. But this isn't how JPA works; when you query you get back the entire object, with which you can access any field you want. I propose the following code instead:
TypedQuery<Company> query = entityManager.createQuery("from Company as c where c.id = :id", Company.class);
query.setParameter("id", companyId);
Company result = query.getSingleResult();
Similarly, for the second join query I suggest the following code:
Query query = entityManager.createQuery("SELECT c" +
"FROM Company c LEFT JOIN c.companyProperties p WHERE c.id = :id");
query.setParameter("id", companyId);
List<Company> companies = query.getResultList();
The reason why only select a Company and not a property entity is that properties would appear as a collection inside the Company class. Assuming a one to many exists between companies and properties, you could access the propeties from each Company entity.
You are expecting to get a complete Company object when doing select only on particular fields, which is not possible. If you really want to save some memory (which in most cases would not be that much of a success) and select only some field, then you should expect a List<Object[]>:
List<Object[]> results = entityManager.createQuery("SELECT c.id, c.name, p " +
"FROM Company c LEFT JOIN c.companyProperties p where c.id = :id")
.setParameter("id", companyId)
.getResultList();
Here the results will contain a single array of the selected fields. You can use getSingleResult, but be aware that it will throw an exception if no results were found.

How do I port query with join across multiple tables from JDOQL to HQL

I am porting an application for KodoJDO to Hibernate.
I have a query that goes across 4 tables in the db, and 3 objects in the java code.
In English the query is Find the users that have entitlements in system X.
my JDOQL where clause called on the User object was
where entitlements.contains(ent) && (upper( ent.system.id ) = 'EVPN')
some sql that does the query is:
select unique(u.id)
from USER u, USERENTITLEMENT ue, ENTITLEMENT e, SYSTEM s
where u.id = ue.userid
and ue.entitlementid = e.id
and e.systemid = s.id
and s.id = 'evpn'
My best guess for HQL gives me an exception
org.hibernate.hql.ast.QuerySyntaxException: unexpected AST node: ( [select user from com.ebig.entity.User as user, com.ebig.entity.Entitlement as ent, com.ebig.entity.System as sys where entitlements.contains(ent) and ent.system = sys and sys.id = 'evpn']
the db is structured like this:
User
id
UserEntitlement
userid
entitlementid
Entitlement
id
systemid
System
id
the java code is structured as below:
class User
{
String id;
Set<Entitlement> entitlements;
}
class Entitlement
{
String id;
System system;
}
class System
{
String id;
}
Update My final query that works
hqlQuery = "select distinct user from User as user "+
"inner join user.entitlements as entitlement inner join entitlement.system as system "+
"where system.id = 'evpn' AND mod(user.flags, 2) = 0 AND source = 1";
Yes I know I should use parameters, but I have a great many problems to solve, and will post pone that code for another day.
Another variation with an implicit inner join for entitlement to system
hqlQuery = "select distinct user from User as user "+
"inner join user.entitlements as entitlement "+
"where entitlement.system.id = 'evpn' AND mod(user.flags, 2) = 0 AND source = 1";
You should use joins :
select distinct u.id from User u
inner join u.entitlements as entitlement
inner join entitlement.system as system
where system.id = :evpn
where :evpn is a named parameter that you have to bind.
You must think in terms of objects and relationships between objects when doing HQL, and not in terms of tables, foreign keys and join tables.

Categories

Resources