Issue
I'm having an Activity
entity which is in @ManyToOne
relationship with Event
entity and their corresponding metamodels - Activity_
and Event_
were generated by JPA model generator.
I've created specialized classes ActivitySpecifications
and EventSpecifications
. Those classes contain only static methods whose return Specification
. For example:
public interface EventSpecifications {
static Specification<Event> newerThan(LocalDateTime date) {
return (root, cq, cb) -> cb.gt(Event_.date, date);
}
...
}
so when I want to build query matching multiple specifications, I can execute following statement using findAll
on JpaSpecificationExecutor<Event>
repository.
EventSpecifications.newerThan(date).and(EventSpecifications.somethingElse())
and ActivitySpecifications
example:
static Specification<Activity> forActivityStatus(int status) { ... }
How do I use EventSpecifications
from ActivitySpecifications
? I mean like merge specifications of different type. I'm sorry, but I don't even know how to ask it properly, but theres simple example:
I want to select all activities with status = :status
and where activity.event.date
is greater than :date
static Specification<Activity> forStatusAndNewerThan(int status, LocalDateTime date) {
return forActivityStatus(status)
.and((root, cq, cb) -> root.get(Activity_.event) ....
// use EventSpecifications.newerThan(date) somehow up there
}
Is something like this possible?
The closest thing that comes to my mind is using the following:
return forActivityStatus(status)
.and((root, cq, cb) -> cb.isTrue(EventSpecifications.newerThan(date).toPredicate(???, cq, cb));
where ???
requires Root<Event>
, but I can only get Path<Event>
using root.get(Activity_.event)
.
Solution
In its basic form, specifications are designed to be composable only if they refer to the same root.
However, it shouldn't be too difficult to introduce your own interface which is easily convertible to Specification
and which allows for specifications refering to arbitrary entities to be composed.
First, you add the following interface:
@FunctionalInterface
public interface PathSpecification<T> {
default Specification<T> atRoot() {
return this::toPredicate;
}
default <S> Specification<S> atPath(final SetAttribute<S, T> pathAttribute) {
// you'll need a couple more methods like this one for all flavors of attribute types in order to make it fully workable
return (root, query, cb) -> {
return toPredicate(root.join(pathAttribute), query, cb);
};
}
@Nullable
Predicate toPredicate(Path<T> path, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);
}
You then rewrite the specifications as follows:
public class ActivitySpecifications {
public static PathSpecification<Activity> forActivityStatus(ActivityStatus status) {
return (path, query, cb) -> cb.equal(path.get(Activity_.status), cb.literal(status));
}
}
public class EventSpecifications {
public static PathSpecification<Event> newerThan(LocalDateTime date) {
return (path, cq, cb) -> cb.greaterThanOrEqualTo(path.get(Event_.createdDate), date);
}
}
Once you've done that, you should be able to compose specifications in the following manner:
activityRepository.findAll(
forActivityStatus(ActivityStatus.IN_PROGRESS).atRoot()
.and(newerThan(LocalDateTime.of(2019, Month.AUGUST, 1, 0, 0)).atPath(Activity_.events))
)
The above solution has the additional advantage in that specifying WHERE
criteria is decoupled from specifying paths, so if you have multiple associations between Activity
and Event
, you can reuse Event
specifications for all of them.
Answered By - crizzis
Answer Checked By - Dawn Plyler (JavaFixing Volunteer)