Skip to content

Instantly share code, notes, and snippets.

@zalito12
Created November 30, 2021 11:23
Show Gist options
  • Save zalito12/86f4161ac8759464fa594cea17737246 to your computer and use it in GitHub Desktop.
Save zalito12/86f4161ac8759464fa594cea17737246 to your computer and use it in GitHub Desktop.
Bindable Spec Query
/**
* 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 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);
}
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());
}
}
public interface CaseRepository {
Mono<Page<MyCase>> find(CaseSearchParams searchParams, PageParams<CaseSort> pageParams);
}
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";
}
}
@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