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]));
}