Is there a way to check for null ZonedDateTime in JPQL? - java

In some E2E tests I'm faced with a problem. Let's say, I have the following JPQL query:
Query query = entityManager.createQuery(
" select d from Document d left join d.audit da " +
" where " +
" (cast(:startDate as java.time.ZonedDateTime)) is null " +
" or truncate_for_minutes(da.dateCreate, 'UTC') >= " +
" truncate_for_minutes(:startDate, 'UTC')")
.setParameter("startDate", ZonedDateTime.now());
In the query string I use named parameter startDate which can be null. The query above works. But if I pass null, the following exception is thrown:
Caused by: org.postgresql.util.PSQLException:
ERROR: cannot cast type bytea to timestamp without time zone
Without type casting the following exception is thrown:
Caused by: org.postgresql.util.PSQLException:
ERROR: could not determine data type of parameter $1
Without check for null the following exception is thrown:
Caused by: org.postgresql.util.PSQLException:
ERROR: function pg_catalog.timezone(unknown, bytea) does not exist
No function matches the given name and argument types.
You might need to add explicit type casts.
I use this query in Spring Data repository by using #Query string. Function truncate_for_minute(...) - is just a small customization for the PostgreSQL function date_trunc(...).
I know that I can implement custom repository and build query string dynamically, but why I can't check for null ZonedDateTime in JPQL string? Maybe there is a way to do it?
My environment:
Java 11
PostgreSQL 11.3 on x86_64-pc-linux-musl, compiled by gcc (Alpine 8.3.0) 8.3.0, 64-bit
Hibernate 5.3.7 Final

Another solution is to use Criteria API and build the query dynamically.
#Repository
#RequiredArgsConstructor
public class CustomDocumentRepositoryImpl implements CustomDocumentRegistry {
private final EntityManager em;
#Override
public Page<Document> findDocumentsForExpertByFilter(SearchDocumentCriteria criteria,
Pageable pageable) {
final String AUDIT_TABLE = "...";
final String USER_TABLE = "...";
final String ID_FIELD = "id";
final String FIRST_NAME_FIELD = "...";
final String LAST_NAME_FIELD = "...";
final String MIDDLE_NAME_FIELD = "...";
final String WORKSTATION_FIELD = "...";
final String DATE_CREATE_FIELD = "...";
final String LIKE_MASK = "%%%s%%";
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Document> query = cb.createQuery(Document.class);
Root<Document> root = query.from(Document.class);
Path<ZonedDateTime> dateCreatePath =
root.get(AUDIT_TABLE).get(DATE_CREATE_FIELD);
Path<String> lastNamePath =
root.get(AUDIT_TABLE).get(USER_TABLE).get(LAST_NAME_FIELD);
Path<String> firstNamePath =
root.get(AUDIT_TABLE).get(USER_TABLE).get(FIRST_NAME_FIELD);
Path<String> middleNamePath =
root.get(AUDIT_TABLE).get(USER_TABLE).get(MIDDLE_NAME_FIELD);
root.fetch(AUDIT_TABLE, JoinType.LEFT)
.fetch(USER_TABLE, JoinType.LEFT);
Predicate documentIdsPredicate;
List<Long> documentIds = criteria.getIds();
if (isNull(documentIds) || documentIds.isEmpty()) {
documentIdsPredicate = cb.isNotNull(root.get(ID_FIELD));
} else {
documentIdsPredicate = root.get(ID_FIELD).in(criteria.getIds());
}
Predicate startDatePredicate;
ZonedDateTime startDate = criteria.getStartDate();
if (isNull(startDate)) {
startDatePredicate = cb.isNotNull(dateCreatePath);
} else {
startDatePredicate = cb.greaterThanOrEqualTo(dateCreatePath, startDate);
}
Predicate endDatePredicate;
ZonedDateTime endDate = criteria.getEndDate();
if (isNull(endDate)) {
endDatePredicate = cb.isNotNull(dateCreatePath);
} else {
endDatePredicate = cb.lessThanOrEqualTo(dateCreatePath, endDate);
}
Predicate lastNamePredicate = cb.like(cb.upper(lastNamePath),
format(LIKE_MASK, criteria.getLastName().toUpperCase()));
Predicate firstNamePredicate = cb.like(cb.upper(firstNamePath),
format(LIKE_MASK, criteria.getFirstName().toUpperCase()));
Predicate middleNamePredicate = cb.like(cb.upper(middleNamePath),
format(LIKE_MASK, criteria.getMiddleName().toUpperCase()));
Predicate fullNamePredicate =
cb.and(lastNamePredicate, firstNamePredicate, middleNamePredicate);
Predicate compositePredicate = cb.and(
fullNamePredicate,
documentIdsPredicate,
startDatePredicate,
endDatePredicate
);
query.where(compositePredicate);
Query limitedQuery = em.createQuery(query
.orderBy(cb.desc(root.get(AUDIT_TABLE).get(DATE_CREATE_FIELD))))
.setFirstResult(nonNull(criteria.getSize()) ?
criteria.getPage() * criteria.getSize() :
criteria.getPage());
if (nonNull(criteria.getSize())) {
limitedQuery.setMaxResults(criteria.getSize());
}
List<Document> documents = limitedQuery.getResultList();
return new PageImpl<>(documents, pageable, criteria.getSize());
}
}
Generates the following SQL:
select
document0_.id as id1_3_0_,
user1_.id as id1_13_1_,
document0_.created_dt as created_2_3_0_,
document0_.updated_dt as updated_3_3_0_,
document0_.created_user_id as created_6_3_0_,
document0_.updated_user_id as updated_7_3_0_,
document0_.name as name4_3_0_,
user1_.first_name as first_na2_13_1_,
user1_.last_name as last_nam3_13_1_,
user1_.middle_name as middle_n5_13_1_
from
some_scheme.document document0_
left outer join
some_scheme.s_user user1_
on document0_.created_user_id=user1_.id cross
join
some_scheme.s_user user2_
where
document0_.created_user_id=user2_.id
and (
upper(user2_.last_name) like '%LASTNAME%'
)
and (
upper(user2_.first_name) like '%FIRSTNAME%'
)
and (
upper(user2_.middle_name) like '%MIDDLENAME%'
)
and (
document0_.id in (
2 , 1
)
)
and document0_.created_dt>=...
and document0_.created_dt<=...
order by
document0_.created_dt desc limit 10;

In my case, the problem was as follows. I registered customization of SQL function date_trunc:
public class CustomSqlFunction implements MetadataBuilderContributor {
#Override
public void contribute(MetadataBuilder metadataBuilder) {
metadataBuilder.applySqlFunction(
"truncate_for_minutes",
new SQLFunctionTemplate(
StandardBasicTypes.TIMESTAMP,
"date_trunc('minute', (?1 AT TIME ZONE ?2))"
)
);
}
}
If change the StandardBasicTypes.TIMESTAMP to ZonedDateTime.INSTANCE and pass ZonedDateTime in one parameter, then the comparison in the JPQL query does not cause errors:
public class CustomSqlFunction implements MetadataBuilderContributor {
#Override
public void contribute(MetadataBuilder metadataBuilder) {
metadataBuilder.applySqlFunction(
"truncate_for_minutes",
new SQLFunctionTemplate(
ZonedDateTimeType.INSTANCE,
"date_trunc('minute', ?1)"
)
);
}
}

Related

NamedQuery select returns List<Object>. How to obtain value from Object

I have a select statement that goes into two table and obtain ids from each table. Here is my pseudo code:
Table 1 (study) has study_id
Table 2 (image) has image_id
NamedNativeQuery(name = "test", query = "select study.study_id, image.image_id from study study, image image where image_id=:imageId and study_id=:studyId")
In my code, I have:
Query query = getSession().getNamedNativeQuery("test");
query.setParameter(STUDY_ID, studyId);
query.setParameter(IMAGE_ID, imageId);
List result = query.getResultList(); //result is List<Object>
result is list of Object, where each Object has two values, one study_id and one image_id.
How do I extract these information from Object? What is the best approach to do this?
You can use the #SqlResultSetMapping with #ConstructorResult in the following way:
public class StudyImageIds {
private final Long studyId;
private final Long imageId;
public StudyImageIds(Long studyId, Long imageId) {
this.studyId = studyId;
this.imageId = imageId;
}
public Long getStudyId() {
return studyId;
}
public Long getImageId() {
return imageId;
}
}
// ...
#NamedNativeQuery(
name = "test",
query = "select "
+ " study.study_id as std_id, image.image_id as img_id "
+ "from study study, image image "
+ "where image_id = :imageId and study_id = :studyId",
resultSetMapping = "study_image_ids_dto"
)
#SqlResultSetMapping(
name = "study_image_ids_dto",
classes = #ConstructorResult(
targetClass = StudyImageIds.class,
columns = {
#ColumnResult(name = "std_id"),
#ColumnResult(name = "img_id")
}
)
)
and then use it:
List<StudyImageIds> studyImageIds = session.getNamedQuery("test")
.setParameter(STUDY_ID, studyId)
.setParameter(IMAGE_ID, imageId)
.list();
IMHO this is much more safer and elegant than types casting. See further details in the documentation.

Check if row record exists with JPA

I want to check if row record by id exists. I tried this:
public Optional<PaymentTransactions> transactionByUnique_Id( String unique_id ) throws Exception
{
String hql = "select e from " + PaymentTransactions.class.getName() + " e where e.unique_id = :unique_id";
TypedQuery<PaymentTransactions> query = entityManager.createQuery( hql, PaymentTransactions.class ) .setParameter( "unique_id", unique_id );
Optional<PaymentTransactions> paymentTransaction = query.getResultList().stream().findFirst();
return paymentTransaction;
}
Optional<PaymentTransactions> tnx = transactionsService.transactionByUnique_Id( transaction.getTransactionId() );
if(tnx.isPresent())
{
return "test";
// This method should be called only when DB record exists
}
But I alway get present object. What is the proper way to implement this check.

use like for integer value in namedquery Eclipselink

when use namedQuery in entity class get error
#NamedQuery(name = "Classification.search", query = "SELECT c FROM Classification c WHERE c.id LIKE :value")
Method for call namedQuery
public List<Classification> search(String value) {
Query query = getEntityManager().createNamedQuery("Classification.search", Classification.getClass()).setParameter("value", "%"+value+"%");
query.setMaxResults(10);
return query.getResultList();
}
java.lang.IllegalArgumentException: You have attempted to set a value of type class java.lang.String for parameter value with expected type of class java.lang.Integer from query string SELECT c FROM Classification c WHERE c.id LIKE :value.
but when use this method is work without Error.
public List<Classification> findLimited(String _clasif, int maxResult) {
String querySt = "SELECT c FROM Classification c WHERE c.id LIKE '%" + _clasif + "%'";
Query query = em.createQuery(querySt);
query.setMaxResults(maxResult);
List<Classification> classif;
classif = query.getResultList();
if (classif != null) {
return classif;
}
return new ArrayList<>();
}
i use eclipselink 2.6 with JPA
As per the BNF for JPQL, "LIKE" is for use with String values only. Use with non-String values would only be a vendor extension, and hence vendor-dependent. Whether it is part of a named query or criteria or string-based JPQL is irrelevant.
like_expression ::= string_expression [NOT] LIKE pattern_value [ESCAPE escape_character]
i solve that by this code.
public List<Classification> search(String value) {
Query query = getEntityManager().createQuery(getNamedQueryCode(entityClass, "Classification.search").replace(":value", value));
query.setMaxResults(10);
return query.getResultList();
}
this method get namedQuery string from entity class by nameKey and class.
private String getNamedQueryCode(Class<? extends Object> clazz, String namedQueryKey) {
NamedQueries namedQueriesAnnotation = clazz.getAnnotation(NamedQueries.class);
NamedQuery[] namedQueryAnnotations = namedQueriesAnnotation.value();
String code = null;
for (NamedQuery namedQuery : namedQueryAnnotations) {
if (namedQuery.name().equals(namedQueryKey)) {
code = namedQuery.query();
break;
}
}
if (code == null) {
if (clazz.getSuperclass().getAnnotation(MappedSuperclass.class) != null) {
code = getNamedQueryCode(clazz.getSuperclass(), namedQueryKey);
}
}
//if not found
return code;
}

