Skip to content

a convenient Specification chain builder? #3804

@NaccOll

Description

@NaccOll

JpaSpecificationExecutor provides some methods that accept Specification as a parameter to perform query condition filtering. Specification uses CriteriaBuilder to build filter expression. Developer can use List.add to temporarily store dynamic Predicate conditions, and finally use CriteriaBuilder.and to construct the final Predicate.

	default Specification<SysDepartPo> buildSpecification1(SysDepartQueryCondition condition) {
		return (root, cq, cb) -> {
			var predicateList = new ArrayList<Predicate>();
			if (StringUtils.hasText(condition.getName())) {
				predicateList.add(cb.like(root.get("name"), "%" + condition.getName() + "%"));
			}
			if (condition.getOrganizationId() != null) {
				predicateList.add(cb.equal(root.get("organizationId"), condition.getOrganizationId()));
			}
			return cb.and(predicateList.toArray(new Predicate[0]));
		};
	}

In addition, I can combine Specification

	default Specification<SysDepartPo> buildSpecification2(SysDepartQueryCondition condition) {
		Specification<SysDepartPo> spec = (root, cq, cb) -> null;
		if (StringUtils.hasText(condition.getName())) {
			spec = spec.and((root, cq, cb) -> cb.like(root.get("name"), "%" + condition.getName() + "%"));
		}
		if (condition.getOrganizationId() != null) {
			spec = spec.and((root, cq, cb) -> cb.equal(root.get("organizationId"), condition.getOrganizationId()));
		}
		return spec;
	}

This got me thinking about whether I could make the whole search a little more convenient without having to deal with combinations. Here’s what I’m currently using:

	default Specification<SysDepartPo> buildSpecification3(SysDepartQueryCondition condition) {
		Specifications<SysDepartPo> spec = Specifications.builder(SysDepartPo.class);
		spec.likeAll(StringUtils.hasText(condition.getName()), SysDepartPo::getName, condition.getName());
		spec.eq(SysDepartPo::getOrganizationId, condition.getOrganizationId());
		return spec;
	}

By chaining, the constant combination and assignment of Specification is avoided.
The condition is embedded in the method to avoid if.

In addition, I also used SerializedLambda to obtain attributes. I know it is not a mainstream method in the jpa community. I should support SingularAttribute, but I don’t want to generate jpamodel. Although it has stronger reference capabilities, it is not more convenient than static references in handling basic types.

The following is my implementation of Specifications, which is not perfect. The supported parameter types are not complete, and the coverage of the Criteria API is not sufficient, but I hope that the community will support this kind of chain mode in the future.

package io.naccoll.boilerplate.core.persistence.jpa.specification;

import io.naccoll.boilerplate.core.interfaces.function.PropertyFunc;
import jakarta.persistence.criteria.*;
import org.springframework.data.jpa.domain.Specification;

import java.util.Collection;
import java.util.function.Consumer;

import static io.naccoll.boilerplate.core.utils.PojoHelper.getPropertyName;

/**
 * The type Specification builder.
 *
 * @param <T> the type parameter
 * @author NaccOll
 */
public class Specifications<T> implements Specification<T> {

	private Specification<T> specification;

	private boolean notCondition;

	private boolean orCondition;

	private Specifications() {
	}

	/**
	 * Builder specification builder.
	 * @param <T> the type parameter
	 * @return the specification builder
	 */
	public static <T> Specifications<T> builder() {
		return new Specifications<>();
	}

	public static <T> Specifications<T> builder(Class<T> tClass) {
		return new Specifications<>();
	}

	public <R> Specifications<T> like(PropertyFunc<T, R> attribute, String value) {
		return like(true, attribute, value);
	}

	public <R> Specifications<T> like(boolean condition, PropertyFunc<T, R> attribute, String value) {
		return maybeDo(condition, () -> (root, query, cb) -> cb.like(root.get(getPropertyName(attribute)), value));
	}

	public <R> Specifications<T> endWith(PropertyFunc<T, R> attribute, String value) {
		return endWith(true, attribute, value);
	}

	public <R> Specifications<T> endWith(boolean condition, PropertyFunc<T, R> attribute, String value) {
		return maybeDo(condition,
				() -> (root, query, cb) -> cb.like(root.get(getPropertyName(attribute)), "%" + value));
	}

	public <R> Specifications<T> startWith(PropertyFunc<T, R> attribute, String value) {
		return startWith(true, attribute, value);
	}

	public <R> Specifications<T> startWith(boolean condition, PropertyFunc<T, R> attribute, String value) {
		return maybeDo(condition,
				() -> (root, query, cb) -> cb.like(root.get(getPropertyName(attribute)), value + "%"));
	}

	public <R> Specifications<T> contain(PropertyFunc<T, R> attribute, String value) {
		return contain(true, attribute, value);
	}

