Hibernate query with #Formula-annotated attribute in order by clause - java

I have 2 entities:
#Entity
class Order {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<Contract> contracts= new HashSet<>();
#Formula("(select count(m.ORDER_ID) from myschema.ORDER_CONTRACTS m where m.ORDER_ID = id)")
private Integer numberOfContracts; // this is basically contracts.size()
}
and
#Entity
class Contract {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String client;
// some other properties
}
When I want to get my orders ordered by numberOfContracts, hibernate generates this query for me
SELECT order0_.id AS id1_5_,
(SELECT COUNT(m.ORDER_ID)
FROM myschema.ORDER_CONTRACTS m
WHERE m.ORDER_ID = order0_.id) AS formula1_
FROM myschema.order order0_
ORDER BY (SELECT COUNT(m.ORDER_ID)
FROM myschema.ORDER_CONTRACTS m
WHERE m.ORDER_ID = order0_.id) DESC
and fails with
com.ibm.db2.jcc.am.SqlSyntaxErrorException: DB2 SQL Error: SQLCODE=-206, SQLSTATE=42703, SQLERRMC=ORDER0_.ID, DRIVER=4.27.25
When I replace the select in the ORDER BY with formula1_ like this:
SELECT order0_.id AS id1_5_,
(SELECT COUNT(m.ORDER_ID)
FROM myschema.ORDER_CONTRACTS m
WHERE m.ORDER_ID = order0_.id) AS formula1_
FROM myschema.order order0_
ORDER BY formula1_ DESC
I get the expected result.
Is there a way to tell hibernate to use the generated alias (formula1_) instead of replicating the formula in the order by?
EDIT:
How I get my query:
I'm using an org.springframework.web.bind.annotation.RestController. This controller offers a endpoint to get all Orders by a method like this:
#GetMapping("orders")
public List<Order> getOrders(Pageable pageable);
When I send a request like http://localhost:8080/api/orders/sort=numberOfContracts,desc&size=100&page=0
to the endpoint, the pageable contains the information about ordering. My contoller then calls my
public interface OrderRepository extends PagingAndSortingRepository<Order, Integer>
witch provides this method:
Page<Order> findAll(Pageable page);
After this point spring and hibernate do their magic.

What kind of HQL query are you using. Hibernate will just do what you tell it to do. You will have to use the HQL alias as well in the order by clause if you want the SQL alias to be used:
SELECT o.id, o.numberOfContracts as num
FROM Order o
ORDER BY num desc

Related

Spring JPA projection problem with subquery

I'm using JPA projections but it fails when my query contains subquery.
For exemple :
public class Student {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "student_id")
private Long id;
private String name;
}
Here is the interface for projection :
public interface StudentProjection {
Integer getId();
}
And repository :
#Query("select s from Student s where s.id = 88831")
List<StudentProjection> findProjections();
With this code everything works fine, and repo returns a list of EleveProjection.
But if I change query to this (i.e. fetching id by subselect - just for example):
#Query("select s from Student s where s.id = (select max(88831) from Student)")
List<StudentProjection> findProjections();
... method findProjections() returns a list of org.springframework.data.jpa.repository.query.AbstractJpaQuery$TupleConverter$TupleBackedMap
And obviously it's not possible to execute getId() on this object (exception thrown).
It looks like a bug in Spring data JPA.
I'm currently using version 2.2.4.RELEASE.
Any idea on how to use subqueries with JPA projections ?
Thank you
The content of #Query respect Jpql spec so it is not about spring data.
Try this
#Query("select s from Student s where s.id in (select max(88831) from Student)")
List<StudentProjection> findProjections();
AFAIK you can use clauses exists and in with subqueries in jpql...

How to map a projection query to a DTO which has a #ManyToMany #JoinTable property by JPQL

