I have slightly modified the implementation so that there is no need to copy-paste aliases and functions
abstract class ReusableJoinSpecification<T> implements Specification<T> {
protected <F extends From<FF, FR>, FF, FR, J extends Join<JF, JR>, JF, JR> J getOrCreateJoin(F from,
JoinData<F, J> joinData) {
Set<Join<FR, ?>> joins = from.getJoins();
//noinspection unchecked
Optional<J> optionalJoin = (Optional<J>) findJoin(joins, joinData.getAlias());
return optionalJoin.orElseGet(() -> {
J join = joinData.getCreationFunction().apply(from);
join.alias(joinData.getAlias());
return join;
}
);
}
private Optional<Join<?, ?>> findJoin(@NotNull Set<? extends Join<?, ?>> joins, @NotNull String alias) {
List<Join<?, ?>> joinList = new ArrayList<>(joins);
for (Join<?, ?> join : joinList) {
if (alias.equals(join.getAlias())) {
return Optional.of(join);
}
}
for (Join<?, ?> j : joinList) {
Optional<Join<?, ?>> res = findJoin(j.getJoins(), alias);
if (res.isPresent()) {
return res;
}
}
return Optional.empty();
}
JoinData:
@Data
class JoinData<F extends From<?, ?>, J extends Join<?, ?>> {
@NotNull
private final String alias;
@NotNull
private final Function<F, J> creationFunction;
}
Usage:
private final JoinData<Root<Project>, Join<Project, Contractor>> contractorJoinData =
new JoinData<>("contractor", root -> root.join(Project_.contractor));
private final JoinData<Join<Project, Contractor>, Join<Contractor, Address>> contractorLegalAddressJoinData =
new JoinData<>("contractorLegalAddress", root -> root.join(Contractor_.legalAddress));
public Specification<Project> contractorLegalAddressCityLike(String address) {
if (address == null)
return null;
return new ReusableJoinSpecification<>() {
@Override
public Predicate toPredicate(Root<Project> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
Join<Project, Contractor> contractorJoin = getOrCreateJoin(root, contractorJoinData);
Join<Contractor, Address> contractorAddressJoin = getOrCreateJoin(contractorJoin, contractorLegalAddressJoinData);
return criteriaBuilder.like(contractorAddressJoin.get(Address_.city), simpleLikePattern(address));
}
};
}