JPQL - implement conditional logic based on most recent element - jpa

I'm new to JPA and Spring data. I would like to implement a function with the following logic in my ordering system:
If no order after given timestamp, return 1
otherwise return last order's counter+1
Can I implement such logic with Pure JPQL?
Order entity:
#Entity
public class Order {
#Id
#GeneratedValue
private UUID id;
private Integer counter;
#CreationTimestamp
private Timestamp creationTimestamp;
...
OrderRepository.java:
#Repository
public interface OrderRepository extends CrudRepository<Order, UUID> {
// TODO what goes after else?
#Query("select case when count(o) < 1 then 1 else ... from Order o where o.creationTimestamp > :timestamp order by o.creationTimestamp desc")
Integer nextCounter(#Param("timestamp") Timestamp timestamp);
}

I'm curious as to what the exact use case is. If you could rely on the fact that the counters for orders created after the instant provided as the parameter are increasing, you could simply select NVL(MAX(o.counter), 0) + 1, or event COUNT(o) + 1. I understand this is not the case.
What you want could be achieved with a subquery as follows:
SELECT NVL(MAX(o.counter), 0) + 1
FROM Order o
WHERE o.creationTimestamp = (
SELECT MAX(o.creationTimestamp)
FROM Order o
WHERE o.creationTimestamp > :timestamp
)

Related

How to find top N elements in Spring Data Jpa?

In Spring Data Jpa to get first 10 rows I can do this findTop10By...(). In my case the number or rows is not defined and comes as a parameter.
Is there something like findTopNBy...(int countOfRowsToGet)?
Here is another way without native query. I added Pageable as a parameter to the method in the interface.
findAllBySomeField(..., Pageable pageable)
I call it like this:
findAllBySomeField(..., PageRequest.of(0, limit)) // get first N rows
findAllBySomeField(..., Pageable.unpaged()) // get all rows
I don't know of a way to do exactly what you want, but if you are open to using #Query in your JPA repository class, then a prepared statement is one alternative:
#Query("SELECT * FROM Entity e ORDER BY e.id LIMIT :limit", nativeQuery=true)
Entity getEntitiesByLimit(#Param("limit") int limit);
Did it by using pagination, as described in the first answer. Just adding a more explicit example.
This example will give you the first 50 records ordered by id.
Repository:
#Repository
public interface MyRepository extends JpaRepository<MyEntity, String> {
Page<MyEntity> findAll(Pageable pageable);
}
Service:
#Service
public class MyDataService {
#Autowired
MyRepository myRepository;
private static final int LIMIT = 50;
public Optional<List<MyEntity>> getAllLimited() {
Page<MyEntity> page = myRepository.findAll(PageRequest.of(0, LIMIT, Sort.by(Sort.Order.asc("id"))));
return Optional.of(page.getContent());
}
}
Found the original idea here:
https://itqna.net/questions/16074/spring-data-jpa-does-not-recognize-sql-limit-command
(which will also link to another SO question btw)

Multi-level subquery with JPA CriteriaBuilder

I have the following JPA entities
#Entity
#Table(name="application_user")
public class ApplicationUser {
#Id
#Column(name="user_id")
private String userid;
#Column(name="last_write_time")
private Instant lastWrite;
//other fields omitted
}
#Entity
#Table(name="demographic")
public class Demographic {
#Id
#Column(name="user_id")
private String userid;
//primary key is a foreign key link
#OneToOne
#PrimaryKeyJoinColumn(name="user_id", referencedColumnName="user_id")
private ApplicationUser user;
//other fields omitted
}
My goal is to retrieve all of the Demographics that contains users where the last write time is the max value in the column. I pretty much want to write the following SQL using the JPA CriteriaBUilder
select * from demographic where
userid in (
select userid from application_user where
last_write in (
select max(last_write) from application_user
)
)
I tried writing the following CriteriaBuilder Code to accomplish this goal and it compiles successfully. Note I am using the generated Metamodel classes.
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Demographic> c = cb.createQuery(Demographic.class);
Root<Demographic> root = c.from(Demographic.class);
root.fetch(Demographic_.user, JoinType.INNER);
Subquery<Instant> sqLatestUsers = c.subquery(Instant.class);
Root<ApplicationUser> subRootLatestUsers = sqLatestUsers.from(ApplicationUser.class);
sqLatestUsers.select(cb.greatest(subRootLatestUsers.<Instant>get(ApplicationUser_.LAST_WRITE)));
Predicate predicateLatestUsers = subRootLatestUsers.get(ApplicationUser_.LAST_WRITE).in(sqLatestUsers);
Subquery<ApplicationUser> sq = c.subquery(ApplicationUser.class);
Root<Demographic> subRoot = sq.from(Demographic.class);
sq.select(subRoot.<ApplicationUser>get(Demographic_.USER)).where(predicateLatestUsers);
Predicate containsUsers = subRoot.get(Demographic_.USER).in(sq);
c.select(root).where(containsUsers);
The code compiles and successfully deploys in Wildfly 14, but when I execute the code, the get the following error (with white space to improve readability):
Invalid path: 'generatedAlias2.user' : Invalid path: 'generatedAlias2.user'
...
Caused by: org.hibernate.hql.internal.ast.QuerySyntaxException: Invalid path: 'generatedAlias2.user' [
select generatedAlias0 from com.company.model.Demographic as generatedAlias0
inner join fetch generatedAlias0.user as generatedAlias1
where generatedAlias2.user in (
select generatedAlias2.user from com.company.model.Demographic as generatedAlias2 where generatedAlias3.lastWrite in (
select max(generatedAlias3.lastWrite) from com.company.model.StarfishUser as generatedAlias3
)
)
]
Is chaining subqueries (nested subqueries) allowed by the JPA spec? Did I find something that is syntactically correctly but not actually allowed?
I figure out how to get the subquery to work. First is my updated Utility method
public static <R, T> Subquery<T> getLatestSubelement(CriteriaBuilder cb, CriteriaQuery<R> c, Class<T> clazz, SingularAttribute<T, Instant> attribute) {
//Get latest timestamp
Subquery<Instant> sq = c.subquery(Instant.class);
Root<T> subRoot = sq.from(clazz);
sq.select(cb.greatest(subRoot.<Instant>get(attribute)));
//Get object with the latest timestamp
Subquery<T> sq2 = c.subquery(clazz);
Root<T> subRoot2 = sq2.from(clazz);
sq2.where(subRoot2.get(attribute).in(sq));
return sq2;
}
Here is the code that uses the utility method
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Demographic> c = cb.createQuery(Demographic.class);
Root<Demographic> root = c.from(Demographic.class);
joinType = JoinType.INNER;
//use fetch instead of join to prevent duplicates in Lists
root.fetch(Demographic_.user, joinType);
Subquery<ApplicationUser> sq = JpaUtil.getLatestSubelement(cb, c, ApplicationUser.class, ApplicationUser_.lastWrite);
c.where(root.get(Demographic_.user).in(sq));
TypedQuery<Demographic> q = em.createQuery(c);
Stream<Demographic> stream = q.getResultStream();

JPA eclipselink sum of integer in JPQL are not integers?

I'm using Eclipselink with postrgresql.
My entity is
public class PedaneMovimenti extends EntityBaseGest implements Serializable {
private static final long serialVersionUID = 1L;
...
#Column(name = "importo", nullable = false)
private Integer importo = 0;
...
In my JPQL Named query I sum the column importo, then use this value in a costructor of a class.
I have two constructor for the class used as projection:
public SaldoPedaneCliente(AnagraficaPGF anagrafica, TipoBancale tipo, Integer saldo);
public SaldoPedaneCliente(AnagraficaPGF anagrafica, TipoBancale tipo, Long saldo);
The JPQL query is
SELECT new com.path.SaldoPedaneCliente(
mov.mastro.anagrafica,
mov.tipobancale,
(
COALESCE(
SELECT SUM(m.importo)
FROM PedaneMovimenti m
WHERE m.mastro.anagrafica = mov.mastro.anagrafica AND m.tipobancale = mov.tipobancale
AND m.verso = com.bsssrl.bssstdgest.enums.VersoMovimento.IN
, 0)
))
from PedaneMovimenti mov WHERE mov.mastro.anagrafica IS NOT NULL
GROUP BY mov.mastro.anagrafica, mov.tipobancale
The query is ok, it works.
I've a type mismatch in the costructor:
javax.persistence.PersistenceException: java.lang.IllegalArgumentException: argument type mismatch
If I change the subquery with a constant (1 for example), it works fine, but if I use the sum, I've the exception.
So, does the sum on Integers is not an Integers or a Long?
EDIT: I've tryed also SELECT SUM(1) but I've the same error.
The sum of "Integer"s is a "Long" !
I've changed the order of the constructors: first the Long version, then the Integer version and it works.

How to map ALL names directly by JPA?

Given a ZIP-code-like hierarchical code/name schema.
For example:
code = 101010
Code:
100000 level 1 code (10....)
101000 level 2 code (..10..)
101010 level 3 code (....10)
Name (short name)
100000 - A
101000 - a
101010 - i
Name (FullQualifiedName)
100000 - A
101000 - A->a
101010 - A-a->i
EDIT
I wanna following code (JPA pseudo code), but CANNOT.
#Entity
public class CodeName{
// ....
String code; // 100101 levels = {100000, 100100, 100101}
String name; //
#HowToMapDirectedToNameOfCode('100000') // #SecondTable ?
String name1;
#HowToMapDirectedToNameOfCode('100100')
String name2;
#HowToMapDirectedToNameOfCode('100101')
String name3;
String getFullQualifiedName(){
return String.format("%s->%s->%s", name1, name2, name3);
}
// getter and setter
}
But it's relatively easier in native SQL:
SELECT (select p1.name from codename p1 where p1.code= concat( substring(p.code,1,2), "0000") ) province,
(select p2.name from codename p2 where p2.code= concat( substring(p.code,1,4), "00") ) city,
(select p3.name from codename p3 where p3.code=p.code) area
FROM codename p WHERE p.code = '100101';
So, I implements it as following snippet.
#Entity
public class CodeName{
// ....
String code; // 100000, 101000, 100101
String name; // province, city , area
#Transient
String name1; // mapping directly?
#Transient
String name2; // mapping directly?
#Transient
String name3; // mapping directly?
String getFullQualifiedName(){
return String.format("%s->%s->%s", name1, name2, name3);
}
// getter and setter
}
public interface CodeNameRepository extends CrudRepository<CodeName, Long>, CodeNameRepositoryCustom {
#Query(" FROM CodeName p " +
" WHERE p.code = CONCAT(SUBSTRING(?1, 1, 2), '0000') " +
" OR p.code = CONCAT(SUBSTRING(?1, 1, 4), '00') " +
" OR p.code = ?1")
List<CodeName> findAllLevelsByCode(String code);
}
#Component
public class CodeNameRepositoryImpl implements CodeNameRepositoryCustom {
#Autowired
private CodeNameRepository codeNameRepository ;
#Override
public CodeName CodeNamefindFullQualifiedNameByCode(String code) {
List<CodeName> codeNames= codeNameRepository .findAllLevelsByCode(code);
CodeName codeName;
// extra name1, name2, name3 from list,
// fill code, name, name1, name2, name3 to codeName and
return codeName;
}
}
But it have SO MANY limitations.
Most likely, I need getFullQualifiedName(), to display it on UI, but every time I must have an extra call to populate all names.
For each entity has CodeName as its children, no matter how deep the codeName is at, I MUST expand to the codeName and reload it with FQN.
Can we mapping all #Transient names directly by JPA?
You could technically model your code repository entity as follows:
public class CodeName {
#Id
#GeneratedValue(GenerationStrategy.AUTO)
#Column
private Long id;
#ManyToOne
private CodeName parent;
#OneToMany(mappedBy = "parent")
private List<CodeName> children;
#Column
private String name;
#Transient
public String getFullyQualifiedName() {
List<String> names = new ArrayList<>();
names.add(name);
CodeName theParent = parent;
while(theParent != null) {
names.add(theParent.getName());
theParent = theParent.parent;
}
Collections.reverse(names);
return StringUtils.join(names, "->");
}
}
Because the parent relationships will be fetched EAGERLY because they mapped as #ManyToOne, you can basically start at any child CodeName entity and traverse up it's parent/child relationship to the root. This basically allows the getFullyQualifiedName method to build the name for you at runtime.
If performance becomes a problem doing this, you can always datamine the names ahead of time in your entity as you described by adding a #Column private String fullyQualifiedName and make sure that field is inserted when you create your codes. Then the transient method I added to my the entity can be dropped since you're caching the names at data insertion.
It is possible to write a JPQL, which is equivalent to your SQL query. The only tricky part is to rewrite nested selects into cross joins, because nested selects are not supported by JPA and you need to join unrelated entities. On the other hand, functions CONCAT and SUBSTRING are supported by JPQL in the same way as in SQL. See the following JPQL query, which should give you the results as the SQL query in the question:
SELECT p1.name // province
, p2.name // city
, p.name // area
FROM CodeName p, CodeName p1, CodeName p2
WHERE p.code = '100101'
AND p1.code = concat( substring(p.code,1,2), "0000")
AND p2.code= concat( substring(p.code,1,4), "00")
The above query will give you 3 values in one row, which cannot be mapped into a single entity. The result of the query will therefore be a list of Object[] arrays. You may also add the original entity into the select clause: SELECT p1.name, p2.name, p.name, p FROM .... This way, you may later process the list of results and assign first three values into the transient fields of the entity:
Object[] rows = query.getResultList();
for (Object row : rows) {
CodeName c = (CodeName)row[3];
c.setName1((String)row[0]);
c.setName2((String)row[1]);
c.setName3((String)row[2]);
}

Criteria for where predicate using columns from two tables and functions in NHibernate

My goal is to fetch two values from two tables joined together and perform a comparison which if true, outputs the rows from table 1. The TSQL code below illustrates the query, question is whether there is a way to do the third predicate in the where clause using NHibernate criteria using session.CreateCriteria:
declare #currentSystemTime datetimeoffset = '2015-07-22 18:42:16.1172838 +00:00'
select
inst.ExpiryDate,
ent.DaysToExpiryNotificationStart,
convert(date, dateadd(day, -ent.DaysToExpiryNotificationStart, inst.ExpiryDate)) as NotificationStart,
convert(date, #currentSystemTime) as CurrentSystemTime,
*
from
Instances inst
inner join Entries ent on inst.Entry_id = ent.Id
where
inst.ExpiryDate is not null
and ent.DaysToExpiryNotificationStart is not null
and convert(date, #currentSystemTime) >= convert(date, dateadd(day, -ent.DaysToExpiryNotificationStart, inst.ExpiryDate))
The properties are defined as follows in the entity classes:
public virtual DateTimeOffset? ExpiryDate { get; set; }
public virtual int? DaysToExpiryNotificationStart { get; set; }
I am using Fluent NHibernate to map these. Are manual queries via CreateQuery or CreateSQLQuery the only way to go? If there is an easier way to accomplish this task, I am open. Any help will be appreciated.
Thanks,
Shawn
session.Query<Instances>()
.Where(i => Datetime.Today >= i.ExpiryDate.AddDays(i.DaysToExpiryNotificationStart))
adddays is not yet natively supported but can be added quite easyly. see here to make AddDays work
the same slightly different can be done with QueryOver
session.QueryOver<Instances>()
.Where(i => Datetime.Today >= i.ExpiryDate.AddDays(i.DaysToExpiryNotificationStart))
public static class QueryOverExtensions
{
public static void Register()
{
ExpressionProcessor.RegisterCustomProjection(() => default(DatTime).AddDays(1), QueryOverExtensions.ProcessAddDays);
}
private static IProjection ProcessAddDays(MethodCallExpression methodCallExpression)
{
IProjection property = ExpressionProcessor.FindMemberProjection(methodCallExpression.Arguments[0]).AsProjection();
return (Projections.SqlFunction("addDays", NHibernateUtil.DateTime, NHibernateUtil.Int32, property));
}
}
Note: I'm not sure if adddays is already defined as sql function. you might need to register one in the driver