I have an Blog entity like:
#Entity
class Blog{
#Id
private Long id;
private String titie;
private String content;
#ManyToMany
#JoinTable(
name = "blog_tag_association",
joinColumns = #JoinColumn(name = "blog_id"),
inverseJoinColumns = #JoinColumn(name = "tag_id")
)
private Set<Tag> tags = new LinkedHashSet<>();
// ...
}
As you can see, i use a separate table to represent the relationship between Blog and Tag.
I've read this post and write my custom JPQL query to map my query result to DTO:
class BlogSummaryDTO{
private Long id;
private String title;
private Set<Tag> tags;
public BlogSummaryDTO(Long id, String title, Set<Tag> tags){
this.id = id;
this.title = title;
this.tags = tags;
}
}
And my custom JPQL like:
String query = "SELECT new com.okatu.rgan.blog.model.BlogSummaryDTO(" +
"b.id, b.title, " +
"b.tags) FROM Blog b";
It cannot work, the terminal told me that:
Caused by: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax;
check the manual that corresponds to your MySQL server version for the right syntax to use near 'as col_2_0_ from blog blog0_ inner join blog_tag_association tags1_ on blog0_.id' at line 1
The sql it generate was like:
Hibernate:
select
blog0_.id as col_0_0_,
blog0_.title as col_1_0_,
. as col_2_0_
from
blog blog0_
inner join
blog_tag_association tags1_
on blog0_.id=tags1_.blog_id
inner join
tag tag2_
on tags1_.tag_id=tag2_.id
where
blog0_.title like ?
or blog0_.title like ? limit ?
If i remove the tags property from the JPQL and DTO's constructor's parameter list, it works well.
So how should i write my custom JPQL to construct the DTO with #JoinTable property?
I've also observed the jpa-generated sql when do the BlogRepository::findById, it use a separate native sql to retrieve the Tag.
Hibernate:
select
blog0_.id as id1_0_0_,
blog0_.content as content2_0_0_,
blog0_.created_time as created_3_0_0_,
blog0_.modified_time as modified4_0_0_,
blog0_.summary as summary5_0_0_,
blog0_.title as title6_0_0_,
blog0_.author_id as author_i9_0_0_,
user1_.id as id1_5_1_,
user1_.created_time as created_2_5_1_,
user1_.username as username7_5_1_,
from
blog blog0_
left outer join
user user1_
on blog0_.author_id=user1_.id
where
blog0_.id=?
Hibernate:
select
tags0_.blog_id as blog_id1_1_0_,
tags0_.tag_id as tag_id2_1_0_,
tag1_.id as id1_4_1_,
tag1_.description as descript2_4_1_,
tag1_.title as title3_4_1_
from
blog_tag_association tags0_
inner join
tag tag1_
on tags0_.tag_id=tag1_.id
where
tags0_.blog_id=?
But i don't know what's the JPQL it generated.

hibernate select new object constructor

I am currently using JPA development project, but encountered a problem, please enlighten
Example:
#Query("
SELECT
new CustomerMaintain(c.content, u, c.createTime, c.updateTime)
FROM
CustomerMaintain c
JOIN
c.user u
WHERE
c.delFlag = FALSE AND c.customer.id = :customerId
")
CustomerMaintain.class DTO:
#Entity
#Table(name = "t_customer_maintain")
#DynamicUpdate
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler", "delFlag"})
public class CustomerMaintain {
public CustomerMaintain() {
}
public CustomerMaintain(String content, User user, Date createTime, Date updateTime) {
this.content = content;
this.user = user;
this.createTime = createTime;
this.updateTime = updateTime;
}
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#Column(columnDefinition = "VARCHAR(512) DEFAULT ''")
private String content;
#ManyToOne(optional=false)
private User user;
#ManyToOne(optional=false)
private Customer customer;
#Column(columnDefinition = "TINYINT(1) DEFAULT FALSE ",insertable = false,updatable = false)
private Boolean delFlag;
#Column(columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP",insertable = false,updatable = false)
private Date createTime;
#Column(columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP",insertable = false,updatable = false)
private Date updateTime;
// omission getter setter method
}
I think there is only one sql query:
SELECT
c.content,
u.id,
u.name,
u.head,
c.createTime,
c.updateTime
FROM
t_customerMaintain c
INNER JOIN
t_user u ...
But hibernate query result is this:
select
customerma0_.content as col_0_0_,
user1_.id as col_1_0_, /* only user.id fields */
customerma0_.createTime as col_2_0_,
customerma0_.updateTime as col_3_0_
from
t_customer_maintain customerma0_
inner join
t_user user1_
...
select
user0_.id as id1_4_0_,
user0_.head as head4_4_0_,
user0_.name as name5_4_0_
from
t_user user0_
where
user0_.id=? /* user1_.id as col_1_0_ */
Question:
Why the first SQL query only user.id, and not user.id, user.name, user.head?
The last SQL query is superfluous.
I try to write like this:
SELECT
new CustomerMaintain(c.content, new User(u.id, u.name, u.head), c.createTime, c.updateTime)
But doing so throws something abnormally: new User(u.id, u.name, u.head)
Please help me, this problem has been bothering me for a long time.
Could you explain why you think you need the NEW operator? The type of c is already CustomerMaintain, so SELECT c is enough.
If you want to explicitly fetch the User, use JOIN FETCH instead of JOIN. This way, the extra query for User will not be executed. Alternatively, you could use Hibernate's #Fetch(FetchMode.JOIN) to force Hibernate to use a join query (instead of a separate select query) to load CustomerMaintain.user whenever needed.
If you need finer-grained control over which properties get populated in the query result, use a fetch graph or a load graph as a query hint. Don't use NEW with entity constructors as results returned from such queries will not become managed.

Get collections within an Entity when mapping it to DTO using Transformers

I have an Entity called Student
#Entity
#Table(name = "students")
public class Student implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "STUDENT_ID")
private Integer studentId;
#Column(name = "STUDENT_NAME", nullable = false, length = 100)
private String studentName;
#OneToMany(fetch = FetchType.EAGER, mappedBy = "student", cascade = CascadeType.ALL)
private List<Note> studentNotes;
// Some other instance variables that are not relevant to this question
/* Getters and Setters */
}
and an entity called as Note
#Entity
#Table(name = "notes")
public class Note implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "NOTE_ID")
private Integer noteId;
#Column(name = "NOTE_CONTENT")
private String noteText;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "STUDENT_ID")
private Student student;
/* Getters and Setters */
}
As you can see the relationship dictates that a Student can have multiple number of notes.
For displaying some information about the student on a particular page I need only the studentName, count of notes and all the notes.
I created a StudentDTO for that and it looks something like this:
public class StudentDTO {
private Long count;
private String name;
private List<Note> notes;
/* Getters and setters */
}
And I am using the following code to map the Student and Notes returned from the DB to the StudentDTO
private static void testDTO() {
Session session = getSessionFactory().openSession();
String queryString = "SELECT count(n) as count, s.studentName as name, s.studentNotes as notes " +
"from Student s join s.studentNotes n where s.id = 3";
Query query = session.createQuery(queryString);
List<StudentDTO> list = query.setResultTransformer(Transformers.aliasToBean(StudentDTO.class)).list();
for (StudentDTO u : list) {
System.out.println(u.getName());
System.out.println(u.getCount());
System.out.println(u.getNotes().size());
}
}
The above code fails when there are notes fetched in the query but if I remove the notes and get only name and count it works fine.
When notes is included in the query, this is the error that is fired by Hibernate:
select
count(studentnot2_.NOTE_ID) as col_0_0_,
. as col_3_0_,
studentnot3_.NOTE_ID as NOTE_ID1_2_,
studentnot3_.NOTE_CONTENT as NOTE_CON2_2_,
studentnot3_.STUDENT_ID as STUDENT_3_2_
from
students studentx0_
inner join
notes studentnot2_
on studentx0_.STUDENT_ID=studentnot2_.STUDENT_ID
inner join
notes studentnot3_
on studentx0_.STUDENT_ID=studentnot3_.STUDENT_ID
where
studentx0_.STUDENT_ID=3;
And this is the error message that I get:
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'as col_3_0_, studentnot3_.NOTE_ID as NOTE_ID1_2_, studentnot3_.NOTE_CONTENT as N' at line 1
Now I can see where the query is wrong but it is generated by Hibernate, not something that I have control on. Is there something that I need to change in my queryString to acheive the result that I need.
I do not want to manually map the results to my DTO, is there a way that I can directly map my studentNotes in Student.java to notes in StudentDTO.java
Looks like this query is wrong. The better way is to get just the student. You can always get collection of notes from a student.
Session session = getSessionFactory().openSession();
String queryString = from Student s where s.studentId = 3;
Query query = session.createQuery(queryString);
Student student = query.getSingleResult();
sysout(student.getNotes().size())
Also, I never retrieved collection this way in SELECT clause; so, not sure but do you really need
join s.studentNotes
in your query? Not sure if my answer is helpful.
Your query is wrong as you would need two joins to also select the count of notes, but that's not even necessary, as you could determine the count by just using the size of the notes collection.
I created Blaze-Persistence Entity Views for exactly that use case. You essentially define DTOs for JPA entities as interfaces and apply them on a query. It supports mapping nested DTOs, collection etc., essentially everything you'd expect and on top of that, it will improve your query performance as it will generate queries fetching just the data that you actually require for the DTOs.
The entity views for your example could look like this
#EntityView(Student.class)
interface StudentDTO {
#Mapping("studentName")
String getName();
#Mapping("studentNotes")
List<NoteDTO> getNotes();
default int getCount() { return getNotes().size(); }
}
#EntityView(Note.class)
interface NoteDTO {
// attributes of Note that you need
#IdMapping Integer getId();
String getNoteText();
}
Querying could look like this
StudentDTO student = entityViewManager.find(entityManager, StudentDTO.class, studentId);