	public <R> Specifications<T> contain(boolean condition, PropertyFunc<T, R> attribute, String value) {
		return maybeDo(condition,
				() -> (root, query, cb) -> cb.like(root.get(getPropertyName(attribute)), "%" + value + "%"));
	}

	public <R> Specifications<T> isNull(PropertyFunc<T, R> attribute) {
		return isNull(true, attribute);
	}

	public <R> Specifications<T> isNull(boolean condition, PropertyFunc<T, R> attribute) {
		return maybeDo(condition,
				() -> (root, query, criteriaBuilder) -> root.get(getPropertyName(attribute)).isNull());
	}

	public <R> Specifications<T> isNotNull(PropertyFunc<T, R> attribute) {
		return isNotNull(true, attribute);
	}

	public <R> Specifications<T> isNotNull(boolean condition, PropertyFunc<T, R> attribute) {
		return maybeDo(condition,
				() -> (root, query, criteriaBuilder) -> root.get(getPropertyName(attribute)).isNotNull());
	}

	public <R> Specifications<T> eq(PropertyFunc<T, R> attribute, R value) {
		return eq(true, attribute, value);
	}

	public <R> Specifications<T> eq(boolean condition, PropertyFunc<T, R> attribute, R value) {
		return maybeDo(condition, () -> (root, query, cb) -> cb.equal(root.get(getPropertyName(attribute)), value));
	}

	public <R> Specifications<T> ne(PropertyFunc<T, R> attribute, R value) {
		return ne(true, attribute, value);
	}

	public <R> Specifications<T> ne(boolean condition, PropertyFunc<T, R> attribute, R value) {
		return maybeDo(condition, () -> (root, query, cb) -> cb.notEqual(root.get(getPropertyName(attribute)), value));
	}

	public <R> Specifications<T> in(PropertyFunc<T, R> attribute, Collection<R> collection) {
		return in(true, attribute, collection);
	}

	public <R> Specifications<T> in(boolean condition, PropertyFunc<T, R> attribute, Collection<R> collection) {
		return maybeDo(condition, () -> (root, query, cb) -> root.get(getPropertyName(attribute)).in(collection));
	}

	public <R> Specifications<T> notIn(PropertyFunc<T, R> attribute, Collection<R> collection) {
		return notIn(true, attribute, collection);
	}

	public <R> Specifications<T> notIn(boolean condition, PropertyFunc<T, R> attribute, Collection<R> collection) {
		return maybeDo(condition, () -> (root, query, criteriaBuilder) -> criteriaBuilder
			.not(root.get(getPropertyName(attribute)).in(collection)));
	}

	public <R> Specifications<T> member(PropertyFunc<T, R> attribute, R collection) {
		return member(true, attribute, collection);
	}

	public <R> Specifications<T> member(boolean condition, PropertyFunc<T, R> attribute, R collection) {
		return maybeDo(condition, () -> (root, query, criteriaBuilder) -> criteriaBuilder.isMember(collection,
				root.get(getPropertyName(attribute))));
	}

	public <R> Specifications<T> notMember(PropertyFunc<T, R> attribute, R collection) {
		return notMember(true, attribute, collection);
	}

	public <R> Specifications<T> notMember(boolean condition, PropertyFunc<T, R> attribute, R collection) {
		return maybeDo(condition, () -> (root, query, criteriaBuilder) -> criteriaBuilder.isNotMember(collection,
				root.get(getPropertyName(attribute))));
	}

	public <R extends Comparable<R>> Specifications<T> gt(PropertyFunc<T, R> attribute, R value) {
		return gt(true, attribute, value);
	}

	public <R extends Comparable<R>> Specifications<T> gt(boolean condition, PropertyFunc<T, R> attribute, R value) {
		return maybeDo(condition,
				() -> (root, query, cb) -> cb.greaterThan(root.get(getPropertyName(attribute)), value));
	}

	public <R extends Comparable<R>> Specifications<T> ge(PropertyFunc<T, R> attribute, R value) {
		return ge(true, attribute, value);
	}

	public <R extends Comparable<R>> Specifications<T> ge(boolean condition, PropertyFunc<T, R> attribute, R value) {
		return maybeDo(condition,
				() -> (root, query, cb) -> cb.greaterThanOrEqualTo(root.get(getPropertyName(attribute)), value));
	}

	public <R extends Comparable<R>> Specifications<T> lt(PropertyFunc<T, R> attribute, R value) {
		return lt(true, attribute, value);
	}

	public <R extends Comparable<R>> Specifications<T> lt(boolean condition, PropertyFunc<T, R> attribute, R value) {
		return maybeDo(condition, () -> (root, query, cb) -> cb.lessThan(root.get(getPropertyName(attribute)), value));
	}

	public <R extends Comparable<R>> Specifications<T> le(PropertyFunc<T, R> attribute, R value) {
		return le(true, attribute, value);
	}

	public <R extends Comparable<R>> Specifications<T> le(boolean condition, PropertyFunc<T, R> attribute, R value) {
		return maybeDo(condition,
				() -> (root, query, cb) -> cb.lessThanOrEqualTo(root.get(getPropertyName(attribute)), value));
	}

	public <R extends Comparable<R>> Specifications<T> between(PropertyFunc<T, R> attribute, R min, R max) {
		return between(true, attribute, min, max);
	}

	public <R extends Comparable<R>> Specifications<T> between(boolean condition, PropertyFunc<T, R> attribute, R min,
			R max) {
		return maybeDo(condition,
				() -> (root, query, cb) -> cb.between(root.get(getPropertyName(attribute)), min, max));
	}

	public <R> Specifications<T> isTrue(PropertyFunc<T, R> attribute) {
		return isTrue(true, attribute);
	}

	public <R> Specifications<T> isTrue(boolean condition, PropertyFunc<T, R> attribute) {
		return maybeDo(condition, () -> (root, query, cb) -> cb.isTrue(root.get(getPropertyName(attribute))));
	}

	public <R> Specifications<T> isFalse(PropertyFunc<T, R> attribute) {
		return isFalse(true, attribute);
	}

	public <R> Specifications<T> isFalse(boolean condition, PropertyFunc<T, R> attribute) {
		return maybeDo(condition, () -> (root, query, cb) -> cb.isFalse(root.get(getPropertyName(attribute))));
	}

	@Override
	public Specifications<T> and(Specification<T> andSpecification) {
		return and(true, andSpecification);
	}

	public Specifications<T> and(boolean condition, Specification<T> andSpecification) {
		return maybeDo(condition, () -> {
			notCondition = false;
			orCondition = false;
			return andSpecification;
		});
	}

	public Specifications<T> and(Consumer<Specifications<T>> consumer) {
		return and(true, consumer);
	}

	public Specifications<T> and(boolean condition, Consumer<Specifications<T>> consumer) {
		Specifications<T> andBuilder = new Specifications<>();
		consumer.accept(andBuilder);
		maybeDo(condition, () -> {
			notCondition = false;
			orCondition = false;
			return andBuilder;
		});
		return this;
	}

	public Specifications<T> not() {
		notCondition = true;
		return this;
	}

	public Specifications<T> not(Specification<T> notSpecification) {
		return not(true, notSpecification);
	}

	public Specifications<T> not(boolean condition, Specification<T> notSpecification) {
		maybeDo(condition, () -> {
			notCondition = true;
			return notSpecification;
		});
		return this;
	}

	public Specifications<T> not(Consumer<Specifications<T>> consumer) {
		return or(true, consumer);
	}

	public Specifications<T> not(boolean condition, Consumer<Specifications<T>> consumer) {
		Specifications<T> notBuilder = new Specifications<>();
		consumer.accept(notBuilder);
		maybeDo(condition, () -> {
			notCondition = true;
			return notBuilder;
		});
		return this;
	}

	public Specifications<T> or() {
		orCondition = true;
		return this;
	}

	@Override
	public Specifications<T> or(Specification<T> orSpecification) {
		return or(true, orSpecification);
	}

	public Specifications<T> or(boolean condition, Specification<T> orSpecification) {
		maybeDo(condition, () -> {
			orCondition = true;
			return orSpecification;
		});
		return this;
	}

	public Specifications<T> or(Consumer<Specifications<T>> consumer) {
		return or(true, consumer);
	}

	public Specifications<T> or(boolean condition, Consumer<Specifications<T>> consumer) {
		Specifications<T> orBuilder = new Specifications<>();
		consumer.accept(orBuilder);
		maybeDo(condition, () -> {
			orCondition = true;
			return orBuilder;
		});
		return this;
	}

	protected Specifications<T> maybeDo(boolean condition, DoSomething<T> something) {
		if (condition) {
			Specification<T> newSpecification = something.doIt();
			if (notCondition) {
				newSpecification = Specification.not(newSpecification);
				notCondition = false;
			}
			if (specification != null) {
				if (orCondition) {
					specification = specification.or(newSpecification);
					orCondition = false;
				}
				else {
					specification = specification.and(newSpecification);
				}
			}
			else {
				specification = newSpecification;
			}
		}
		return this;
	}

	@Override
	public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
		if (specification == null) {
			specification = (r, cq, cb) -> null;
		}
		return specification.toPredicate(root, query, criteriaBuilder);
	}

	@FunctionalInterface
	public interface DoSomething<T> {

		/**
		 * 执行获取Specification
		 * @return
		 */
		Specification<T> doIt();

	}

}

Metadata

Metadata

Assignees

No one assigned

    Labels

    status: declinedA suggestion or change that we don't feel we should currently apply

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions