What are Specifications in Spring Data Jpa?

Specifications allows the creation of dynamic queries programmatically using the provided filters in a flexible way.

“JPA 2 introduces a criteria API that you can use to build queries programmatically. By writing a criteria, you define the where clause of a query for a domain class. Taking another step back, these criteria can be regarded as a predicate over the entity that is described by the JPA criteria API constraints.”

To be able to use Specification, it is necessary to make use of an interface called “JpaSpecificationExecutor“.

This way, we can perform the searches. For it to work, we need to use it in the Repository. I’ll use an example.

Prescription” is the Entity.

import jakarta.persistence.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@Entity
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Prescription {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @EqualsAndHashCode.Include
    private Long id;

    private String code;

    @Column(name = "creation_date")
    @CreationTimestamp
    private OffsetDateTime creationDate;

    private String addition;

    @Column(name = "due_date")
    @UpdateTimestamp
    private OffsetDateTime dueDate;

    @Column(name="status", nullable=false)
    @Enumerated(EnumType.STRING)
    private StatusPrescription status = StatusPrescription.CURRENT;

    @ManyToOne
    @JoinColumn(name = "doctor_id", nullable = false)
    private Doctor doctor;

    @OneToMany(mappedBy = "prescription", cascade = CascadeType.ALL)
    @Column(nullable = false)
    private List<Measure> measures = new ArrayList<>();

    @ManyToOne
    @JoinColumn(name = "user_id", nullable = false)
    private User client;

    @ManyToOne
    @JoinColumn(name = "order_id", nullable = false)
    private Order order;

    @PrePersist
    private void generateCode() {
        setCode(UUID.randomUUID().toString());
    }

}

I have a Repository that needs to make use of the “JpaSpecificationExecutor” interface.

import com.natancode.otica.domain.model.Prescription;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface PrescriptionRepository extends JpaRepository<Prescription, Long>,
        JpaSpecificationExecutor<Prescription> {

    Optional<Prescription> findByCode(String code);

}

This way, when using the “PrescriptionRepository“, we can use the methods provided by the “JpaSpecificationExecutor” interface.

We need to create a filter and Spec class.

import com.natancode.otica.domain.model.StatusPrescription;
import lombok.Getter;
import lombok.Setter;

import java.time.OffsetDateTime;

@Getter
@Setter
public class PrescriptionFilter {

    private Long clientId;
    private Long orderId;
    private Long doctorId;
    private StatusPrescription status;
    private OffsetDateTime creationDate;
    private OffsetDateTime dueDate;

}

import com.natancode.otica.domain.filter.PrescriptionFilter;
import com.natancode.otica.domain.model.Prescription;
import jakarta.persistence.criteria.Predicate;
import org.springframework.data.jpa.domain.Specification;

import java.util.ArrayList;

public class PrescriptionSpecs {

    public static Specification<Prescription> usingFilter(PrescriptionFilter filter) {
        return (root, query, criteriaBuilder) -> {
          if (Prescription.class.equals(query.getResultType())) {
              root.fetch("order").fetch("client");
              root.fetch("doctor");
          }

          var predicates = new ArrayList<Predicate>();

          if (filter.getClientId() != null) {
              predicates.add(criteriaBuilder.equal(root.get("client").get("id"), filter.getClientId()));
          }

          if (filter.getOrderId() != null) {
              predicates.add(criteriaBuilder.equal(root.get("order").get("id"), filter.getOrderId()));
          }

          if (filter.getDoctorId() != null) {
              predicates.add(criteriaBuilder.equal(root.get("doctor").get("id"), filter.getDoctorId()));
          }

          if (filter.getStatus() != null) {
              predicates.add(criteriaBuilder.equal(root.get("status"), filter.getStatus()));
          }

          if (filter.getDueDate() != null) {
              predicates.add(criteriaBuilder.equal(root.get("dueDate"), filter.getDueDate()));
          }

          if (filter.getCreationDate() != null) {
              predicates.add(criteriaBuilder.equal(root.get("creationDate"), filter.getCreationDate()));
          }

          return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
        };
    }

}

Inside the if block, we have “root.fetch“, so when performing the query, it’s not necessary to run separate queries. We make a single query with joins.

We use a list of predicates to add what we want to filter, according to what was provided in the “PrescriptionFilter“. Then, we add the predicates to the “criteriaBuilder“.

Now let’s take a look at the usage of the “PrescriptionSpecs” class.

In the search endpoint, we have the filter.

@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
    public Page<PrescriptionSummaryModel> search(PrescriptionFilter filter,
                                                 @PageableDefault(size = 10) Pageable pageable) {

        pageable  = translatePageable(pageable);
        Page<Prescription> prescriptionsPage = prescriptionRepository
                .findAll(PrescriptionSpecs.usingFilter(filter), pageable);

        List<PrescriptionSummaryModel> prescriptionsSummaryModel = prescriptionSummaryModelAssembler
                .toCollectionModel(prescriptionsPage.getContent());

        return new PageImpl<>(prescriptionsSummaryModel, pageable, prescriptionsPage.getTotalElements());
    }

In this example, we are using the method with paginated search, but as mentioned earlier, the “JpaSpecificationExecutor” interface has several methods; it doesn’t have to be paginated—use whichever you prefer. We provide the result of the “usingFilter” method from the “PrescriptionSpecs” class to the “findAll” method of the “JpaSpecificationExecutor” interface. This way, we get the filtered result according to the filters specified in the request of the endpoint dynamically.

I have some records in the database; let’s make a request without filtering.

Now let’s filter by doctor.

Let’s filter by status as well.

This way, we can perform advanced searches dynamically by specifying only what we want to filter, as seen in the previous example, which provides great flexibility for the API.

Author

  • Natan Ferreira

    Hello there, I’m Natan Lara Ferreira, Full Stack Developer Java and Angular since 2016. I’m in Open Finance Brazil project using framework Quarkus and Angular since the beginning 2021. I'm a problem solver, critical thinker and team player.