Home > Back-end >  Spring Boot 3 JPA Specification not filtering with jakarta
Spring Boot 3 JPA Specification not filtering with jakarta

Time:12-29

I'm moving to SpringBoot 3.0.1. After the update and replacing all javax.persistence with jakarta.persistence all filters which based on org.springframework.data.jpa.domain.Specification doesn't work, no error just not filtering.

Example entity:

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

import java.util.List;
import java.util.Objects;

@Getter
@Setter
@Entity
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "roles")
public class Role {

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

    @Column(name = "system_name")
    private String systemName;

    @Column(name = "title")
    private String title;

    @ManyToMany(mappedBy = "roles", fetch = FetchType.LAZY)
    @ToString.Exclude
    private List<User> users;

    // equals hashcode
}

Repository and Specification implementation:

import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Expression;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import my.package.model.Role;
import my.package.repository.filter.RoleFilter;
import my.package.util.SqlUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;


import java.util.List;

@Repository
public interface RoleRepository extends JpaRepository<Role, Long>, JpaSpecificationExecutor<Role> {

    record RoleSpecification(@NotNull RoleFilter filter) implements Specification<Role> {

        @NotNull
        @Override
        public Predicate toPredicate(@NotNull Root<Role> root,
                                     @NotNull CriteriaQuery<?> query,
                                     @NotNull CriteriaBuilder builder) {

            Predicate predicate = builder.conjunction();
            List<Expression<Boolean>> exps = predicate.getExpressions();

            filter.getTitle().ifPresent(title ->
                    exps.add(builder.like(builder.lower(root.get("title")), SqlUtils.toLikeLower(title))));

            return predicate;
        }
    }
}

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RoleFilter {

    private String title;

    public Optional<String> getTitle() {
        return Objects.isNull(title) || title.isEmpty() ? Optional.empty() : Optional.of(title);
    }

}

And usage:

import static org.springframework.data.jpa.domain.Specification.where;

@Service
@RequiredArgsConstructor
public class RoleServiceImpl implements RoleService {

    private final RoleRepository roleRepository;

    @Override
    public List<Role> list() {
        RoleFilter filter = new RoleFilter();
        filter.setTitle("test");
        return roleRepository.findAll(where(new RoleSpecification(filter)));
    }
}

And hibernate generate this strange query:

select
    g1_0.id,
    g1_0.created_at,
    g1_0.is_hidden,
    g1_0.title 
from
    groups g1_0 
where
    1=1 
order by
    g1_0.id desc offset ? rows fetch first ? rows only

Nothing about title in WHERE statement.

build.gradle

plugins {
    id 'org.springframework.boot' version '3.0.1'
    id 'io.spring.dependency-management' version '1.0.14.RELEASE'
    id 'java'
}

ext {
    jupiterVersion = '5.9.1'
    lombokVersion = '1.18.24'
    openApiVersion = '1.6.14'
    mapstructVersion = '1.5.3.Final'
    lombokMapstructBindingVersion = "0.2.0"
}

group = 'my.package'
version = '1.0'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-batch'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation "org.springdoc:springdoc-openapi-ui:${openApiVersion}"
    implementation "org.mapstruct:mapstruct:${mapstructVersion}"
    implementation 'com.google.guava:guava:31.1-jre'
    implementation 'net.minidev:json-smart:2.4.8'
    implementation 'io.nats:jnats:2.16.5'
    implementation 'io.jsonwebtoken:jjwt:0.9.1'
    implementation 'javax.xml.bind:jaxb-api:2.3.1'
    implementation 'org.flywaydb:flyway-core:9.10.1'
    implementation 'org.jetbrains:annotations:23.1.0'
    compileOnly "org.projectlombok:lombok:${lombokVersion}"
    runtimeOnly 'com.h2database:h2:2.1.214'
    runtimeOnly 'org.postgresql:postgresql:42.5.1'
    annotationProcessor "org.projectlombok:lombok:${lombokVersion}"
    annotationProcessor "org.projectlombok:lombok-mapstruct-binding:${lombokMapstructBindingVersion}"
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
    testImplementation 'org.springframework.batch:spring-batch-test'
    testImplementation "org.junit.jupiter:junit-jupiter-api:${jupiterVersion}"
    testImplementation "org.junit.jupiter:junit-jupiter-engine:${jupiterVersion}"
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:${jupiterVersion}'
    testRuntimeOnly 'org.junit.vintage:junit-vintage-engine:5.8.1'
    testCompileOnly 'junit:junit:4.12'
}

tasks.named('test') {
    useJUnitPlatform()
}

No errors, no exceptions, no warnings, just ignore all conditions. Why does it's happening and how to fix it?

CodePudding user response:

I'm afraid that never supposed to work before - you are modifying list of boolean expressions forming the predicate, and according to javadoc such modification does not affect resulting query:

Return the top-level conjuncts or disjuncts of the predicate. Returns empty list if there are no top-level conjuncts or disjuncts of the predicate. Modifications to the list do not affect the query.

Previous implementation of CompoundPredicate didn't follow JPA contract, mentioned above:

    @Override
    public List<Expression<Boolean>> getExpressions() {
        return expressions;
    }

Now it does:

    @Override
    public List<Expression<Boolean>> getExpressions() {
        return new ArrayList<>( predicates );
    }

UPD. The correct implementation should look like:

        @NotNull
        @Override
        public Predicate toPredicate(@NotNull Root<Role> root,
                                     @NotNull CriteriaQuery<?> query,
                                     @NotNull CriteriaBuilder builder) {

            List<Predicate> exps = new ArrayList<>;

            filter.getTitle().ifPresent(title ->
                    exps.add(builder.like(builder.lower(root.get("title")), SqlUtils.toLikeLower(title))));

            return builder.and(exps.toArray(new Predicate[0]));
        }
  • Related