简体   繁体   中英

spring boot JPA specification dynamic generate SQL and how can I improve it?

Now I'm used to the spring-boot version 2.4.3, and we primarily use the spring-boot JPA.

public class ReportSpecification implements Specification<Report> {
    private Long userId;
    private String name;

    @Override
    public Predicate toPredicate(Root<Report> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
        List<Predicate> predicates = Lists.newArrayList();

        if (ObjectUtils.isNotEmpty(userId))
            predicates.add(criteriaBuilder.equal(root.get(Report_.user).get(User_.id), userId));

        if (Strings.isNotBlank(name))
            predicates.add(criteriaBuilder.equal(root.get(Report_.name), name));

        return criteriaBuilder.and(Iterables.toArray(predicates, Predicate.class));
    }
}

This is just sample code, but it's actually more complicated, and the Specification class wants it to be generic, that will be more than 10 if sentences in this method because its purpose is one Table to one Specification class, it can generate SQL dynamically.

My question is, there will be so many if statements in this method, so I hope the code can be more readable, can someone give me some advice or how can I refactor this method using which design pattern, or is there any way to improve it?

Specification is a small, single‐purpose class, a predicate that determines if an object does or does not satisfy some criteria. It is flexible to implement one specification per one simple criterion. In cases where the rules are complex, simple specifications can be combined, just as predicates are combined with logical operators.
The main idea is to divide large specification into small reusable pieces and combine them via a composite pattern.
Specification for userId :

    public static Specification<Report> byUserId(Long userId) {
        return (root, query, criteriaBuilder) -> {
            Predicate predicate = null;
            if (ObjectUtils.isNotEmpty(userId)) {
                predicate = criteriaBuilder.equal(root.get("user").get("id"), userId);
            }
            return predicate;
        };
    }

Specification for reportName :

    public static Specification<Report> byReportName(String reportName) {
        return (root, query, criteriaBuilder) -> {
            Predicate predicate = null;
            if (Strings.isNotBlank(reportName)) {
                predicate = criteriaBuilder.equal(root.get("name"), reportName);
            }
            return predicate;
        };
    }

Combine specifications:

    public static Specification<Report> reportCompositeSpecification(ReportCriteria reportCriteria) {
        return combine(getSpecifications(reportCriteria));
    }
    
    private static <T> Specification<T> combine(Collection<Specification<T>> specifications) {
        Specification<T> combinedSpecification = null;
        for (Specification<T> specification : specifications) {
            if (combinedSpecification == null) {
                combinedSpecification = Specification.where(specification);
            } else {
                combinedSpecification = combinedSpecification.and(specification);
            }
        }
        return combinedSpecification;        
    }

    public static List<Specification<Report>> getSpecifications(ReportCriteria reportCriteria) {
        return Stream.of(byUserId(reportCriteria.getUserId()),
                         byReportName(reportCriteria.getName()))
                     .collect(Collectors.toList());
    }

Full implementation:

public class ReportSpecification implements Specification<Report> {

    private final ReportCriteria reportCriteria;

    public ReportSpecification(ReportCriteria reportCriteria) {
        this.reportCriteria = reportCriteria;
    }

    @Override
    public Predicate toPredicate(Root<Report> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
        return reportCompositeSpecification(reportCriteria).toPredicate(root, query, criteriaBuilder);
    }

    public static Specification<Report> reportCompositeSpecification(ReportCriteria reportCriteria) {
        return combine(getSpecifications(reportCriteria));
    }

    private static <T> Specification<T> combine(Collection<Specification<T>> specifications) {
        Specification<T> combinedSpecification = null;
        for (Specification<T> specification : specifications) {
            if (combinedSpecification == null) {
                combinedSpecification = Specification.where(specification);
            } else {
                combinedSpecification = combinedSpecification.and(specification);
            }
        }
        return combinedSpecification;
    }

    public static List<Specification<Report>> getSpecifications(ReportCriteria reportCriteria) {
        return Stream.of(byUserId(reportCriteria.getUserId()),
                         byReportName(reportCriteria.getName()))
                     .collect(Collectors.toList());
    }

    public static Specification<Report> byUserId(Long userId) {
        return (root, query, criteriaBuilder) -> {
            Predicate predicate = null;
            if (ObjectUtils.isNotEmpty(userId)) {
                predicate = criteriaBuilder.equal(root.get("user").get("id"), userId);
            }
            return predicate;
        };
    }

    public static Specification<Report> byReportName(String reportName) {
        return (root, query, criteriaBuilder) -> {
            Predicate predicate = null;
            if (Strings.isNotBlank(reportName)) {
                predicate = criteriaBuilder.equal(root.get("name"), reportName);
            }
            return predicate;
        };
    }
}

public class ReportCriteria {
    private Long userId;
    private String name;

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Example of usage:

import static com.test.repository.specification.ReportSpecification.*;

        ReportCriteria reportCriteria = new ReportCriteria();
        reportCriteria.setName("name");
        reportCriteria.setUserId(1L);
        reportRepository.findAll(reportCompositeSpecification(reportCriteria));

OR

        ReportCriteria reportCriteria = new ReportCriteria();
        reportCriteria.setName(null);
        reportCriteria.setUserId(1L);
        return reportRepository.findAll(new ReportSpecification(reportCriteria));

OR

import static com.test.repository.specification.ReportSpecification.*;

        ReportCriteria criteria = new ReportCriteria();
        criteria.setName("name");
        criteria.setUserId(1L);
        return reportRepository.findAll(byUserId(criteria.getUserId()).and(byReportName(criteria.getName())));

Advantages:

  • you have flexible simple specifications which can be combined or reused in other context
  • solution is extensible. You need just implement the new specification and register it in the common collection getSpecifications(ReportCriteria reportCriteria)


References
Specifications - Martin Fowler
Domain-Driven Design: Tackling Complexity in the Heart of Software


Also more simple implementation without common combine method:

public class ReportSpecification implements Specification<Report> {
    private final ReportCriteria reportCriteria;

    public ReportSpecification(ReportCriteria reportCriteria) {
        this.reportCriteria = reportCriteria;
    }

    @Override
    public Predicate toPredicate(Root<Report> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
        return byUserId(reportCriteria.getUserId()).and(byReportName(reportCriteria.getName()))
                .toPredicate(root, query, criteriaBuilder);
    }

    public static Specification<Report> byUserId(Long userId) {
        return (root, query, criteriaBuilder) -> {
            Predicate predicate = null;
            if (ObjectUtils.isNotEmpty(userId)) {
                predicate = criteriaBuilder.equal(root.get("user").get("id"), userId);
            }
            return predicate;
        };
    }

    public static Specification<Report> byReportName(String reportName) {
        return (root, query, criteriaBuilder) -> {
            Predicate predicate = null;
            if (Strings.isNotBlank(reportName)) {
                predicate = criteriaBuilder.equal(root.get("name"), reportName);
            }
            return predicate;
        };
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM