Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Boot 3 JPA Specification not filtering with jakarta

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?

like image 510
Pavel Avatar asked Sep 20 '25 12:09

Pavel


1 Answers

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]));
        }
like image 113
Andrey B. Panfilov Avatar answered Sep 23 '25 02:09

Andrey B. Panfilov