Spring Data Jpa Specifications

Spring Data Jpa provides multiple ways to create underlying database queries such as using method names, @Query annotation, named queries etc.
Spring Data JPA Criteria query helps to create where clause of the query for domain or entity class. Spring Data Jpa provides Criteria Api to build Query for underlying database programmatically.

Spring Data Jpa adopt the concept from "Domain Driven Design" from Eric Evans' book, using the same semantics, Spring Data creates JpaSpecificationExecutor interface.
JpaSpecificationExecutor interface provides APIs and methods to define Specifications and create Spring Data Jpa Criteria. Spring Data JPA Criteria. are build on top of the Criteria API to simplify the where clause.
Specifications interface help to build a set of query methods for Domain or entity class without actually declaring query.

The specification requires metamodel type which is generated using the JPA Metamodel generator

drop database if exists AppleDb;
create database AppleDb;
use AppleDb;
create TABLE apple ( apple_id BIGINT NOT NULL, apple_name VARCHAR(255) DEFAULT NULL, taste VARCHAR(255) DEFAULT NULL, price float NOT NULL, PRIMARY KEY (apple_id)) ENGINE=INNODB;
create TABLE hibernate_sequence ( next_val BIGINT) ENGINE=INNODB;
insert into hibernate_sequence values ( 1 );
insert into apple (apple_name, apple_id, taste, price) values ('Macintosh', 1, 'tangy',1.1);
insert into apple (apple_name, apple_id, taste, price) values ('Fuji', 2, 'sweet',2.2);
insert into apple (apple_name, apple_id, taste, price) values ('Gala', 3, 'tangy',3.3);
insert into apple (apple_name, apple_id, taste, price) values ('Jonagold', 4, 'sweet',4.4);
insert into apple (apple_name, apple_id, taste, price) values ('GrannySmith', 5, 'tangy',5.5);
package com.sujan.example.jpa.controller;

import com.sujan.example.jpa.entity.Apple;
import com.sujan.example.jpa.repository.AppleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;
import java.util.List;

import static com.sujan.example.jpa.controller.AppleService.*;

@RestController
public class AppleController {
    @Autowired
    private AppleRepository appleRepository;

    @GetMapping
    void projection() {
        List<Apple> getApplesByName = appleRepository.findAll(getApplesByName("Macintosh"));
        getApplesByName.forEach(System.out::println);

        List<Apple> getApplesByNameIn = appleRepository.findAll(getApplesByTasteIn(Arrays.asList("sweet", "tangy")));
        getApplesByNameIn.forEach(System.out::println);

        List<Apple> getPricesInBetween = appleRepository.findAll(getPricesInBetween(2.2f, 4.4f));
        getPricesInBetween.forEach(System.out::println);

        List<Apple> getApplesNameLike = appleRepository.findAll(getApplesNameLike("int"));
        getApplesNameLike.forEach(System.out::println);
    }
}
package com.sujan.example.jpa.controller;

import com.sujan.example.jpa.entity.Apple;
import com.sujan.example.jpa.repository.AppleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;

import java.util.List;

import static com.sujan.example.jpa.entity.Apple_.*;

@Service
public class AppleService {

    @Autowired
    private AppleRepository appleRepository;

    public static Specification<Apple> getApplesByName(String appleName) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(APPLE_NAME), appleName);
    }

    public static Specification<Apple> getApplesByTasteIn(List<String> taste) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.in(root.get(TASTE)).value(taste);
    }

    public static Specification<Apple> getPricesInBetween(float minPrice, float maxPrice) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.between(root.get(PRICE), minPrice, maxPrice);
    }

    public static Specification<Apple> getApplesNameLike(String appleName) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.like(root.get(APPLE_NAME), "%" + appleName + "%");
    }
}
package com.sujan.example.jpa.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Data
@Entity
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Apple {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long appleId;
    private String appleName;
    private String taste;
    private float price;

}

package com.sujan.example.jpa.repository;

import com.sujan.example.jpa.entity.Apple;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface AppleRepository extends CrudRepository<Apple, Long>, JpaSpecificationExecutor {
}
package com.sujan.example.jpa;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class JpaApplication {
    public static void main(String[] args) {
        SpringApplication.run(JpaApplication.class, args);
    }
}
spring.datasource.url=jdbc:mysql://localhost:3306/AppleDb?autoReconnect=true&useSSL=false&createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.show-sql=true
spring.datasource.initialization-mode=always
plugins {
    id 'org.springframework.boot' version '2.3.3.RELEASE'
    id 'io.spring.dependency-management' version '1.0.10.RELEASE'
    id 'java'
}
group = 'com.sujan'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}
repositories {
    mavenCentral()
}
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    annotationProcessor 'org.projectlombok:lombok'
    compileOnly 'org.projectlombok:lombok'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'mysql:mysql-connector-java'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}
test {
    useJUnitPlatform()
}
curl --location --request GET 'http://localhost:8080/'
gradle clean build
select apple0_.apple_id as apple_id1_0_, apple0_.apple_name as apple_na2_0_, apple0_.price as price3_0_, apple0_.taste as taste4_0_ from apple apple0_ where apple0_.apple_name=?
Apple(appleId=1, appleName=Macintosh, taste=tangy, price=1.1)
select apple0_.apple_id as apple_id1_0_, apple0_.apple_name as apple_na2_0_, apple0_.price as price3_0_, apple0_.taste as taste4_0_ from apple apple0_ where apple0_.taste in (? , ?)
Apple(appleId=1, appleName=Macintosh, taste=tangy, price=1.1)
Apple(appleId=2, appleName=Fuji, taste=sweet, price=2.2)
Apple(appleId=3, appleName=Gala, taste=tangy, price=3.3)
Apple(appleId=4, appleName=Jonagold, taste=sweet, price=4.4)
Apple(appleId=5, appleName=GrannySmith, taste=tangy, price=5.5)
select apple0_.apple_id as apple_id1_0_, apple0_.apple_name as apple_na2_0_, apple0_.price as price3_0_, apple0_.taste as taste4_0_ from apple apple0_ where apple0_.price between 2.2 and 4.4
Apple(appleId=2, appleName=Fuji, taste=sweet, price=2.2)
Apple(appleId=3, appleName=Gala, taste=tangy, price=3.3)
select apple0_.apple_id as apple_id1_0_, apple0_.apple_name as apple_na2_0_, apple0_.price as price3_0_, apple0_.taste as taste4_0_ from apple apple0_ where apple0_.apple_name like ?
Apple(appleId=1, appleName=Macintosh, taste=tangy, price=1.1)

Traditional Spring Data Jpa Query requires creating a separate method for each individual query. With each new requirement, the Query method increases. Individual query methods cannot be combined to create new Query methods.
Spring Data Jpa Specifications can be reused and combined even they have almost the same query to create new Specifications with desired where clause.

drop database if exists AppleDb;
create database AppleDb;
use AppleDb;
create TABLE apple ( apple_id BIGINT NOT NULL, apple_name VARCHAR(255) DEFAULT NULL, taste VARCHAR(255) DEFAULT NULL, price float NOT NULL, PRIMARY KEY (apple_id)) ENGINE=INNODB;
create TABLE hibernate_sequence ( next_val BIGINT) ENGINE=INNODB;
insert into hibernate_sequence values ( 1 );
insert into apple (apple_name, apple_id, taste, price) values ('Macintosh', 1, 'tangy',1.1);
insert into apple (apple_name, apple_id, taste, price) values ('Fuji', 2, 'sweet',2.2);
insert into apple (apple_name, apple_id, taste, price) values ('Gala', 3, 'tangy',3.3);
insert into apple (apple_name, apple_id, taste, price) values ('Jonagold', 4, 'sweet',4.4);
insert into apple (apple_name, apple_id, taste, price) values ('GrannySmith', 5, 'tangy',5.5);
package com.sujan.example.jpa.controller;

import com.sujan.example.jpa.entity.Apple;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;
import java.util.List;

@RestController
public class AppleController {
    @Autowired
    private AppleService appleService;

    @GetMapping
    void projection() {
        List<Apple> getAllSpecification = appleService.getAllSpecification("Fuji", Arrays.asList("sweet", "tangy"), 1.1f, 4.4f);
        getAllSpecification.forEach(System.out::println);
    }
}
package com.sujan.example.jpa.controller;

import com.sujan.example.jpa.entity.Apple;
import com.sujan.example.jpa.repository.AppleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;

import java.util.List;

import static com.sujan.example.jpa.entity.Apple_.*;
import static org.springframework.data.jpa.domain.Specification.where;

@Service
public class AppleService {

    @Autowired
    private AppleRepository appleRepository;

