Format for a JPA query containing an aggregation and subquery? - java

I have the following entity (some columns omitted for brevity):
#Entity
#Table(name = "INSTRUCTION")
public class Instruction {
#Id
#Column(name = "ID", nullable = false, unique = true)
public Long id;
#Column(name = "CURRENT_STATUS", nullable = false)
private InstructionState currentStatus;
#Column(name = "SUBTYPE", nullable = false)
private InstructionAction subtype;
//Getters & Setters
}
I want to write the following query in JPA to retrieve a count of the instructions grouped by their CURRENT_STATUS and SUBTYPE. I know that the following SQL works:
SELECT CURRENT_STATUS, SUBTYPE, COUNT(*) count
FROM (SELECT ID, CURRENT_STATUS, SUBTYPE
FROM INSTRUCTION
WHERE VALUE_DATE= '17-JUN-21'
AND LAST_UPDATED >= '16-JUN-21'
AND LAST_UPDATED < '17-JUN-21'
GROUP BY ID, CURRENT_STATUS, SUBTYPE)
GROUP BY CURRENT_STATUS, SUBTYPE;
I want to take the result from this query and map it to a new object call InstructionCount:
public class InstructionCount {
private InstructionState status;
private InstructionAction subType;
private Integer count;
public InstructionCount(final InstructionState status, final InstructionAction subType, final Integer count) {
this.status = status;
this.subType = subType;
this.count = count;
}
//Getters and setters
}
The Problem
I have come up with the following query in JPA for this in my repository class:
#Query(value = "SELECT new com.modles.InstructionCount(CURRENT_STATUS status, SUBTYPE subType, COUNT(*) count) \n" +
"FROM (SELECT ID, CURRENT_STATUS, SUBTYPE \n" +
"\t\tFROM LTD_RTGS_CASHINSTRUCTION \n" +
"\t\tWHERE VALUE_DATE= :valueDate \n" +
"\t\tAND LAST_UPDATED >= :lastUpdatedFrom \n" +
"\t\tAND LAST_UPDATED < :lastUpdatedTo \n" +
"\t\tGROUP BY ID, CURRENT_STATUS, SUBTYPE)\n" +
"GROUP BY CURRENT_STATUS, SUBTYPE", nativeQuery = true)
List<InstructionCount> findInstructionCounts(#Param("valueDate") LocalDate valueDate, #Param("lastUpdatedFrom") LocalDateTime lastUpdatedFrom, #Param("lastUpdatedTo") LocalDateTime lastUpdatedTo);
The issue is that this does not work, and I have found it is because I cannot use the approach of mapping it to a new object using the SELECT new com.modles.InstructionCount with nativeQuery = true. However when I try to remove the nativeQuery = true part and run my test I get the following error:
I also notice that the SQL gets syntax errors in the IDE, highlighted on the second SELECT statement, so I presume there is an issue with this format when not using nativeQuery.
Can anyone help with how I can resolve this issue? I know this can work, because when I remove the SELECT new com.modles.InstructionCount part, it will just return a list of Object[] with the correct values I'm expecting, but I would much prefer to map this to the correct object as part of the query. Alternatively, if there is a way to write this query using the Specification API, I would also use that approach!

Below query should just work just fine and there is no need of your inner/sub query
SELECT CURRENT_STATUS, SUBTYPE, COUNT(ID)
FROM INSTRUCTION
WHERE VALUE_DATE= '17-JUN-21'
AND LAST_UPDATED >= '16-JUN-21'
AND LAST_UPDATED < '17-JUN-21'
GROUP BY CURRENT_STATUS, SUBTYPE;
This should works seemlessly while returning the result as
InstructionCount
------->Edit------------->
#Query(value = "SELECT new com.modles.InstructionCount(currentStatus, subtype, count(id)) FROM Instruction WHERE valueDate= :valueDate AND lastUpdatedFrom >= :lastUpdatedFrom AND lastUpdatedTo < :lastUpdatedTo GROUP BY id, currentStatus, subtype")
List<InstructionCount> findInstructionCounts(#Param("valueDate") LocalDate valueDate, #Param("lastUpdatedFrom") LocalDateTime lastUpdatedFrom, #Param("lastUpdatedTo") LocalDateTime lastUpdatedTo);

Related

hibernate native query inserting records in a loop with many-to-one relationship

I'm working on a spring boot project, there I have two tables that are related to each other with OneToMany relationship
public class PackGroupEntity{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String description;
private Double qty;
private Integer packingNr;
#Temporal(TemporalType.TIMESTAMP)
private Date deliveredTime;
#OneToMany(mappedBy = "packGroup", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<PackArticlesEntity> packArticles= new ArrayList<>();
}
And
public class PackArticlesEntity{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Double qty;
private Double confirmedName;
#Enumerated(EnumType.STRING)
private ArticleStatus status;
private Double weight;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "pack_group_id")
private PackGroupEntity packGroup;
}
And I insert data on these two tables in a loop, first I group the articles by packing number for which I will create a PackGroup that has a list of articles:
Map<Integer, List<RecivedArticlesDTO >> groupedArticles =
recivedArticlesListDTO.stream().collect(Collectors.groupingBy(RecivedArticlesDTO::getPackingNr));
for (Map.Entry<Integer, List<RecivedArticlesDTO>> entry : groupedArticles.entrySet()) {
List<RecivedArticlesDTO > groups = entry.getValue();
PackGroupEntity packGroup = new PackGroupEntity();
packGroup.setPackingNr(entry.getKey())
//some manipulations and setting data for each field
List<PackArticlesEntity> listWithArticles = new ArrayList<>();
groups.forEach(pack -> {
PackArticlesEntity packArticle= new PackArticlesEntity();
packArticles.setQty(pack.getQty);
//some manipulations and setting data for each field
listWithArticles.add(packArticles);
}
packGroup.setPackArticles(listWithArticles);
// here I have to save the data using native query
packGroupService.savePackGroupData(packGroup);
}
In this way, it is so slow so I wanted to do it on the native query.
The problem is that I have many packGroups a many packArticles that needs to be saved. I was thinking to somehow do only one connection with DB to send the list of pack groups and pack articles to save, but I don't know how to do this on native queries. This here is just for one pack_group but yet I don't know how to pass packArticles on a native query since it's a list
#Query(value = "insert into pack_group " +
" (id,packing_nr, description, qty, deliveredTime, packArticles) " +
" values (1?, 2?, 3?, 4?, 5?, 6?)", nativeQuery = true)
void savePackGroupData(id, packing_nr, description, qty, packArticles);
Can someone help with this, please?
EDIT:
I want to return the id from insert on
String query = String.format("insert into pack_group(group, remark, description ) " +
"values ( %s, %s, %s)", "x","y","z" );
Query q = entityManager.createNativeQuery(query );
BigInteger biid = (BigInteger) q.getSingleResult();
long id = biid.longValue();
And I get this error com.microsoft.sqlserver.jdbc.SQLServerException: The statement did not return a result set.
To speed things up consider using batch updates. It can be a bit tricky with spring data to get it to work, but it do speed things up considerably when working.
See How to do bulk (multi row) inserts with JpaRepository?
If you wish more control over your insert statements then perhaps spring jdbc is a better option :How to do multiple inserts in database using spring JDBC Template batch?
This is an answer for 'I have a list of articles in which I have to use the reference id of the previous inserted pack group. Can you get that on SQL?' in comment.
You have Main and Detail table. And you want to insert Main first, and Detail second with Id of Main just inserted.
You can use output inserted of SQL Server to get only currently inserted rows.
And you can join it with string_split result to insert into Detail table.
Here's example.
create table Main (
Id int identity(1, 1),
Name nvarchar(50)
);
create table Detail (
MainId int,
Name nvarchar(50)
);
insert into Main (Name) values ('X'); -- To make new inserted Id starts with 2
declare #MainList nvarchar(1000) = 'A,B,C';
declare #DetailList nvarchar(1000) = 'A2,B2,C2';
declare #IdList table (
Seq int identity(1, 1),
Id int
);
-- Insert 3 rows, get all 3 Id using inserted, insert 3 Id to #IdList table
insert into Main (Name)
output inserted.Id into #IdList
select value from string_split(#MainList, ',');
-- Join #IdList table with string_split returned table with Seq
-- Seq of #IdList is auto generated by identity(1, 1)
-- Seq of string_split returned table generated by row_number()
insert into Detail (MainId, Name)
select m.Id MainId, d.value Name
from #IdList m
inner join
(select row_number() over (order by (select 1)) Seq, value
from string_split(#DetailList, ',')
) d
on m.Seq = d.Seq;
Result:
select * from Main;
select * from Detail;
Id Name
--------
1 X
2 A
3 B
4 C
MainId Name
------------
2 A2
3 B2
4 C2
Demo: https://dbfiddle.uk/?rdbms=sqlserver_2019&fiddle=30213ab0cd59fcb7f541c18c738d4dad

What should LIMIT have for attributes in Spring JPA? SQL

I'm trying to have limits with nativeQuery = true but Spring JPA can't find the query for this.
My error is:
Caused by: java.lang.IllegalStateException: Using named parameters for method public abstract void se.danielmartensson.repositories.DataRepository.deleteByJobNameOrderByDateTimeLimit(java.lang.String,long,long) but parameter 'Optional[jobName]' not found in annotated query 'DELETE FROM Data data ORDER BY data.dateTime WHERE data.jobName =: jobName LIMIT firstIndex = :firstIndex, selectedSamples = :selectedSamples'!
So I'm guessing that LIMIT should have different attributes, rather than firstIndex and selectedSamples ? What can it be then?
Where is my query from my Repository in Spring Boot
#Query(value = "SELECT * FROM Data data ORDER BY data.dateTime WHERE data.jobName = :jobName LIMIT firstIndex = :firstIndex, selectedSamples = :selectedSamples", nativeQuery = true)
List<Data> findByJobNameOrderByDateTimeLimit(#Param("jobName") String jobName, #Param("firstIndex") long firstIndex, #Param("selectedSamples") long selectedSamples);
#Modifying
#Query(value = "DELETE FROM Data data ORDER BY data.dateTime WHERE data.jobName =: jobName LIMIT firstIndex = :firstIndex, selectedSamples = :selectedSamples", nativeQuery = true)
void deleteByJobNameOrderByDateTimeLimit(#Param("jobName") String jobName, #Param("firstIndex") long firstIndex, #Param("selectedSamples") long selectedSamples);
I have created a sample project that uses H2. You must change limit and offset part in which database you are using. For the delete operation, there is delete from table where id in (select id from table where .. order by .. limit .. offset ..) is defined.
Note: Lombok is used for getter, setter and toString. It is not required.
DataRepository.java
public interface DataRepository extends JpaRepository<Data, Integer> {
#Query(value = "SELECT * FROM Data data WHERE data.jobName = :jobName ORDER BY data.dateTime limit :selectedSamples offset :firstIndex"
, nativeQuery = true)
List<Data>findByJobNameOrderByDateTimeLimit(#Param("jobName") String jobName, #Param("firstIndex") Integer firstIndex, #Param("selectedSamples") Integer selectedSamples);
#Transactional
#Modifying
#Query(value = "DELETE FROM Data data WHERE data.id in (select id from Data d where d.jobName = :jobName order by d.dateTime limit :selectedSamples offset :firstIndex)"
, nativeQuery = true)
void deleteByJobNameOrderByDateTime(#Param("jobName") String jobName, #Param("firstIndex") Integer firstIndex, #Param("selectedSamples") Integer selectedSamples);
}
Data.java
#lombok.Data
#Entity
#Table
public class Data {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
#Column(name = "jobName")
private String jobName;
#Column(name = "dateTime")
private LocalDateTime dateTime;
public Data() {
}
public Data(String jobName, LocalDateTime dateTime) {
this.jobName = jobName;
this.dateTime = dateTime;
}
}
Test
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.util.List;
#SpringBootTest
class DemoApplicationTests {
#Autowired
private DataRepository dataRepository;
#Test
void test1() {
// insert dummy records
dataRepository.save(new Data("job1", LocalDateTime.now().minusMinutes(1)));
dataRepository.save(new Data("job1", LocalDateTime.now().minusMinutes(2)));
dataRepository.save(new Data("job1", LocalDateTime.now().minusMinutes(3)));
dataRepository.save(new Data("job1", LocalDateTime.now().minusMinutes(4)));
// get records
List<Data> dataList = dataRepository.findByJobNameOrderByDateTimeLimit("job1", 0, 4);
for (Data data : dataList) {
System.out.println(data);
}
// delete
dataRepository.deleteByJobNameOrderByDateTime("job1", 1, 2);
// get records
dataList = dataRepository.findByJobNameOrderByDateTimeLimit("job1", 0, 4);
for (Data data : dataList) {
System.out.println(data);
}
}
}
Output
Hibernate: SELECT * FROM Data data WHERE data.jobName = ? ORDER BY data.dateTime limit ? offset ?
Data(id=4, jobName=job1, dateTime=2021-01-08T05:25:31.830)
Data(id=3, jobName=job1, dateTime=2021-01-08T05:26:31.829)
Data(id=2, jobName=job1, dateTime=2021-01-08T05:27:31.827)
Data(id=1, jobName=job1, dateTime=2021-01-08T05:28:31.756)
Hibernate: DELETE FROM Data data WHERE data.id in (select id from Data d where d.jobName = ? order by d.dateTime limit ? offset ?)
Hibernate: SELECT * FROM Data data WHERE data.jobName = ? ORDER BY data.dateTime limit ? offset ?
Data(id=4, jobName=job1, dateTime=2021-01-08T05:25:31.830)
Data(id=1, jobName=job1, dateTime=2021-01-08T05:28:31.756)
Mysql
For mysql delete operation is successful with below script.
Reference: MySQL DELETE FROM with subquery as condition - Answer
#Transactional
#Modifying
#Query(value = "DELETE FROM Data WHERE id in (select id from (select id from Data where jobName = :jobName order by dateTime limit :selectedSamples offset :firstIndex) x)"
, nativeQuery = true)
void deleteByJobNameOrderByDateTime(#Param("jobName") String jobName, #Param("firstIndex") Integer firstIndex, #Param("selectedSamples") Integer selectedSamples);
If you just want to get the row which jobName and firstIndex and selectedSamples match the parameters. you should use correct SQL syntax and put them in where.
SELECT * FROM Data data
WHERE data.jobName = :jobName AND firstIndex = :firstIndex AND selectedSamples = :selectedSamples
ORDER BY data.dateTime
the correct syntax is like this :
select *
from [table]
where 'condiction'
order by [column]
limit [int]
LIMIT however is used to set the max tuples SQL return, for example if the query return 10K rows but you only want to look at first 5 rows, you can use limit 5 to tell SQL only return first 5 fetch.
Also in different DBMS there might need to use different syntax or method to achieve the same thing, like in ORACLE we got no limit clause, instead we got FETCH or simply using ROWNUM.

Dynamic where JPA Spring Boot

I have an Table and Pojo in my Spring Boot application like below.
#Entity
#Table(name = "attendance_summary")
public class AttendanceSummary implements Serializable {
#Id
#SequenceGenerator(name = "attendance_summary_id_seq",
sequenceName = "attendance_summary_id_seq",
allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "attendance_summary_id_seq")
#Column(name = "id", updatable = false)
public Integer id;
#Column(name = "emp_id", nullable = false)
public Integer empId;
#Column(name = "designation_id")
public Integer designationId;
#Column(name = "designation_category_id")
public Integer designationCategoryId;
#Column(name = "department_id")
public Integer departmentId;
......
}
Now I want have dynamic inputs for these fields. Meaning user might select
a list of empIds, designationIds.... or any combinations of them or even none of them.
If they select none of the fields I need to return all the rows from the table in the database.
But in jpa when we write methods in repository we have to specify the field names like
public interface AttendanceSummaryRepository extends JpaRepository<Integer,AttendanceSummary>{
List<AttendanceSummary> findByEmpIdAndDesignationId....(List<Integer> empIdList,List<Integer> designationIdList ... );
}
Which means if any of these parameters are null I will get an error or an exception and as a result I will miss some data.
where as in PHP or some other language like that I can just check the value of the desired filters and just dynamically add a where clause in the query.
query= "Select * from attendance_summary where ";
if(empIdList != null)
query = query + " emp_id in empIdList "
if(designationIdList != null)
query = query + " designation_id in designationIdList "
.....
//You get the idea.
Is there any way to do so with jpaDataMethods and if yes how. Any detailed explanation / link to resources are much appreciated.
Sorry for poor english and if I couldn't explain my problem properly.
Take a look at Criteria API. It allows you to create dynamic queries.
In your example, something similar to this could work:
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<AttendanceSummary> query = cb.createQuery(AttendanceSummary.class);
Root<AttendanceSummary> root = query.from(AttendanceSummary.class);
List<Predicate> predList = new ArrayList<>();
if (empIdList != null) {
predList.add(root.get('empId').in(empIdList));
}
if (designationIdList != null) {
predList.add(root.get('designationId').in(designationIdList));
}
// ... You get the idea.
Predicate[] predicates = new Predicate[predList.size()];
predList.toArray(predicates);
query.where(predicates);
return entityManager.createQuery(query).getResultList();
You can achieve this by using #Query annotation. Please refer spring guide for more details.
#Query(value = "from attendance_summary where (emp_id in (?1) or ?1 is null) and (designation_id in (?2) or ?2 is null)" )
Query Detail:
SELECT * #implicit so removed
FROM attendance_summary
WHERE ( emp_id IN (?1) #true when IDs are not null, thus apply filter
OR ?1 IS NULL #true when user input null, return all rows )
AND ( designation_id IN (?2) #true when IDs are not null, thus apply filter
OR ?2 IS NULL #true user input null, return all rows)
Example Project on github with spring-boot, jpa & h2. Look for SchoolController & SchoolRepo classes, applying the same logic, the endpoints \school will filter the result for input Ids & \allschool will return everything as input is null.

Hibernate query for entity with map property

I've been struggling with a query in hibernate to update the state of an entity. Such entity, called PaymentRequestLink, is in a one to many relation with another entity called extraparameters
PaymentRequestType.java:
#Entity
#Table(name="payment_link")
public class PaymentRequestLink {
private Map<String, ExtraParameter<E>> extraParameters;
#Fetch(FetchMode.SELECT)
#ElementCollection(fetch=FetchType.EAGER)
#CollectionTable(name="extraparameter_payment_link",
schema=BaseEntity.DATABASE_SCHEMA, joinColumns=#JoinColumn(name="extraparameter_payment_link_id"))
#MapKeyColumn(name = "name", length=64, nullable = false)
public Map<String, ExtraParameter<String>> getExtraParameters() {
return extraParameters;
}
ExtraParameter.java:
#Embeddable
public class ExtraParameter<T extends Serializable> implements Serializable {
//...
#Column(name="extra_type", length=64, nullable=false)
#NotNull
public String getType() {
return type;
}
#Column(name="extra_value", length=255, nullable=false)
#NotNull
public T getValue() {
return value;
}
This is a data example of the extraparameter_payment_link table:
extraparameter_payment_link_id extra_type extra_value name
1 java.sql.Date 2019-01-01 EXPIRATION_DATE
The query I want to make is to update the state of the PaymentRequestLink to EXPIRED when the expiration date, which is stored in the extraparameters, is a date before to the current date.
This is the query I have now:
String stateSentence = "AND state <> '";
String sqlUpdateLinkDetailed = "UPDATE PaymentRequestLink ld SET state='EXPIRED' " +
"WHERE extraParameters.EXPIRATION_DATE.value <= ? " +
stateSentence + PaymentRequestLinkState.PAID;
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MINUTE, 1);
Timestamp now = new Timestamp(calendar.getTimeInMillis());
hibernateTemplate.bulkUpdate(sqlUpdateLinkDetailed, now);
The exception I've got is:
error processing job
org.springframework.orm.hibernate3.HibernateQueryException: could not resolve property: EXPIRATION_DATE of: component[type,value] [UPDATE
com.some.company.PaymentRequestLink ld SET state='EXPIRED' WHERE extraParameters.EXPIRATION_DATE.value <= ? AND state <> 'PAID']; nested exception is org.hibernate.QueryException: could not resolve property: EXPIRATION_DATE of: component[type,value] [UPDATE com.some.company.PaymentRequestLink ld SET state='EXPIRED' WHERE extraParameters.EXPIRATION_DATE.value <= ? AND state <> 'PAID']
Can anyone help me with this?
Elements of indexed collections (arrays, lists, and maps) can be
referred to by index in a where clause only:
String stateSentence = "AND state <> '";
String sqlUpdateLinkDetailed = "UPDATE PaymentRequestLink ld SET state='EXPIRED' " +
"WHERE extraParameters['EXPIRATION_DATE'].value <= ? " +
stateSentence + PaymentRequestLinkState.PAID;
See HQL expressions documentation for details.

Fetch database records using hibernate without using Primary Key

I want to fetch records from database without the primary key, for a table that i have created. Here is the model class for it.
#Entity
#Table(name = "DOCTORS_INFORMATION")
public class DoctorInformation {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "DOCTOR_ID")
private int ID;
#Column(name = "FIRST_NAME")
private String firstName;
#Column(name = "LAST_NAME")
private String lastName;
#Column(name = "ADDRESS_LINE1")
private String addressLine1;
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "CITY")
private Location location;
//along with setters and getters
It is very difficult to remember the Doctor_Id for every doctor, and logically First_Name or anything cannot be made a primary key. All the tutorials and lecture videos that i have gone through fetch records using session.get where session is an object of class Session. In the source code of Session class all the overloaded methods of get mandatory take id as a parameter...
Is their a workaround for the above problem?
There are multiple options. All of the following examples search for a doctor whose lastName contains Johnson.
Criteria
String lastNameToSearchFor = "Johnson";
List doctors = session.createCriteria(DoctorInformation.class)
.add( Restrictions.like("lastName", "%"+lastNameToSearchFor+"%") ) // the leading wild card can become a problem since it cannot be indexed by the DB!
.addOrder( Order.asc("lastName") )
.list();
http://docs.jboss.org/hibernate/orm/3.5/javadocs/org/hibernate/Criteria.html
HQL
String lastNameToSearchFor = "Johnson";
String hql = "FROM " + DoctorInformation.class.getName() + " di WHERE di.lastName LIKE :lastName ORDER BY di.lastName ASC";
Query query = session.createQuery(hql);
query.setParameter("lastName", "%" + lastNameToSearchFor + "%"); // again, the leading wild card may be a problem
List doctors = query.list();
http://www.tutorialspoint.com/hibernate/hibernate_query_language.htm
Native SQL
String lastNameToSearchFor = "Johnson";
String sql = "SELECT * FROM DOCTORS_INFORMATION WHERE lastName LIKE :lastName ORDER BY lastName ASC";
Query query = session.createSQLQuery(sql).addEntity(DoctorInformation.class);
query.setString("lastName", "%" + lastNameToSearchFor + "%"); // again, the leading wild card may be a problem
List doctors = query.list();
If you want to add capability to search with, let's say firstName, then you might want to look into Hibernate's Disjunction: Creating a hibernate disjunction query programatically

Categories