How to add sub-select to select

I want to execute a query like this:
SELECT Table1.COL1,
Table1.COL2,
(SELECT SUM(Table2.COL3)
FROM Table2
WHERE Table2.UID = Table1.UID) SUMOF
FROM Table1;
How can I do it?
I usually create a Criteria add ProjectionList to it, to fill COL1 and COL2 only.
I have created a DetachedCriteria to calculate the sum...
Now, how to attach this detached criteria to the main one? My intuition says - it's some sort of Projection which needs to be added to the list, but I don't see how. Also, not sure how WHERE Table2.COL4 = Table1.COL5 of detached criteria will work.
Also, I'm sure this query might be written in different way, for example with join statement. It's still interesting if there's a way to run it like this.
DetachedCriteria and main Criteria
DetachedCriteria detachedCriteria = DetachedCriteria.forClass(Table2.class, "table2");
detachedCriteria
.setProjection(
Projections.projectionList()
.add(Projections.sum("table2.col3"), "sumCol3")
)
.add(Restrictions.eq("table2.uid", "table1.uid"))
;
Criteria criteria = session.createCriteria(Table1.class, "Table1");
criteria
.setProjection(
Projections.projectionList()
.add(Projections.property("Table1.col1"), "col1")
.add(Projections.property("Table1.col2"), "col2")
)
;
Entities (very short version)
#Entity
#Table(name = "Table1")
public class Table1 {
#Id
#Column(name = "uid")
public String getUid();
#Column(name = "col1")
public String getCol1();
#Column(name = "col2")
public String getCol2();
#Column(name = "col3")
public String getCol3();
#Column(name = "col4")
public String getCol4();
#Column(name = "col5")
public String getCol5();
}
#Entity
#Table(name = "Table2")
public class Table2 {
#Id
#Column(name = "uid")
public String getUid();
#Column(name = "col3")
public BigDecimal getCol3();
#Column(name = "col4")
public String getCol4();
#Column(name = "col5")
public String getCol5();
}
For a correlated subquery (like the one you presented above), you can use #Formula which can take an arbitrary SQL query. Then, you'll need to fetch the entity and the subquery will be executed.
However, a native SQL is more elegant if you only need this query for a single business requirement.
As for derived table queries (e.g. select from select), neither JPA nor Hibernate support derived table queries for a very good reason.
Entity queries (JPQL pr Criteria) are meant to fetch entities that you plan to modify.
For a derived table projection, native SQL is the way to go. Otherwise, why do you think EntityManager offers a createNativeQuery method?

Categories

Resources