CriteriaBuilder OffsetDateTime Comparison - spring-boot

I use Springboot, and I'd like to implement a "search" API for an entity that has an "OffsetDateTime" attribute.
The attribute is of type "OffsetDateTime" in my Hibernate entity :
#Column(name = "creation_date")
private OffsetDateTime creationDate;
The attribute that I get from the API is of type "OffsetDateTime" too.
I use the Springboot Specification, with the OffsetDateTime value as criteria.getValue() :
#Override
public Predicate toPredicate(Root<Job> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder builder) {
return builder.equal(root.get("creationDate"), criteria.getValue());
}
But the above code doesn't return me the entity with the specified date
However, the following code does :
#Override
public Predicate toPredicate(Root<Job> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder builder) {
Predicate predicate = builder.between(root.<OffsetDateTime>get("creationDate"),
((OffsetDateTime) criteria.getValue()).minus(1, ChronoUnit.MILLIS),
((OffsetDateTime) criteria.getValue()));
}
In my DB I have "2020-03-12 17:25:11.047", and the value I pass to the API is "2020-03-12T17:25:11.047+01:00". I'm in France, so I have a 1 hour offset, and the time is saved as a local time in the db, but that doesn't seem to be the problem as the second solution does return the entity I want.
Do you know why the first solution doesn't return me the entity, and how I could get the entity without having to set a 1 millisecond interval ?

Related

Subquery using CriteriaBuilder in Predicate overridden method Spring boot

I was trying to create one subquery in Springboot project, where I have created one custom class which is extending interface implements Specification<T> which gives overridden method #Override public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb).
Below is my SQL query which I would like to make it using CriteriaBuilder.
Select * from student_projects WHERE project_id IN (Select project_id
from student_courses where course_type = 3)
Below is the code snippet of the custom model class of implements Specification<T> which I have created,
#Override
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
query.distinct(true);
return root.join("student_courses", JoinType.LEFT).get("project_id").in("3");
}
Kindly suggest me if more information is needed. Thank you all

QueryDSL Predicate for use with JPARepository where field is a JSON String converted using an AttributeConverter to a List<Object>

I have a JPA Entity (Terminal) which uses an AttributeConverter to convert a Database String into a list of Objects (ProgrmRegistration). The converter just uses a JSON ObjectMapper to turn the JSON String into POJO objects.
Entity Object
#Entity
#Data
public class Terminal {
#Id
private String terminalId;
#NotEmpty
#Convert(converter = ProgramRegistrationConverter.class)
private List<ProgramRegistration> programRegistrations;
#Data
public static class ProgramRegistration {
private String program;
private boolean online;
}
}
The Terminal uses the following JPA AttributeConverter to serialize the Objects from and to JSON
JPA AttributeConverter
public class ProgramRegistrationConverter implements AttributeConverter<List<Terminal.ProgramRegistration>, String> {
private final ObjectMapper objectMapper;
private final CollectionType programRegistrationCollectionType;
public ProgramRegistrationConverter() {
this.objectMapper = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
this.programRegistrationCollectionType =
objectMapper.getTypeFactory().constructCollectionType(List.class, Terminal.ProgramRegistration.class);
}
#Override
public String convertToDatabaseColumn(List<Terminal.ProgramRegistration> attribute) {
if (attribute == null) {
return null;
}
String json = null;
try {
json = objectMapper.writeValueAsString(attribute);
} catch (final JsonProcessingException e) {
LOG.error("JSON writing error", e);
}
return json;
}
#Override
public List<Terminal.ProgramRegistration> convertToEntityAttribute(String dbData) {
if (dbData == null) {
return Collections.emptyList();
}
List<Terminal.ProgramRegistration> list = null;
try {
list = objectMapper.readValue(dbData, programRegistrationCollectionType);
} catch (final IOException e) {
LOG.error("JSON reading error", e);
}
return list;
}
}
I am using Spring Boot and a JPARepository to fetch a Page of Terminal results from the Database.
To filter the results I am using a BooleanExpression as the Predicate. For all the filter values on the Entity it works well, but the List of objects converted from the JSON string does not allow me to easily write an Expression that will filter the Objects in the list.
REST API that is trying to filter the Entity Objects using QueryDSL
#GetMapping(path = "/filtered/page", produces = MediaType.APPLICATION_JSON_VALUE)
public Page<Terminal> findFilteredWithPage(
#RequestParam(required = false) String terminalId,
#RequestParam(required = false) String programName,
#PageableDefault(size = 20) #SortDefault.SortDefaults({ #SortDefault(sort = "terminalId") }) Pageable p) {
BooleanBuilder builder = new BooleanBuilder();
if (StringUtils.isNotBlank(terminalId))
builder.and(QTerminal.terminal.terminalId.upper()
.contains(StringUtils.upperCase(terminalId)));
// TODO: Figure out how to use QueryDsl to get the converted List as a predicate
// The code below to find the programRegistrations does not allow a call to any(),
// expects a CollectionExpression or a SubqueryExpression for calls to eqAny() or in()
if (StringUtils.isNotBlank(program))
builder.and(QTerminal.terminal.programRegistrations.any().name()
.contains(StringUtils.upperCase(programName)));
return terminalRepository.findAll(builder.getValue(), p);
}
I am wanting to get any Terminals that have a ProgramRegistration object with the program name equal to the parameter passed into the REST service.
I have been trying to get CollectionExpression or SubQueryExpression working without success since they all seem to be wanting to perform a join between two Entity objects. I do not know how to create the path and query so that it can iterate over the programRegistrations checking the "program" field for a match. I do not have a QProgamRegistration object to join with, since it is just a list of POJOs.
How can I get the predicate to match only the Terminals that have programs with the name I am searching for?
This is the line that is not working:
builder.and(QTerminal.terminal.programRegistrations.any().name()
.contains(StringUtils.upperCase(programName)));
AttributeConverters have issues in Querydsl, because they have issues in JPQL - the query language of JPA - itself. It is unclear what actually the underlying query type of the attribute is, and whether the parameter should be a basic type of that query type, or should be converted using the conversion. Such conversion, whilst it appears logical, is not defined in the JPA specification. Thus a basic type of the query type needs to be used instead, which leads to new difficulties, because Querydsl can't know the type it needs to be. It only knows the Java type of the attribute.
A workaround can be to force the field to result into a StringPath by annotating the field with #QueryType(PropertyType.STRING). Whilst this fixes the issue for some queries, you will run into different issues in other scenarios. For more information, see this thread.
Although the following desired QueryDsl looks like it should work
QTerminal.terminal.programRegistrations.any().name().contains(programName);
In reality JPA would never be able to convert it into something that would make sense in terms of SQL. The only SQL that JPA could convert it into could be as follows:
SELECT t.terminal_id FROM terminal t where t.terminal_id LIKE '%00%' and t.program_registrations like '%"program":"MY_PROGRAM_NAME"%';
This would work in this use case, but be semantically wrong, and therefore it is correct that it should not work. Trying to select unstructured data using a structured query language makes no sense
The only solution is to treat the data as characters for the DB search criteria, and to treat it as a list of Objects after the query completes and then perform filtering of the rows in Java. Although This makes the paging feature rather useless.
One possible solution is to have a secondary read only String version of the column that is used for the DB search criteria, that is not converted to JSON by the AttributeConverter.
#JsonIgnore
#Column(name = "programRegistrations", insertable = false, updatable = false)
private String programRegistrationsStr;
The real solution is do not use unstructured data when you want structured queries on that data Therefore convert the data to either a database that supports the JSON natively for queries or model the data correctly in DDL.
To have a short answer: the parameter used in the predicate on attribute with #QueryType must be used in another predicate on attribute of type String.
It's a clearly known issue describe in this thread: https://github.com/querydsl/querydsl/issues/2652
I simply want to share my experience about this bug.
Model
I have an entity like
#Entity
public class JobLog {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private String id;
#QueryType(PropertyType.STRING)
private LocalizedString message;
}
Issue
I want to perform some predicate about message. Unfortunately, with this configuration, I can't do this:
predicates.and(jobLog.message.likeIgnoreCase(escapedTextFilter));
because I have the same issues that all people!
Solution
But I find a way to workaround :)
predicates.and(
(jobLog.id.likeIgnoreCase(escapedTextFilter).and(jobLog.id.isNull()))
.or(jobLog.message.likeIgnoreCase(escapedTextFilter)));
Why it workaround the bug?
It's important that escapedTextFilter is the same in both predicate!
Indeed, in this case, the constant is converter to SQL in the first predicate (which is of String type). And in the second predicate, we use the conterted value
Bad thing?
Add a performance overflow because we have OR in predicate
Hope this can help someone :)
I've found one way to solve this problem, my main idea is to use mysql function cast(xx as char) to cheat hibrenate. Below is my base info. My code is for work , so I've made an example.
// StudentRepo.java
public interface StudentRepo<Student, Long> extends JpaRepository<Student, Long>, QuerydslPredicateExecutor<Student>, JpaSpecificationExecutor<Student> {
}
// Student.java
#Data
#AllArgsConstructor
#NoArgsConstructor
#EqualsAndHashCode(of = "id")
#Entity
#Builder
#Table(name = "student")
public class Student {
#Convert(converter = ClassIdsConvert.class)
private List<String> classIds;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
// ClassIdsConvert.java
public class ClassIdsConvert implements AttributeConverter<List<String>, String> {
#Override
public String convertToDatabaseColumn(List<String> ips) {
// classid23,classid24,classid25
return String.join(",", ips);
}
#Override
public List<String> convertToEntityAttribute(String dbData) {
if (StringUtils.isEmpty(dbData)) {
return null;
} else {
return Stream.of(dbData.split(",")).collect(Collectors.toList());
}
}
}
my db is below
id
classIds
name
address
1
2,3,4,11
join
北京市
2
2,31,14,11
hell
福建省
3
2,12,22,33
work
福建省
4
1,4,5,6
ouy
广东省
5
11,31,34,22
yup
上海市
-- ----------------------------
-- Table structure for student
-- ----------------------------
DROP TABLE IF EXISTS `student`;
CREATE TABLE `student` (
`id` int(11) NOT NULL,
`classIds` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
Use JpaSpecificationExecutor solve the problem
Specification<Student> specification = (root, query, criteriaBuilder) -> {
String classId = "classid24"
String classIdStr = StringUtils.wrap(classId, "%");
var predicate = criteriaBuilder.like(root.get("classIds").as(String.class), classIdStr);
return criteriaBuilder.or(predicate);
};
var students = studentRepo.findAll(specification);
log.info(new Gson().toJson(students))
attention the code root.get("classIds").as(String.class)
In my opinion, if I don't add .as(String.class) , hibernate will think the type of student.classIds is list and throw an Exception as below.
SQL will like below which runs correctly in mysql. But hibnerate can't work.
org.springframework.dao.InvalidDataAccessApiUsageException: Parameter value [%classid24%] did not match expected type [java.util.List (n/a)]; nested exception is java.lang.IllegalArgumentException: Parameter value [%classid24%] did not match expected type [java.util.List (n/a)]
SELECT
student0_.id AS id1_0_,
student0_.class_ids AS class_ids2_0_
FROM
student student0_
WHERE
student0_.class_ids LIKE '%classid24%' ESCAPE '!'
if you add .as(String.class) , hibnerate will think the type of student.classIds as string and won't check it at all.
SQL will be like below which can run correct in mysql. Also in JPA.
SELECT
student0_.id AS id1_0_,
student0_.class_ids AS class_ids2_0_
FROM
student student0_
WHERE
cast( student0_.class_ids AS CHAR ) LIKE '%classid24%' ESCAPE '!'
when the problem is solved by JpaSpecificationExecutor, so I think this can be solve also in querydsl. At last I find the template idea in querydsl.
String classId = "classid24";
StringTemplate st = Expressions.stringTemplate("cast({0} as string)", qStudent.classIds);
var students = Lists.newArrayList<studentRepo.findAll(st.like(StringUtils.wrap(classId, "%"))));
log.info(new Gson().toJson(students));
it's sql is like below.
SELECT
student0_.id AS id1_0_,
student0_.class_ids AS class_ids2_0_
FROM
student student0_
WHERE
cast( student0_.class_ids AS CHAR ) LIKE '%classid24%' ESCAPE '!'

Spring Boot JPA Specification for Compare Dates

I need to find the table records which will come between the dates passed by the user. I am trying to write a specification for this but it is showing me compile time error as below :
The method between(Expression<? extends Y>, Expression<? extends Y>, Expression<? extends Y>)
in the type CriteriaBuilder
is not applicable for the arguments (Expression<Date>, Object, Object)
I have tried search on the various forums but didn't able to get how to solve this issue,may be I am doing something wrong. Please help me on this.
Specification Class
public class ScheduleClassSpecification implements Specification<ScheduleClassInformation> {
private SearchCriteria criteria;
#Override
public Predicate toPredicate(Root<ScheduleClassInformation> root,
CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
Expression<String> expression;
Predicate predicate =null;
if((criteria.getKey().equalsIgnoreCase("student"))){
expression = root.join("course").join("student").get("student");
predicate = criteriaBuilder.equal(expression, criteria.getValue());
}else if(criteria.getKey().equalsIgnoreCase("startDate") || criteria.getKey().equalsIgnoreCase("endDate")){
predicate = criteriaBuilder.between
(root.<Date>get(criteria.getKey()).as(java.util.Date.class),
criteria.getValue(),
criteria.getValue()); // Compile Time Error on this line
}
return predicate;
}
Criteria Class
public class SearchCriteria {
private String key;
private String operation;
private Object value;}
Service For Extracting the record from Repository
if(!Utility.isNull(Id)){
idSpec = new ScheduleClassSpecification(new SearchCriteria("Student",":",Id));
}
Page<SCOutput> listreturn = scRepo.findAll(Specification.where(idSpec), SCOutput.class,new PageRequest(0, 100));
As the compiler error tells you need an Expression.
You can easily do that by replacing criteria.getValue() with criteriaBuilder.literal(criteria.getValue())

How can I use Spring's pagination (using Pageable) while writing a dynamic query using QueryDSL?

I am trying to use pagination with QueryDSL - using the com.mysema.querydsl package.
All my Querydsl query types look like this -
#Generated("com.mysema.query.codegen.EntitySerializer")
public class QCountry extends EntityPathBase<Country> {...}
Currently, my repository implementation class looks something like this -
#Override
public Page<Country> findPaginatedCountries(String country, Optional<String> status, Pageable pageable) {
QCountry qCountry= QCountry.someObject;
QActiveCountry qActiveCountry = QActiveCountry.activeCountry;
JPAQuery jpaQuery = new JPAQuery(entityManager);
QueryBase queryBase = jpaQuery.from(qCountry).innerJoin(qActiveCountry).fetch()
.where(qCountry.codeLeft.country.upper().eq(country.toUpperCase()))
.where(qCountry.codeRight.country.upper().eq(country.toUpperCase()));
if(status.isPresent()){
queryBase = queryBase.where(qActiveCountry.id(qCountry.active.id))
.where(qActiveCountry.status.upper().eq(status.get().toUpperCase()));
}
.......}
Now, I want this dynamic query to return a paginated response. I want to use Spring's pagination to do that and not manually set offset, size etc.
I know I can use QueryDslRepositorySupport class - as implemented here - https://github.com/keke77/spring-data-jpa-sample/blob/master/spring-data-jpa/src/main/java/com/gmind7/bakery/employee/EmployeeRepositoryImpl.java
Sample code from the above link -
#Override
public Page<Employees> QFindByOfficeCode(long officeCode, Pageable pageable) {
//JPAQuery query = new JPAQuery(em);
JPQLQuery query = from(QEmployees.employees).where(QEmployees.employees.officeCode.eq(officeCode));
query = super.getQuerydsl().applyPagination(pageable, query);
SearchResults<Employees> entitys = query.listResults(QEmployees.employees);
return new PageImpl<Employees>(entitys.getResults(), pageable, entitys.getTotal());
}
However, to do that -
I need to pass JPQLQuery object to the applyPagination method. How can I do that without changing my code (Ofcourse, the repository class will extend QueryDslRepositorySupport class). Currently, I am using JPAQuery as you can see.
OR
I probably need to change my QueryDSL types by having them extend EntityPath instead of EntityPathBase so that I can use JPQLQuery.from() to generate the query and then use the applyPagination method, which requires a JPQLQuery object. However, my Q classes are extending EntityPathBase class instead. Should I be use com.querydsl package instead of com.mysemsa.querydsl package to generate query types?
OR
Other option is to use the following - http://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/querydsl/QueryDslPredicateExecutor.html#findAll-com.querydsl.core.types.Predicate-org.springframework.data.domain.Pageable-
Code snippet below -
Page<T> page = QueryDslPredicateExecutor.findAll(org.springframework.data.querydsl.Predicate predicate, Pageable pageable)
However, I am making joins between two tables and then filtering results with a where clause (as you can see above in my code). How can I pass a predicate object in the findAll method above? Not sure how to include a join in it.
Please let me know if the problem is not clear, I can add more details.
EDIT: There is a many to one relationship between Country and ActiveCountry.
Country class has an ActiveCountry reference. And we have to do a join between both ids. Is is possible that Country can have null ActiveCountry. Therefore, we want an inner join - only non null values for active country
#ManyToOne
#JoinColumn(name="id")
ActiveCountry active;
Step 1: Annotate the entity class with #QueryEntity
#Entity
#QueryEntity
public class Country {}
This seems to have been addressed already since the question shows Q classes.
Step 2: Have the repository interface extend QueryDslPredicateExecutor
public interface CountryRepository
extends PagingAndSortingRepository<Country, Long>
, QueryDslPredicateExecutor<Country> {
}
Step 3: Invoke the Page<T> findAll(Predicate query, Pageable page) method provided by QueryDslPredicateExecutor
public Page<Country> getCountries(String country, Optional<String> status, Pageable page) {
QCountry root = QCountry.country;
BooleanExpression query = root.codeLeft.country.equalsIgnoreCase(country);
query = query.and(root.codeRight.country.equalsIgnoreCase(country));
if (status.isPresent()) {
query = query.and(root.active.status.equalsIgnoreCase(status));
}
return countryRepository.findAll(query, page);
}

Spring data JPA Specifications - #OneToMany dependency

i have a problem with getting List from entity Person using Spring data JPA specifications (because of pagination). I need to get all notes by person but dependency between these two entities is on Person side. I don't know how to create my Predicate because Note doesn't contain any attribute related to Person.
I simply can get List with Persons getter but i can't use this way because i need returned data paginated.
#Entity
public class Person implements Serializable {
#Id
private Long personId;
#OneToMany
#JoinColumn(name = "personId")
private List<Note> notes;
}
#Entity
public class Note implements Serializable {
#Id
private Long noteId;
}
Normally, I would write something like this, but i don't have an attribute person in Note and database can't be remapped at this stage.
public static Specification<Note> notesByPerson(final Long personId) {
return new Specification<Note>() {
#Override
public Predicate toPredicate(final Root<Note> root, final CriteriaQuery<?> query,
final CriteriaBuilder builder) {
final Path<Person> per = root.<Person> get("person");
return builder.equal(per.<Long> get("personId"), personId);
}
};
}
Thank you,
Zdend
Solved..
public static Specification<Note> notesByPerson(final Long personId) {
return new Specification<Note>() {
#Override
public Predicate toPredicate(final Root<Note> noteRoot, final CriteriaQuery<?> query,
final CriteriaBuilder cb) {
final Subquery<Long> personQuery = query.subquery(Long.class);
final Root<Person> person = personQuery.from(Person.class);
final Join<Person, Note> notes = person.join("notes");
personQuery.select(notes.<Long> get("noteId"));
personQuery.where(cb.equal(person.<Long> get("personId"), personId));
return cb.in(noteRoot.get("noteId")).value(personQuery);
}
};
}
I am not sure how to do that with Predicates, as I usually dont use them, but in JPQL (or HQL, which is similar), you can do something like this:
SELECT Note n FROM Person.notes WHERE XXXX
It is basically the same thing as doing this in SQL
SELECT n.noteId FROM person as p JOIN persons_notes pn ON pn.person=p.personId JOIN notes as n ON n.noteId=pn.noteId
I would venture a guess that the Predicate method has similar abilities as described above.

Resources