Created
November 30, 2021 11:23
-
-
Save zalito12/86f4161ac8759464fa594cea17737246 to your computer and use it in GitHub Desktop.
Bindable Spec Query
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Base class to create an specification builder with helper methods for bind parameters of any type. | |
* | |
* @param <EntityT> Specification entity type | |
*/ | |
public abstract class AbstractSpecificationBuilder<EntityT> implements BindableSpecification<EntityT> { | |
private Specification<EntityT> spec; | |
private final Map<String, Object> params; | |
public AbstractSpecificationBuilder() { | |
spec = null; | |
params = new HashMap<>(); | |
} | |
/** | |
* Turns the specification into a predicate. | |
* | |
* @return A predicate based on current specification to use in a query where clause. | |
*/ | |
@Override | |
@Nullable | |
public <T> Predicate toPredicate(Root<EntityT> root, CriteriaQuery<T> criteriaQuery, | |
CriteriaBuilder criteriaBuilder) { | |
return nonNull(spec) ? spec.toPredicate(root, criteriaQuery, criteriaBuilder) : null; | |
} | |
/** | |
* Binds query predicate parameters with parameters values set on specification build. | |
* | |
* @param query Query with named parameters to set values on | |
* @param <T> Query type | |
*/ | |
@Override | |
@Nullable | |
public <T> void bind(TypedQuery<T> query) { | |
if (!CollectionUtils.isEmpty(params)) { | |
params.forEach(query::setParameter); | |
} | |
} | |
/** | |
* Applies an specification function to the current specification filter with parameter as literal. | |
* | |
* @param filterF specification function | |
* @param param parameter value | |
* @param <T> parameter type | |
*/ | |
public <T> void apply(@NonNull Function<T, Specification<EntityT>> filterF, @Nullable T param) { | |
if (isNullOrEmpty(param)) { | |
return; | |
} | |
Specification<EntityT> nextSpec = filterF.apply(param); | |
if (!isNull(nextSpec)) { | |
spec = isNull(spec) ? nextSpec : spec.and(nextSpec); | |
} | |
} | |
/** | |
* Applies an specification function to the current specification filter binding the parameter. | |
* | |
* @param filterF specification function | |
* @param name bound parameter name | |
* @param param parameter value | |
* @param <T> parameter type | |
*/ | |
public <T> void apply(@NonNull BiFunction<String, T, Specification<EntityT>> filterF, String name, | |
@Nullable T param) { | |
if (isNullOrEmpty(param)) { | |
return; | |
} | |
Specification<EntityT> nextSpec = filterF.apply(name, param); | |
if (!isNull(nextSpec)) { | |
params.put(name, param); | |
spec = isNull(spec) ? nextSpec : spec.and(nextSpec); | |
} | |
} | |
/** | |
* Checks if the parameter is null and if it's an instance of List it will check for empty list. | |
* | |
* @param param parameter value | |
* @param <T> parameter type | |
* @return true if parameter is null or an empty list. | |
*/ | |
public static <T> boolean isNullOrEmpty(@Nullable T param) { | |
return Optional.ofNullable(param) | |
.map(p -> (p instanceof List) && ((List) p).isEmpty()) | |
.orElse(true); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* This interface provides the required methods to create a query with bound params from a jpa specification. | |
* | |
* @param <EntityT> The root entity class | |
*/ | |
public interface BindableSpecification<EntityT> { | |
@Nullable | |
<T> Predicate toPredicate(Root<EntityT> root, CriteriaQuery<T> criteriaQuery, CriteriaBuilder criteriaBuilder); | |
@Nullable | |
<T> void bind(TypedQuery<T> query); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class CaseJpaRepository implements CaseRepository { | |
private final JpaRepositoryRxManager manager; | |
private final CaseEntityMapper caseMapper; | |
private final PageEntityMapper pageEntityMapper; | |
private final Integer daysToBeOlder; | |
private final Boolean skipSkus; | |
private final JpaBindableSpecificationClient jpaBindableSpecClient; | |
@Override | |
public Mono<Page<MyCase>> find(CaseSearchParams searchParams, PageParams<CaseSort> pageParams) { | |
CaseLiteSpecsBuilder filter = CaseSpecs.builder() | |
.hasCurrentUser(searchParams.getCurrentUser()) | |
.hasNotCurrentUser(searchParams.getCurrentUser()) | |
.isSensitive(searchParams.isSensitive()) | |
.hasBusiness(searchParams.getBusinessId()) | |
.isOld(searchParams.isOlder(), daysToBeOlder) | |
.statusIn(searchParams.getStatus()) | |
.hasCurrentBox(searchParams.getCurrentBox()) | |
.hasCurrentDepartment(searchParams.getCurrentDepartment()) | |
.hasCurrentBrand(searchParams.getBrandId()) | |
.contextIdIn(searchParams.getContextId()) | |
.storeIdIn(searchParams.getStoreId()); | |
return manager.invokePaged( | |
pageParams, | |
pageEntityMapper::pageParamsWithSort, | |
pageable -> jpaBindableSpecClient.findPage(CaseEntity.class, filter, "case-entity-graph", pageable), | |
caseEntity -> { | |
MyCase myCase = caseMapper.toDomain(caseEntity, skipSkus); | |
return myCase.toBuilder().older(validateOlder(myCase.getCreationDate(), daysToBeOlder)).build(); | |
} | |
).map(page -> page.toBuilder() | |
.data( | |
page.getData() | |
.stream() | |
.sorted(comparator(pageParams)) | |
.collect(Collectors.toList()) | |
).build()); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public interface CaseRepository { | |
Mono<Page<MyCase>> find(CaseSearchParams searchParams, PageParams<CaseSort> pageParams); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class CaseSpecs { | |
private static final BiFunction<String, Long, Specification<CaseEntity>> hasCurrentUser = | |
(@NonNull String paramName, @NonNull Long userId) -> (Specification<CaseEntity>) (root, query, builder) -> builder | |
.equal(root.<Long>get("assignedUser"), builder.parameter(Long.class, paramName)); | |
private static final Function<Boolean, Specification<CaseEntity>> hasNotCurrentUser = | |
(@NonNull Boolean isNullUserId) -> (Specification<CaseEntity>) (root, query, builder) -> isNullUserId ? builder | |
.isTrue(root.<Long>get("assignedUser").isNull()) : null; | |
private static final Function<Boolean, Specification<CaseEntity>> isSensitive = | |
(@NonNull Boolean isSensitive) -> (Specification<CaseEntity>) (root, query, builder) -> isSensitive ? builder | |
.isTrue(root.<CaseEntity, SensitivityEntity>join("sensitive").isNotNull()) : null; | |
private static final BiFunction<String, LocalDateTime, Specification<CaseEntity>> isOldLess = | |
(@NonNull String paramName, @NonNull LocalDateTime olderDate) -> (Specification<CaseEntity>) (root, query, builder) -> builder | |
.lessThan(root.get("creationDate"), builder.parameter(LocalDateTime.class, paramName)); | |
private static final BiFunction<String, LocalDateTime, Specification<CaseEntity>> isOldGreater = | |
(@NonNull String paramName, @NonNull LocalDateTime olderDate) -> (Specification<CaseEntity>) (root, query, builder) -> builder | |
.greaterThan(root.get("creationDate"), builder.parameter(LocalDateTime.class, paramName)); | |
private static final BiFunction<String, Long, Specification<CaseEntity>> hasCurrentDepartment = | |
(@NonNull String paramName, @NonNull Long departmentId) -> (Specification<CaseEntity>) (root, query, builder) -> builder | |
.equal(root.<Long>get("assignedDepartment"), builder.parameter(Long.class, paramName)); | |
private static final BiFunction<String, Short, Specification<CaseEntity>> hasCurrentBrand = | |
(@NonNull String paramName, @NonNull Short brandId) -> (Specification<CaseEntity>) (root, query, builder) -> builder | |
.equal(root.<StoreEntity>get("store").<MasterStoreEntity>get("ecommerceEntity").<Short>get("brandId"), | |
builder.parameter(Short.class, paramName)); | |
private static final BiFunction<String, Long, Specification<CaseEntity>> hasCurrentBox = | |
(@NonNull String paramName, @NonNull Long boxId) -> (Specification<CaseEntity>) (root, query, builder) -> builder | |
.equal(root.<Long>get("assignedBox"), builder.parameter(Long.class, paramName)); | |
private static final Function<List<Integer>, Specification<CaseEntity>> statusIn = | |
(@NonNull List<Integer> statusIds) -> (Specification<CaseEntity>) (root, query, builder) -> root | |
.<StatusTypeEntity>get("status").<String>get("id").in(statusIds); | |
private static final BiFunction<String, List<Integer>, Specification<CaseEntity>> businessIn = | |
(@NonNull String paramName, @NonNull List<Integer> businessIds) -> (Specification<CaseEntity>) (root, query, builder) -> root | |
.<Integer>get("business").in(builder.parameter(List.class, paramName)); | |
private static final BiFunction<String, List<Integer>, Specification<CaseEntity>> storesIn = | |
(@NonNull String paramName, @NonNull List<Integer> storeIds) -> (Specification<CaseEntity>) (root, query, builder) -> root | |
.<StoreEntity>get("store").<Integer>get("id").in(builder.parameter(List.class, paramName)); | |
private static final BiFunction<String, List<Integer>, Specification<CaseEntity>> hasAnyContextTypeIdIn = | |
(@NonNull String paramName, @NonNull List<Integer> contextTypeIds) -> (Specification<CaseEntity>) (root, query, builder) -> { | |
Subquery<CaseContextEntity> subquery = query.subquery(CaseContextEntity.class); | |
Root<CaseContextEntity> subqueryRoot = subquery.from(CaseContextEntity.class); | |
subquery.select(subqueryRoot); | |
Predicate caseIdPredicate = builder.equal(subqueryRoot.get("caseId"), root.<String>get("id")); | |
Predicate contextIdPredicate = subqueryRoot.get("contextId").in(builder.parameter(List.class, paramName)); | |
subquery.select(subqueryRoot).where(caseIdPredicate, contextIdPredicate); | |
return builder.exists(subquery); | |
}; | |
private CaseSpecs() { | |
} | |
static CaseLiteSpecsBuilder builder() { | |
return new CaseLiteSpecsBuilder(); | |
} | |
public static class CaseLiteSpecsBuilder extends AbstractSpecificationBuilder<CaseEntity> { | |
private CaseLiteSpecsBuilder() { | |
super(); | |
} | |
@NonNull | |
CaseSpecs.CaseLiteSpecsBuilder hasCurrentDepartment(@Nullable Long departmentId) { | |
apply(hasCurrentDepartment, Parameters.hasCurrentDepartment, departmentId); | |
return this; | |
} | |
@NonNull | |
CaseSpecs.CaseLiteSpecsBuilder hasCurrentBrand(@Nullable Integer brandId) { | |
apply(hasCurrentBrand, Parameters.hasCurrentBrand, isNull(brandId) ? null : brandId.shortValue()); | |
return this; | |
} | |
@NonNull | |
CaseSpecs.CaseLiteSpecsBuilder hasCurrentBox(@Nullable Long boxId) { | |
apply(hasCurrentBox, Parameters.hasCurrentBox, boxId); | |
return this; | |
} | |
@NonNull | |
CaseSpecs.CaseLiteSpecsBuilder hasCurrentUser(Long userId) { | |
apply(hasCurrentUser, Parameters.hasCurrentUser, userId); | |
return this; | |
} | |
CaseSpecs.CaseLiteSpecsBuilder hasNotCurrentUser(Long userId) { | |
apply(hasNotCurrentUser, isNullOrEmpty(userId)); | |
return this; | |
} | |
CaseSpecs.CaseLiteSpecsBuilder isSensitive(Boolean isSensitiveCase) { | |
apply(isSensitive, isSensitiveCase); | |
return this; | |
} | |
CaseSpecs.CaseLiteSpecsBuilder hasBusiness(List<Integer> business) { | |
apply(businessIn, Parameters.businessIn, business); | |
return this; | |
} | |
CaseSpecs.CaseLiteSpecsBuilder isOld(Boolean isOlder, Integer daysToBeOlder) { | |
LocalDateTime olderDate = LocalDateTime.now().minusDays(daysToBeOlder); | |
if (nonNull(isOlder) && isOlder) { | |
apply(isOldLess, Parameters.isOldLess, olderDate); | |
apply(isOldGreater, Parameters.isOldGreater, olderDate.minusMonths(3)); | |
} | |
return this; | |
} | |
@NonNull | |
CaseSpecs.CaseLiteSpecsBuilder statusIn(List<Integer> statusId) { | |
apply(statusIn, statusId); | |
return this; | |
} | |
@NonNull | |
CaseSpecs.CaseLiteSpecsBuilder contextIdIn(@Nullable List<Integer> contextTypeIds) { | |
apply(hasAnyContextTypeIdIn, Parameters.hasAnyContextTypeIdIn, contextTypeIds); | |
return this; | |
} | |
@NonNull | |
CaseSpecs.CaseLiteSpecsBuilder storeIdIn(List<Integer> storeId) { | |
apply(storesIn, Parameters.storesIn, storeId); | |
return this; | |
} | |
} | |
public static class Parameters { | |
public static final String hasCurrentUser = "hasCurrentUser"; | |
public static final String isOldLess = "isOldLess"; | |
public static final String isOldGreater = "isOldGreater"; | |
public static final String hasCurrentDepartment = "hasCurrentDepartment"; | |
public static final String hasCurrentBrand = "hasCurrentBrand"; | |
public static final String hasCurrentBox = "hasCurrentBox"; | |
public static final String businessIn = "businessIn"; | |
public static final String storesIn = "storesIn"; | |
public static final String hasAnyContextTypeIdIn = "hasAnyContextTypeIdIn"; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@RequiredArgsConstructor | |
@Slf4j | |
public class JpaBindableSpecificationClient { | |
private final EntityManager entityManager; | |
/** | |
* Query page metadata and results based on a jpa specification. It makes use of {@link BindableSpecification} to make possible the use of | |
* bind and literal parameters in the same query. | |
* | |
* @param entityType Query and specification entity type | |
* @param filter Specification builder with the restrictions that apply to the query | |
* @param namedEntityGraph Entity graph to be applied to the results query, if any. See {@link javax.persistence.NamedEntityGraph} | |
* @param pageable Query page data | |
* @param <EntityT> Query and specification entity type | |
* @return A page with the result found for the specification restrictions. | |
*/ | |
public <EntityT> Page<EntityT> findPage(Class<EntityT> entityType, BindableSpecification<EntityT> filter, | |
@Nullable String namedEntityGraph, Pageable pageable) { | |
CriteriaBuilder cb = entityManager.getCriteriaBuilder(); | |
Long totalCount = countQuery(entityType, filter, cb); | |
if (totalCount == 0) { | |
return PageableExecutionUtils.getPage(new ArrayList<>(), pageable, () -> totalCount); | |
} | |
TypedQuery<EntityT> bindedFindQuery = findQueryBinded(entityType, filter, pageable, cb); | |
addEntityGraph(entityType, namedEntityGraph, bindedFindQuery); | |
bindedFindQuery.setFirstResult((int) pageable.getOffset()); | |
bindedFindQuery.setMaxResults(pageable.getPageSize()); | |
List<EntityT> results = bindedFindQuery.getResultList(); | |
return PageableExecutionUtils.getPage(results, pageable, () -> totalCount); | |
} | |
/** | |
* Count query for page metadata. | |
*/ | |
private <EntityT> Long countQuery(Class<EntityT> entityType, BindableSpecification<EntityT> filter, CriteriaBuilder cb) { | |
// Create count criteria | |
CriteriaQuery<Long> countCriteria = cb.createQuery(Long.class); | |
Root<EntityT> entity = countCriteria.from(entityType); | |
countCriteria.select(cb.count(entity)); | |
// Spec to query restrictions | |
Predicate restriction = filter.toPredicate(entity, countCriteria, cb); | |
countCriteria.where(restriction); | |
// Create query and bind spec parameters | |
TypedQuery<Long> countQuery = entityManager.createQuery(countCriteria); | |
filter.bind(countQuery); | |
return countQuery.getSingleResult(); | |
} | |
/** | |
* Page results query. | |
*/ | |
private <EntityT> TypedQuery<EntityT> findQueryBinded(Class<EntityT> entityType, BindableSpecification<EntityT> filter, Pageable pageable, | |
CriteriaBuilder cb) { | |
// Create select criteria | |
CriteriaQuery<EntityT> cr = cb.createQuery(entityType); | |
Root<EntityT> root = cr.from(entityType); | |
cr.select(root); | |
// Spect to restrictions | |
Predicate predicate = filter.toPredicate(root, cr, cb); | |
cr.where(predicate); | |
// Add order | |
cr.orderBy(QueryUtils.toOrders(pageable.getSort(), root, cb)); | |
// Create query and bind parameters | |
TypedQuery<EntityT> query = entityManager.createQuery(cr); | |
filter.bind(query); | |
return query; | |
} | |
/** | |
* Adds entity graph to query if it exists. | |
*/ | |
private <EntityT> void addEntityGraph(Class<EntityT> entityType, String namedEntityGraph, TypedQuery<EntityT> query) { | |
if (StringUtils.isNotEmpty(namedEntityGraph)) { | |
try { | |
EntityGraph<EntityT> entityGraph = (EntityGraph<EntityT>) entityManager.getEntityGraph(namedEntityGraph); | |
query.setHint("javax.persistence.fetchgraph", entityGraph); | |
} catch (Exception e) { | |
log.error("Using wrong entity graph '{}' with entity class '{}'", namedEntityGraph, entityType.getName()); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment