Skip to content

Instantly share code, notes, and snippets.

@josergdev
Last active August 24, 2024 10:51
Show Gist options
  • Save josergdev/84da286f32c4d93a113b1e33bfe65da6 to your computer and use it in GitHub Desktop.
Save josergdev/84da286f32c4d93a113b1e33bfe65da6 to your computer and use it in GitHub Desktop.
This class maps http query parameters to the JPA specification with common convention behavior
package dev.joserg.jpa;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.JoinType;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import jakarta.persistence.metamodel.ListAttribute;
import jakarta.persistence.metamodel.SingularAttribute;
import org.springframework.data.jpa.domain.Specification;
public record SimpleHttpCriteria<E>(
Map<PathMaker<E, ?>, List<?>> filters,
Operator internalOperator,
Operator externalOperator) implements Specification<E> {
public SimpleHttpCriteria() {
this(new HashMap<>(), Operator.OR, Operator.AND);
}
@Override
public Predicate toPredicate(Root<E> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
return this.specification().toPredicate(root, query, criteriaBuilder);
}
public <J> SimpleHttpCriteria<E> filter(PathMaker<E, J> path, List<J> values) {
if (!values.isEmpty()) {
this.filters.put(path, values);
}
return this;
}
public SimpleHttpCriteria<E> internalOperator(Operator operator) {
return new SimpleHttpCriteria<>(this.filters, operator, this.externalOperator);
}
public SimpleHttpCriteria<E> externalOperator(Operator operator) {
return new SimpleHttpCriteria<>(this.filters, this.internalOperator, operator);
}
private Specification<E> specification() {
final var filtersSpecs = this.filters.entrySet().stream().map(this::byFilter).toList();
return this.externalOperator.reduce(filtersSpecs);
}
private Specification<E> byFilter(Entry<PathMaker<E, ?>, List<?>> filter) {
final var filterSpecs = filter.getValue().stream().map(value -> this.byObject(filter.getKey(), value)).toList();
return this.internalOperator.reduce(filterSpecs);
}
private Specification<E> byObject(PathMaker<E, ?> path, Object value) {
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(path.toPath(root), value);
}
@FunctionalInterface
public interface PathMaker<R, P> {
static <R, P> PathMaker<R, P> attr(final SingularAttribute<? super R, P> attribute) {
return root -> root.get(attribute);
}
static <R, J, P> PathMaker<R, P> join(final ListAttribute<? super R, J> list,
final SingularAttribute<? super J, P> attribute,
JoinType joinType) {
return root -> root.join(list, joinType).get(attribute);
}
static <R, J, P> PathMaker<R, P> joinLeft(final ListAttribute<? super R, J> list,
final SingularAttribute<? super J, P> attribute) {
return join(list, attribute, JoinType.LEFT);
}
Path<P> toPath(Root<R> root);
}
public enum Operator {
AND, OR;
public <E> Specification<E> reduce(Iterable<Specification<E>> specifications) {
return switch (this) {
case AND -> Specification.allOf(specifications);
case OR -> Specification.anyOf(specifications);
};
}
}
}
@josergdev
Copy link
Author

josergdev commented Aug 24, 2024

Usage:

@Entity
public class Note {
  @Id
  private Integer id;

  @OneToMany
  private List<NoteLine> lines;
}

@Entity
public class NoteLine {
  @Id
  private Integer id;
}
var noteIds = List.of(UUID.fromString("f9bf9cfc-7a27-4d80-a39c-0c0173d4383b"), UUID.fromString("e2e4f3f8-86e4-44b7-82a0-bf9c3d1e4b3a"));
var noteLineIds = List.of(UUID.fromString("f9bf9cfc-7a27-4d80-a39c-0c0173d4383c"), UUID.fromString("e2e4f3f8-86e4-44b7-82a0-bf9c3d1e4b3b"));

var spec = new SimpleHttpCriteria<Note>()
    .filter(attr(Note_.Id), noteIds)
    .filter(joinLeft(Note_.lines, NoteLine_.Id), noteLineIds)
    .internalOperator(Operator.OR)
    .externalOperator(Operator.AND);

var result = this.jpaRepository.findAll(spec)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment