Dynamic where JPA Spring Boot - java

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.

Related

Format for a JPA query containing an aggregation and subquery?

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);

How to combine distinct and sort in spring data jpa specification query with joins

Example setup:
Entity
#Entity
class Book {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null
#ManyToMany(cascade = [CascadeType.PERSIST, CascadeType.MERGE])
#JoinTable(name = "book_authors",
joinColumns = [JoinColumn(name = "book_id")],
inverseJoinColumns = [JoinColumn(name = "author_id")])
var authors: MutableSet<Author> = HashSet()
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "publisher_id")
lateinit var publisher: Publisher
}
Both Author and Publisher are simple entities with just an id and a name.
The spring data jpa BookSpecification: (notice the distinct on query)
fun hasAuthors(authorNames: Array<String>? = null): Specification<Book> {
return Specification { root, query, builder ->
query.distinct(true)
val matchingAuthors = authorRepository.findAllByNameIn(authorNames)
if (matchingAuthors.isNotEmpty()) {
val joinSet = root.joinSet<Book, Author>("authors", JoinType.LEFT)
builder.or(joinSet.`in`(matchingContentVersions))
} else {
builder.disjunction()
}
}
}
Executing the query (pageable containing a sort on publisher.name)
bookRepository.findAll(
Specification.where(bookSpecification.hasAuthors(searchRequest)),
pageable!!)
The REST request:
MockMvcRequestBuilders.get("/books?authors=Jane,John&sort=publisherName,desc")
This results in the following error:
Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException: Order by expression "PUBLISHERO3_.NAME" must be in the result list in this case;
The problem is in the combination of the distinct and sort. The distinct requires the publisher name to be in the select fields to be able to sort.
How can I fix this with Specification query?
You'll likely have to explicitly select the PUBLISHERO3_.NAME column like so:
query.select(builder.array(root.get("PUBLISHERO3_.NAME"), root.get("yourColumnHere")));
Joined columns are probably not included by default because they're out of scope with regards to the root generic type.
you can't do this. basically, if you have distinct and you want to sort, you can only use the selected columns.
what you can do is to use row_number() window function instead of distinct, and then select everything with row_number=1.
you can find an (a little bit old) example here: https://stackoverflow.com/a/30827497/10668681

Aggregation in Hibernate / JPA

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

JPA Discriminator using Spring-Data Repositories and JPQL Query

I have two entities in MySQL as below. The primary key of nnm_tran is a composite of id and source. The primary key of bargains is actually a foreign key link to the nnm_tran table
I'm trying to use JPA inheritance to represent these.
nnm_tran entity
#Entity
#Table(name = "nnm_tran")
#IdClass(CommonTransactionKey.class)
#Inheritance(strategy = InheritanceType.JOINED)
#DiscriminatorColumn(name = "bargain_flag", discriminatorType = DiscriminatorType.CHAR)
#DiscriminatorValue("N")
public class CommonTransaction {
#Id
#Column(name = "id", nullable = false)
private String transactionId;
#Column(name = "plan_number", nullable = false)
private String planNumber;
#Column(name = "tran_date")
private LocalDateTime transactionDatetime;
#Column(name = "bargain_flag")
private String bargainFlag;
...
}
bargains entity
#Data
#EqualsAndHashCode(callSuper = true)
#Entity
#Table(name = "bargains")
#DiscriminatorValue("B")
#PrimaryKeyJoinColumns({ #PrimaryKeyJoinColumn(name = "nnm_tran_id", referencedColumnName = "id"), #PrimaryKeyJoinColumn(name = "nnm_tran_source", referencedColumnName = "source") })
public class Bargain extends CommonTransaction implements Serializable {
#Column(name = "unit_price")
private BigDecimal unitPrice;
#Column(name = "client_price")
private BigDecimal clientPrice;
...
}
I think so far this is all hooked up correctly. My problem comes when I attach a spring-data repository with a custom query.
Repository
public interface CommonTransactionRepository extends CrudRepository<CommonTransaction, CommonTransactionKey> {
#Query("select t from CommonTransaction t left join IoPlan p ON t.planNumber = p.planNumber "
+ "where (p.planNumber is NULL or p.planNumber = '') "
+ "and t.transactionDatetime between ?1 and ?2 "
+ "and t.cancelled = false")
public Iterable<CommonTransaction> findOrphanedTransactionsByTranDate(LocalDateTime fromDate, LocalDateTime toDate);
...
}
When this gets proxied and the method is executed it generates the SQL statement
SELECT DISTINCT nnm_tran.bargain_flag FROM nnm_tran t1 LEFT OUTER JOIN io_plan t0 ON (t1.plan_number = t0.plan_number) WHERE ((((t0.plan_number IS NULL) OR (t0.plan_number = ?)) AND (t1.tran_date BETWEEN ? AND ?)) AND (t1.CANCELLED = ?))
The issue with this is that the nnm_tran table is aliased to t1 but the discriminator column is referencing the full table name nnm_tran.bargain_flag The result is a lovely
UnitOfWork(17171249)--Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.5.2.v20140319-9ad6abd): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Unknown column 'nnm_tran.bargain_flag' in 'field list'
Question here is, am I doing something wrong or is this a bug in spring-data and/or eclipselink?
Versions: spring-data 1.7.2, Eclipselink 2.5.2, MySQL 5.6.28
Using #manish's sample app as a starting point I started layering back on the complexity that was missing and quickly stumbled across the thing causing the rogue SQL. It was down to the join I had performed in the JPQL
NOTE: If you've come here from the future then ignore the remainder of this answer and instead use #Chris's comment instead.
Most of the time I don't need to look at or even think about the IoPlan table that can be seen in the #Query
#Query("select t from CommonTransaction t left join IoPlan p ON t.planNumber = p.planNumber "
+ "where (p.planNumber is NULL or p.planNumber = '') "
+ "and t.transactionDatetime between ?1 and ?2 "
+ "and t.cancelled = false")
and so this table is not a part of the CommonTransaction entity as a field. Even the result of this query doesn't really care because it's looking only as a one off for CommonTransaction with no associated join in the IoPlan table.
When I added the join back in to the sample app from #manish it all broke in the same way my app has in EclipseLink, but broke in a different way for Hibernate. Hibernate requires a field for you to join with, which if you ask me defeats the purpose of writing the join in the #Query. In fact in Hibernate you have to define the join purely in JPA so you might as well then use dot notation to access it in the JPQL.
Anyway, going along with this idea I tried adding a dummy field to hold an IoPlan in my CommonTransaction entity and it almost worked. It defaulted some of the join logic but it was closer
SELECT DISTINCT t1.bargain_flag FROM nnm_tran t1 LEFT OUTER JOIN io_plan t0 ON ((t0.ID = t1.IOPLAN_ID) AND (t1.plan_number = t0.plan_number)) WHERE ((((t0.plan_number IS NULL) OR (t0.plan_number = ?)) AND (t1.tran_date BETWEEN ? AND ?)) AND (t1.CANCELLED = ?))
In this case t1.IOPLAN_ID and t0.ID don't exist. So I ended up defining the entire join in my CommonTransaction entity
#OneToOne
#JoinColumn(insertable = false, updatable = false, name = "plan_number", referencedColumnName = "plan_number")
private IoPlan ioPlan;
and voila, it started working. It's not pretty and now I have a redundant join condition
LEFT OUTER JOIN io_plan t1
ON ((t1.plan_number = t0.plan_number) AND (t0.plan_number = t1.plan_number))
but I can fix that. It's still annoying that I have to define a field for it whatsoever, I don't actually want or need it there, not to mention that the result from this query is returning CommonTransaction entities that have no IoPlan so the field will be permanently null.

Hibernate Criteria conditions or subqueries on OneToMany property values

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));

Categories