    private static Specification<Apple> getApplesByName(String appleName) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get(APPLE_NAME), appleName);
    }

    private static Specification<Apple> getApplesByTasteIn(List<String> taste) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.in(root.get(TASTE)).value(taste);
    }

    private static Specification<Apple> getPricesInBetween(float minPrice, float maxPrice) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.between(root.get(PRICE), minPrice, maxPrice);
    }

    private static Specification<Apple> getApplesNameLike(String appleName) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.like(root.get(APPLE_NAME), "%" + appleName + "%");
    }

    public List<Apple> getAllSpecification(String appleName, List<String> taste, float minPrice, float maxPrice) {
        return appleRepository.findAll(where(getApplesByName(appleName)).and(getApplesByTasteIn(taste)).and(getPricesInBetween(minPrice, maxPrice)).and(getApplesNameLike(appleName)));
    }
}
package com.sujan.example.jpa.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Data
@Entity
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Apple {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long appleId;
    private String appleName;
    private String taste;
    private float price;

}

package com.sujan.example.jpa.repository;

import com.sujan.example.jpa.entity.Apple;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface AppleRepository extends CrudRepository<Apple, Long>, JpaSpecificationExecutor {
}
package com.sujan.example.jpa;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class JpaApplication {
    public static void main(String[] args) {
        SpringApplication.run(JpaApplication.class, args);
    }
}
spring.datasource.url=jdbc:mysql://localhost:3306/AppleDb?autoReconnect=true&useSSL=false&createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.show-sql=true
spring.datasource.initialization-mode=always
plugins {
    id 'org.springframework.boot' version '2.3.3.RELEASE'
    id 'io.spring.dependency-management' version '1.0.10.RELEASE'
    id 'java'

}
group = 'com.sujan'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}
repositories {
    mavenCentral()
}
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    annotationProcessor 'org.projectlombok:lombok'
    compileOnly 'org.projectlombok:lombok'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    annotationProcessor 'org.hibernate:hibernate-jpamodelgen'
    runtimeOnly 'mysql:mysql-connector-java'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}
test {
    useJUnitPlatform()
}
curl --location --request GET 'http://localhost:8080/'
gradle clean build
select apple0_.apple_id as apple_id1_0_, apple0_.apple_name as apple_na2_0_, apple0_.price as price3_0_, apple0_.taste as taste4_0_ from apple apple0_ where (apple0_.apple_name like ?) and (apple0_.price between 1.1 and 4.4) and (apple0_.taste in (? , ?)) and apple0_.apple_name=?
Apple(appleId=2, appleName=Fuji, taste=sweet, price=2.2)

 

Dynamic Queries With Specifications

SPring Data Jpa Specifications helps us to create dynamic queries based on the requirement at run time. Spring Data Jpa Specifications allows a combination of the attributes or properties of a domain or entity class and creates a query.

drop database if exists AppleDb;
create database AppleDb;
use AppleDb;
create TABLE apple ( apple_id BIGINT NOT NULL, apple_name VARCHAR(255) DEFAULT NULL, taste VARCHAR(255) DEFAULT NULL, price float NOT NULL, PRIMARY KEY (apple_id)) ENGINE=INNODB;
create TABLE hibernate_sequence ( next_val BIGINT) ENGINE=INNODB;
insert into hibernate_sequence values ( 1 );
insert into apple (apple_name, apple_id, taste, price) values ('Macintosh', 1, 'tangy',1.1);
insert into apple (apple_name, apple_id, taste, price) values ('Fuji', 2, 'sweet',2.2);
insert into apple (apple_name, apple_id, taste, price) values ('Gala', 3, 'tangy',3.3);
insert into apple (apple_name, apple_id, taste, price) values ('Jonagold', 4, 'sweet',4.4);
insert into apple (apple_name, apple_id, taste, price) values ('GrannySmith', 5, 'tangy',5.5);
package com.sujan.example.jpa.controller;

import com.sujan.example.jpa.entity.Apple;
import com.sujan.example.jpa.repository.AppleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class AppleController {
    @Autowired
    private AppleRepository appleRepository;

    @PostMapping
    void specification(@RequestBody List<SearchCriteria> searchCriteria) {
        AppleSpecification appleSpecification = new AppleSpecification();
        searchCriteria.stream().map(searchCriterion -> new SearchCriteria(searchCriterion.getKey(), searchCriterion.getValue(), searchCriterion.getOperation())).forEach(appleSpecification::add);
        List<Apple> msGenreList = appleRepository.findAll(appleSpecification);
        msGenreList.forEach(System.out::println);
    }
}
package com.sujan.example.jpa.controller;

import com.sujan.example.jpa.entity.Apple;
import lombok.Data;
import org.springframework.data.jpa.domain.Specification;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.List;

@Data
public class AppleSpecification implements Specification<Apple> {
    private List<SearchCriteria> list = new ArrayList<>();

    public void add(SearchCriteria criteria) {
        list.add(criteria);
    }

