Skip to content

Commit 44069af

Browse files
tkaczmarzykTomasz Kaczmarzyk
andauthored
Workaround for Spring Boot 4/Hibernate 6 issue with Root.equals (#294)
* Workaround for Spring Boot 4/Hibernate 6 issue with Root.equals --------- Co-authored-by: Tomasz Kaczmarzyk <tomasz.kaczmarzyk@tratif.com>
1 parent c6f5abb commit 44069af

5 files changed

Lines changed: 71 additions & 9 deletions

File tree

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@
206206
<dependency>
207207
<groupId>org.springframework.boot</groupId>
208208
<artifactId>spring-boot-dependencies</artifactId>
209-
<version>4.0.0-M1</version>
209+
<version>4.0.0</version>
210210
<type>pom</type>
211211
<scope>import</scope>
212212
</dependency>

src/main/java/net/kaczmarzyk/spring/data/jpa/domain/Join.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package net.kaczmarzyk.spring.data.jpa.domain;
1717

1818
import jakarta.persistence.criteria.*;
19+
import net.kaczmarzyk.spring.data.jpa.utils.Alias;
1920
import net.kaczmarzyk.spring.data.jpa.utils.QueryContext;
2021
import org.springframework.data.jpa.domain.Specification;
2122

@@ -88,7 +89,7 @@ private void putValToQueryContext(String alias, Root<T> root, Function<Root<?>,
8889
// because most typical scenario tends to be a LEFT join with distinct = true
8990
// and in such scenario if there is no filtering on the joined part (e.g. no related http param was sent)
9091
// then we can optimize behaviour by not joining at all
91-
queryContext.putLazyVal(alias, lazyVal);
92+
queryContext.putLazyVal(Alias.of(alias, root), lazyVal);
9293
// but inner joins or non-distinct queries must have them evaluated eagerly
9394
// because they affect query result even when there is no filtering applied
9495
if (!distinctQuery || joinType == JoinType.INNER) {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2014-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package net.kaczmarzyk.spring.data.jpa.utils;
17+
18+
import jakarta.persistence.criteria.Root;
19+
20+
import java.util.Objects;
21+
22+
/**
23+
* This is a pair of alias (text from query) and query Root.
24+
*
25+
* Equals uses identity comparison for Root part. Why? Because in Hibernate 6 2 roots from 2 different queries
26+
* may yield true in equals. This caused problems with paginated queries: join was evaluated and cached but
27+
* then Hibernate threw exception on attempt to reuse cached join in count query. It used to work in previous versions...
28+
*
29+
* Identity comparison seems to fix that. I don't like this solution, but I don't see any other for now.
30+
*
31+
* @author Tomasz Kaczmarzyk
32+
*/
33+
public class Alias {
34+
35+
private String alias;
36+
private Root<?> queryRoot;
37+
38+
public Alias(String alias, Root<?> queryRoot) {
39+
this.alias = alias;
40+
this.queryRoot = queryRoot;
41+
}
42+
43+
@Override
44+
public boolean equals(Object o) {
45+
if (this == o) return true;
46+
if (o == null || getClass() != o.getClass()) return false;
47+
Alias alias1 = (Alias) o;
48+
return Objects.equals(alias, alias1.alias) && queryRoot == alias1.queryRoot; // identity comparison for Root!
49+
}
50+
51+
@Override
52+
public int hashCode() {
53+
return Objects.hash(alias, queryRoot);
54+
}
55+
56+
public static Alias of(String alias, Root<?> queryRoot) {
57+
return new Alias(alias, queryRoot);
58+
}
59+
}

src/main/java/net/kaczmarzyk/spring/data/jpa/utils/QueryContext.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import jakarta.persistence.criteria.Fetch;
1919
import jakarta.persistence.criteria.Join;
2020
import jakarta.persistence.criteria.Root;
21+
2122
import java.util.function.Function;
2223

2324
/**
@@ -32,7 +33,7 @@ public interface QueryContext {
3233

3334
Join<?, ?> getEvaluated(String key, Root<?> root);
3435

35-
void putLazyVal(String key, Function<Root<?>, Join<?, ?>> value);
36+
void putLazyVal(Alias key, Function<Root<?>, Join<?, ?>> value);
3637

3738
Fetch<?, ?> getEvaluatedJoinFetch(String key);
3839

src/main/java/net/kaczmarzyk/spring/data/jpa/web/DefaultQueryContext.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package net.kaczmarzyk.spring.data.jpa.web;
1717

18+
import net.kaczmarzyk.spring.data.jpa.utils.Alias;
1819
import net.kaczmarzyk.spring.data.jpa.utils.QueryContext;
1920
import org.apache.commons.lang3.tuple.Pair;
2021

@@ -31,10 +32,10 @@
3132
*/
3233
public class DefaultQueryContext implements QueryContext {
3334

34-
private Map<String, Function<Root<?>, Join<?, ?>>> contextMap;
35+
private Map<Alias, Function<Root<?>, Join<?, ?>>> contextMap;
3536
private Map<String, Fetch<?, ?>> evaluatedJoinFetch;
3637

37-
private Map<Pair<String, Root>, Join<?, ?>> rootCache;
38+
private Map<Alias, Join<?, ?>> rootCache;
3839

3940
public DefaultQueryContext() {
4041
this.contextMap = new HashMap<>();
@@ -44,18 +45,18 @@ public DefaultQueryContext() {
4445

4546
@Override
4647
public boolean existsJoin(String key, Root<?> root) {
47-
return contextMap.containsKey(key);
48+
return contextMap.containsKey(Alias.of(key, root));
4849
}
4950

5051
@Override
5152
public Join<?, ?> getEvaluated(String key, Root<?> root) {
52-
Function<Root<?>, Join<?, ?>> value = contextMap.get(key);
53+
Function<Root<?>, Join<?, ?>> value = contextMap.get(Alias.of(key, root));
5354

5455
if (value == null) {
5556
return null;
5657
}
5758

58-
Pair<String, Root> rootKey = Pair.of(key, root);
59+
Alias rootKey = Alias.of(key, root);
5960

6061
if (!rootCache.containsKey(rootKey)) {
6162
Join<?, ?> evaluated = value.apply(root);
@@ -65,7 +66,7 @@ public boolean existsJoin(String key, Root<?> root) {
6566
}
6667

6768
@Override
68-
public void putLazyVal(String key, Function<Root<?>, Join<?, ?>> value) {
69+
public void putLazyVal(Alias key, Function<Root<?>, Join<?, ?>> value) {
6970
contextMap.put(key, value);
7071
}
7172

0 commit comments

Comments
 (0)