Spring Boot JPA Specification for Compare Dates - spring-boot

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

Related

custom sql queries in JPA Criteria Predicate, StringPath, Querydsl

I have a Spring boot project with hibernate 5.4.12, Java 11 and Postgres.
I am trying to build a custom Sort/Filter mechanism using JPA and Querydsl, here is one blog for reference.
We have a gin index column which is used for full text search feature by postgres. In jpa repository, I can query the column easily as below
#Query(value = "select * from products where query_token ## plainto_tsquery(:query)", nativeQuery = true)
Page<Product> findAllByTextSearch(#Param("query") String query, Pageable pageable);
I am aware that fts queries are not yet supported by JPA criteria or querydsl APIs (I may be wrong). Since normal filtering logic will go through criteria API, how do add fts capabilities in criteria API? Is there a way to add custom native query as predicate or StringPath or any other Qtype paths?
UPDATE
My SearchPredicate class
public class SearchPredicate<E extends Enum<E>> {
private SearchCriteria<E> searchCriteria;
public <T> BooleanExpression getPredicate(Class<T> entityClass, String entityName) {
PathBuilder<T> entityPath = new PathBuilder<>(entityClass, entityName);
switch (searchCriteria.getPathType()) {
case String:
StringPath stringPath = entityPath.getString(searchCriteria.getKey());
return stringPath.eq(searchCriteria.getStringValue());
case Enum:
return entityPath.getEnum(searchCriteria.getKey(), searchCriteria.getEnumClass())
.eq(Enum.valueOf(searchCriteria.getEnumClass(), searchCriteria.getStringValue()));
case Float:
NumberPath<Float> floatPath = entityPath.getNumber(searchCriteria.getKey(), Float.class);
Float floatValue = Float.parseFloat(searchCriteria.getStringValue());
return floatPath.eq(floatValue);
case Integer:
NumberPath<Integer> integerPath = entityPath.getNumber(searchCriteria.getKey(), Integer.class);
Integer integerValue = Integer.parseInt(searchCriteria.getStringValue());
return integerPath.eq(integerValue);
}
return null;
}
}
My SearchCriteria class
public class SearchCriteria<E extends Enum<E>> {
private String key;
private Object value;
private PathType pathType;
private Class<E> enumClass;
public String getStringValue() {
return value.toString();
}
}
And My PathType Enum
public enum PathType {
String, Enum, Integer, Float;
}
On these same lines, I am assuming/expecting something for text search as well e.g.
case Search:
FtsPath ftsPath = entityPath.getFtsPath("query_token");
return ftsPath.search("some search string")
You should first make the ## operator available by registering a custom function for your ORM. Then you can do plainto_tsquery(query_token, :query) in your JPQL query. How to register a custom function depends on the ORM you use. Assuming you use Hibernate, you're probably best of using the MetadataContributor SPI because functions registered through the Dialect have less flexibility with regard to the underlying SQL rendering AFAIK.
Then, if you want to use this in QueryDSL, you'd have to create a custom Operator and register a Template for that Operator in a subclass of JPQLTemplates. Alternatively, you can bypass the Operation expressions using a simple TemplateExpression: Expressions.booleanTemplate("plainto_tsquery({0}, {1})", QProduct.product.queryToken, query), which returns a predicate.

CriteriaBuilder OffsetDateTime Comparison

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 ?

Multiple aliases with QuerydslBinderCustomizer

I'm using QuerydslPredicate in my RestController on an entity which has a date object, I want to be able to query for a date before/after/between given dates, hoping to have something like
GET /problems?createdOnAfter=XXX
GET /problems?createdOnBefore=YYY
GET /problems?createdOnAfter=XXX&createdOnBefore=YYY
My entity has the date field createdOn and I was hoping I could customise bindings for an entity path using multiple aliases i.e. adding aliases createdOnAfter & createdOnBefore - it doesn't look like I can create multiple aliases though, e.g.
#Repository
public interface ProblemRepository extends JpaRepository<Problem, String>, QueryDslPredicateExecutor<Problem>,
QuerydslBinderCustomizer<QProblem> {
....
#Override
default void customize(QuerydslBindings bindings, QProblem root) {
bindings.bind(root.createdOn).as("createdOnAfter").first(TemporalExpression::after);
bindings.bind(root.createdOn).as("createdOnBefore").first(TemporalExpression::before);
}
}
The before alias is obviously overwriting the after one.
What's the correct approach to avoid having to manually create the predicates?
Why not using QueryDSL Predicate ? You could do :
#GetMapping("/problems")
#Timed
public ResponseEntity<List<ProblemDTO>> getAllProblems(
#RequestParam(required = false) LocalDateTime createdOnAfter,
#RequestParam(required = false) LocalDateTime createdOnBefore,
#ApiParam Pageable pageable) {
BooleanBuilder where = new BooleanBuilder();
if (startDate != null) {
where = where.and(problem.createdOn.after(createdOnAfter));
}
if (endDate != null) {
where = where.and(problem.createdOn.before(createdOnBefore));
}
Page<Donnee> page = problemRepository.findAll(where, pageable);
return new ResponseEntity<>(problemMapper.toDTO(page.getContent())), null, HttpStatus.OK);
}
Hope it helps,
Regards
How about adding new Entity properties and marking those as transient like this:
#Transient
private Instant createdOnBefore;
#Transient
private Instant createdOnAfter;
and then customise your repository class like this:
#Override
default void customize(final QuerydslBindings bindings, final QProblem root) {
bindings.bind(root.createdOnBefore).first((path, value) -> root.createdOn.goe(value));
bindings.bind(root.createdOnAfter).first((path, value) -> root.createdOn.loe(value));
}

Spring Data - QueryDSL InnerJoin predicate

Lets assume we have the following domain objects (partially complete to reduce code).
public class Student {
#OneToMany(mappedBy="student")
List<Assignment> assignments;
}
public class Assignment {
#ManyToOne
Student student;
#OneToOne
Implementation implementation;
}
public class Implementation {
#OneToOne
Assignment assignment;
#OneToMany(mappedBy="implementation")
List<Assessment> assessments;
}
public class Assessment {
#ManyToOne
Implementation implementation;
String grade;
}
So the query I want to perform is "Select all students whose assignment implementation has been performed (not null) and has not been assessed at all (List<Assessment>#isEmpty())
So I'm using QueryDSL and try to use the following query just to get Students with non-implemented assignments
public class MyService {
#Autowired
private StudentRepository studentRepository;
public Iterable<Student> foo() {
return studentRepository.findAll(
QStudent.student.assignments.any().implementation.isNotNull()
);
}
}
and the above query seems to get ignored.
Then I use the following to get the combination (implemented exercises and empty assessments)
studentRepository.findAll(
QStudent.student.assignments.any().implementation.isNotNull()
.and(QStudent.student.assignments.any().implementation.assessments.isEmpty())
);
Again this makes no differences. Any idea what I'm doing wrong
Have you tried this:
QAssignment assignment = QAssignment.assignment;
ListSubQuery<Assignment> subQuery = new JPASubQuery().from(assignment)
.where(assignment.implementation.isNotNull()))
.and(assignment.implementation.assessments.isEmpty())
.list(assignment);
studentRepository.findAll(
QStudent.student.assignments.contains(subQuery)
);
I'm not sure why your code is not working, but I guess it's because
any() is only a subquery exists shortcut.
See https://stackoverflow.com/a/25453708/2672352

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