    @Override
    public Predicate toPredicate(Root<Apple> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
        List<Predicate> predicates = new ArrayList<>();
        for (SearchCriteria criteria : list) {
            switch (criteria.getOperation()) {
                case GREATER_THAN:
                    predicates.add(builder.greaterThan(root.get(criteria.getKey()), criteria.getValue().toString()));
                    break;
                case LESS_THAN:
                    predicates.add(builder.lessThan(root.get(criteria.getKey()), criteria.getValue().toString()));
                    break;
                case GREATER_THAN_EQUAL:
                    predicates.add(builder.greaterThanOrEqualTo(root.get(criteria.getKey()), criteria.getValue().toString()));
                    break;
                case LESS_THAN_EQUAL:
                    predicates.add(builder.lessThanOrEqualTo(root.get(criteria.getKey()), criteria.getValue().toString()));
                    break;
                case NOT_EQUAL:
                    predicates.add(builder.notEqual(root.get(criteria.getKey()), criteria.getValue()));
                    break;
                case EQUAL:
                    predicates.add(builder.equal(root.get(criteria.getKey()), criteria.getValue()));
                    break;
                case LIKE:
                    predicates.add(builder.like(builder.lower(root.get(criteria.getKey())), "%" + criteria.getValue().toString().toLowerCase() + "%"));
                    break;
                case LIKE_END:
                    predicates.add(builder.like(builder.lower(root.get(criteria.getKey())), criteria.getValue().toString().toLowerCase() + "%"));
                    break;
                case LIKE_START:
                    predicates.add(builder.like(builder.lower(root.get(criteria.getKey())), "%" + criteria.getValue().toString().toLowerCase()));
                    break;
                case IN:
                    predicates.add(builder.in(root.get(criteria.getKey())).value(criteria.getValue()));
                    break;
                case NOT_IN:
                    predicates.add(builder.not(root.get(criteria.getKey())).in(criteria.getValue()));
                    break;
            }
        }
        return builder.and(predicates.toArray(new Predicate[0]));
    }
}
package com.sujan.example.jpa.controller;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class SearchCriteria {
    private String key;
    private Object value;
    private SearchOperation operation;
}
package com.sujan.example.jpa.controller;

public enum SearchOperation {
    GREATER_THAN,
    LESS_THAN,
    GREATER_THAN_EQUAL,
    LESS_THAN_EQUAL,
    NOT_EQUAL,
    EQUAL,
    LIKE,
    LIKE_START,
    LIKE_END,
    IN,
    NOT_IN
}
package com.sujan.example.jpa.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Data
@Entity
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Apple {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long appleId;
    private String appleName;
    private String taste;
    private float price;
}
package com.sujan.example.jpa.repository;

import com.sujan.example.jpa.entity.Apple;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface AppleRepository extends CrudRepository<Apple, Long>, JpaSpecificationExecutor {
}
package com.sujan.example.jpa;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class JpaApplication {
    public static void main(String[] args) {
        SpringApplication.run(JpaApplication.class, args);
    }
}
spring.datasource.url=jdbc:mysql://localhost:3306/AppleDb?autoReconnect=true&useSSL=false&createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.show-sql=true
spring.datasource.initialization-mode=always
plugins {
    id 'org.springframework.boot' version '2.3.3.RELEASE'
    id 'io.spring.dependency-management' version '1.0.10.RELEASE'
    id 'java'
}
group = 'com.sujan'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}
repositories {
    mavenCentral()
}
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    annotationProcessor 'org.projectlombok:lombok'
    compileOnly 'org.projectlombok:lombok'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'mysql:mysql-connector-java'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}
test {
    useJUnitPlatform()
}
curl --location --request POST 'http://localhost:8080/' --header 'Content-Type: application/json' --data-raw '[
    {
        "key": "appleName",
        "value": "Fuji",
        "operation": "EQUAL"
    },
    {
        "key": "taste",
        "value": "sweet",
        "operation": "IN"
    },
    {
        "key": "price",
        "value": "1.1f",
        "operation": "GREATER_THAN_EQUAL"
    },
    {
        "key": "price",
        "value": "4.4f",
        "operation": "LESS_THAN_EQUAL"
    }
]'
select apple0_.apple_id as apple_id1_0_, apple0_.apple_name as apple_na2_0_, apple0_.price as price3_0_, apple0_.taste as taste4_0_ from apple apple0_ where apple0_.apple_name=? and (apple0_.taste in (?)) and apple0_.price>=1.1 and apple0_.price<=4.4
Apple(appleId=2, appleName=Fuji, taste=sweet, price=2.2)

 

follow us on