JPA TypedQuery error

I get the error "Cannot create TypedQuery for query with more than one return using requested result type"
for the following query using JPA on Glassfish, any ideas what is wrong here? I want to get the latest debit record with a certain debit status.
entityManager.createQuery("select dd, MAX(dd.createdMillis) from T_DEBIT dd" +
" where dd.debitStatus in (:debitStatus)" +
" and dd.account = :account" , Debit.class)
.setParameter("debitStatus", false)
.setParameter("account", account)
.getSingleResult();
A generic parameter is normally specified for a TypedQuery. If you declared a TypedQuery you would use an Object[] as the generic parameter for the TypedQuery, since you are projecting columns and not returning a complete entity.
However, since you have not declared a TypedQuery (your using a concise coding style), you need to change Debit.class to Object[].class since your not selecting an object, but instead only two fields.
Object[] result = entityManager.createQuery("select dd, MAX(dd.createdMillis) from T_DEBIT dd" +
" where dd.debitStatus in (:debitStatus)" +
" and dd.account = :account" , Object[].class) //Notice change
.setParameter("debitStatus", false)
.setParameter("account", account)
.getSingleResult();
Executing this query will return a Object[] where each index in the Object[] corresponds with a field in your select statement. For example:
result[0] = dd
result[1] = max(dd.createdMillis)
To avoid using the Object[] you could create a new class to retrieve these values in a more strongly typed fashion. Something like:
public class Result {
String dd;
Date createdMillis;
public Result(String dd, Date createdMillis) {
super();
this.dd = dd;
this.createdMillis = createdMillis;
}
public String getDd() {
return dd;
}
public void setDd(String dd) {
this.dd = dd;
}
public Date getCreatedMillis() {
return createdMillis;
}
public void setCreatedMillis(Date createdMillis) {
this.createdMillis = createdMillis;
}
}
Then in your JPQL statement you could call the constructor:
Result result = entityManager.createQuery("select NEW fully.qualified.Result(dd, MAX(dd.createdMillis)) from T_DEBIT dd" +
" where dd.debitStatus in (:debitStatus)" +
" and dd.account = :account" , Result.class)
.setParameter("debitStatus", false)
.setParameter("account", account)
.getSingleResult();
Recently, I have blogged about this exact topic. I encourage you to view this video tutorial I created: https://tothought.cloudfoundry.com/post/16

How to use named parameter in plain sql with jooq

I'm using JOOQ with plain/raw SQL, so that means i'm not using any code generation or the fluid DSL thingy.
The following code works:
Connection connection = ...;
DSLContext context = DSL.using(connection, ...);
String sql = "select * from mytable t where (t.id = ?)";
String id = ...; //
Result<Record> result = context.fetch(sql, id);
Now let's say i have a query with multiple parameters like this:
String sql = "select * from mytable t where (t.id = ?) " +
"and (t.is_active = ?) and (t.total > ?)";
How do i use a named parameter with these types of queries?
I'm thinking something like :
String sql = "select * from mytable t where (t.id = :id) " +
"and (t.is_active = :is_active) and (t.total > :total)";
ResultQuery<Record> rq = context.resultQuery(sql);
rq.getParam("id").setValue(...);
rq.getParam("is_active").setValue(...);
rq.getParam("total").setValue(...);
Result<Record> result = rq.fetch();
But the above code doesn't work (for obvious reasons). Thanks in advance.
jOOQ currently doesn't support executing SQL with named parameters. You can use jOOQ to render named parameters if you're executing the query with another API, such as Spring JDBC. For more information, consider the manual:
http://www.jooq.org/doc/latest/manual/sql-building/bind-values/named-parameters
But the plain SQL templating API allows for re-using templates, e.g.
String sql = "select * "
+ "from mytable t "
+ "where t.id = {0} or (t.id != {0} and t.name = {1})";
ResultQuery<Record> q = ctx.resultQuery(sql, val(1), val("A"));
This way, you can at least re-use values several times.
Because Lukas said that this feature is not available, I thought I'll code a 'good enough' solution.
The idea is that the variable name like :name has to be replaced with the {0} at all places and the rest is done by JOOQ. I thought this is the easiest way of doing it. (Replacing variables with their proper form, like handling data types is definitely a lot of work.)
I merited some ideas from this other StackOverflow answer and then created this gist in Kotlin (it would have been too long in Java otherwise).
The current gist looks like this now:
import org.jooq.DSLContext
import org.jooq.Record
import org.jooq.ResultQuery
import org.jooq.impl.DSL
object SqlJooqBindVariableOrganizer {
data class Processed(
val statement: String,
val originalStatement: String,
val variables: List<Pair<String, Any>>,
) {
fun toResultQuery(context: DSLContext): ResultQuery<Record> {
return context.resultQuery(
statement,
*variables.map { DSL.`val`(it.second) }.toTypedArray(),
)
}
}
private fun extractBindVariableLocations(
statement: String,
): Map<String, List<IntRange>> {
// https://stackoverflow.com/a/20644736/4420543
// https://gist.github.com/ruseel/e10bd3fee3c2b165044317f5378c7446
// not sure about this regex, I haven't used colon inside string to test it out
return Regex("(?<!')(:[\\w]*)(?!')")
.findAll(statement)
.map { result ->
val variableName = result.value.substringAfter(":")
val range = result.range
variableName to range
}
.groupBy(
{ it.first },
{ it.second }
)
}
fun createStatement(
statement: String,
vararg variables: Pair<String, Any>,
): Processed {
return createStatement(statement, variables.toList())
}
fun createStatement(
statement: String,
variables: List<Pair<String, Any>>,
): Processed {
val locations = extractBindVariableLocations(statement)
val notProvidedKeys = locations.keys.subtract(variables.map { it.first })
if (notProvidedKeys.isNotEmpty()) {
throw RuntimeException("Some variables are not provided:\n"
+ notProvidedKeys.joinToString()
)
}
val relevantVariables = variables
// there may be more variables provided, so filter this
.filter { it.first in locations.keys }
// these locations should have the same order as the variables
// so it is important to know the proper order of the indices
val variableNameToIndex = relevantVariables
.mapIndexed { index, variable -> variable.first to index }
.associateBy({ it.first }, { it.second })
val variableNameReplacements = locations
.flatMap { (variableName, ranges) ->
ranges.map { range -> variableName to range }
}
// the replacements have to be done in a reversed order,
// as the replaced string is not equal length
.sortedByDescending { it.second.first }
// replace :name with {0}
val processedStatement = variableNameReplacements
.fold(statement) { statementSoFar, (variableName, range) ->
// index has to exist, we just checked it
val index = variableNameToIndex[variableName]!!
statementSoFar.replaceRange(range, "{$index}")
}
return Processed(
statement = processedStatement,
originalStatement = statement,
variables = relevantVariables,
)
}
}

Categories