From 1ebf05623257d97641c1956b4b3cbd10984a55b0 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 19 Dec 2025 09:05:12 +0100 Subject: [PATCH 1/3] Allow single-element contains for derived AOT queries. Closes #5123 Original pull request: #5124 --- .../repository/aot/AotPlaceholders.java | 33 +++++++++++++++++++ .../repository/aot/AotQueryCreator.java | 25 ++++++++++++-- .../repository/aot/AotStringQuery.java | 20 +++++++++-- .../mongodb/repository/aot/QueryBlocks.java | 4 +-- ...tractPersonRepositoryIntegrationTests.java | 22 +++++++++++++ .../mongodb/repository/PersonRepository.java | 3 ++ 6 files changed, 100 insertions(+), 7 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java index fca6ae5b7..0e5c6501b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java @@ -111,6 +111,26 @@ class AotPlaceholders { return new RegexPlaceholder(index, options); } + /** + * Create a placeholder that indicates the value should be treated as list. + * + * @param index zero-based index referring to the bindable method parameter. + * @return new instance of {@link Placeholder}. + */ + static Placeholder asList(int index) { + return asList(indexed(index)); + } + + /** + * Create a placeholder that indicates the wrapped placeholder should be treated as list. + * + * @param source the target placeholder + * @return new instance of {@link Placeholder}. + */ + static Placeholder asList(Placeholder source) { + return new AsListPlaceholder(source); + } + /** * A placeholder expression used when rending queries to JSON. * @@ -295,4 +315,17 @@ class AotPlaceholders { } } + record AsListPlaceholder(Placeholder placeholder) implements Placeholder { + + @Override + public String toString() { + return getValue(); + } + + @Override + public String getValue() { + return "[" + placeholder.getValue() + "]"; + } + } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java index b7d4439d7..d37f78c60 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java @@ -24,7 +24,6 @@ import java.util.regex.Pattern; import org.bson.conversions.Bson; import org.jspecify.annotations.NullUnmarked; import org.jspecify.annotations.Nullable; - import org.springframework.data.core.TypeInformation; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Range; @@ -48,6 +47,7 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.TextCriteria; import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.mongodb.repository.VectorSearch; +import org.springframework.data.mongodb.repository.aot.AotPlaceholders.AsListPlaceholder; import org.springframework.data.mongodb.repository.aot.AotPlaceholders.Placeholder; import org.springframework.data.mongodb.repository.aot.AotPlaceholders.RegexPlaceholder; import org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor; @@ -132,6 +132,10 @@ record AotQueryCreator(MappingContext mappingContext return criteria.raw("$regex", param); } + if (param instanceof AsListPlaceholder asListPlaceholder && !property.isCollectionLike()) { + return super.createContainingCriteria(part, property, criteria, asListPlaceholder.placeholder()); + } + return super.createContainingCriteria(part, property, criteria, param); } } @@ -226,7 +230,24 @@ record AotQueryCreator(MappingContext mappingContext partForIndex.shouldIgnoreCase().equals(IgnoreCaseType.ALWAYS) || partForIndex.shouldIgnoreCase().equals(IgnoreCaseType.WHEN_POSSIBLE) ? "i" : null)); - } else { + } else if (partForIndex != null && (partForIndex.getType().equals(Type.IN) + || partForIndex.getType().equals(Type.NOT_IN) || partForIndex.getType().equals(Type.CONTAINING) + || partForIndex.getType().equals(Type.NOT_CONTAINING))) { + + if (partForIndex.getProperty().isCollection() + && !TypeInformation.of(parameter.getType()).isCollectionLike()) { + if (partForIndex.shouldIgnoreCase().equals(IgnoreCaseType.ALWAYS)) { + placeholders.add(parameter.getIndex(), + AotPlaceholders.asList(AotPlaceholders.regex(parameter.getIndex(), "i"))); + } else { + placeholders.add(parameter.getIndex(), AotPlaceholders.asList(parameter.getIndex())); + } + } else { + placeholders.add(parameter.getIndex(), AotPlaceholders.indexed(parameter.getIndex())); + } + } + + else { placeholders.add(parameter.getIndex(), AotPlaceholders.indexed(parameter.getIndex())); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotStringQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotStringQuery.java index 0e8fae6f8..b5c87011e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotStringQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotStringQuery.java @@ -22,12 +22,12 @@ import java.util.Set; import org.bson.Document; import org.jspecify.annotations.Nullable; - import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Field; import org.springframework.data.mongodb.core.query.Meta; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.repository.aot.AotPlaceholders.AsListPlaceholder; import org.springframework.data.mongodb.repository.aot.AotPlaceholders.RegexPlaceholder; import org.springframework.util.StringUtils; @@ -83,7 +83,7 @@ class AotStringQuery extends Query { return false; } - return this.placeholders.get(index) instanceof RegexPlaceholder; + return obtainAndPotentiallyUnwrapPlaceholder(index) instanceof RegexPlaceholder; } @Nullable @@ -92,7 +92,21 @@ class AotStringQuery extends Query { return null; } - return this.placeholders.get(index) instanceof RegexPlaceholder rgp ? rgp.regexOptions() : null; + Object placeholderValue = obtainAndPotentiallyUnwrapPlaceholder(index); + return placeholderValue instanceof RegexPlaceholder rgp ? rgp.regexOptions() : null; + } + + @Nullable Object obtainAndPotentiallyUnwrapPlaceholder(int index) { + + if (this.placeholders.isEmpty()) { + return null; + } + + Object placeholerValue = this.placeholders.get(index); + if (placeholerValue instanceof AsListPlaceholder asListPlaceholder) { + placeholerValue = asListPlaceholder.placeholder(); + } + return placeholerValue; } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java index 84025466c..b27939c1f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryBlocks.java @@ -195,9 +195,9 @@ class QueryBlocks { String regexOptions = source.getQuery().getRegexOptions(i); if (StringUtils.hasText(regexOptions)) { - formatted.add(CodeBlock.of("toRegex($L)", parameterName)); - } else { formatted.add(CodeBlock.of("toRegex($L, $S)", parameterName, regexOptions)); + } else { + formatted.add(CodeBlock.of("toRegex($L)", parameterName)); } } else { formatted.add(CodeBlock.of("$L", parameterName)); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java index 5be81d930..b6cac676b 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java @@ -1341,6 +1341,20 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie assertThat(result).hasSize(1).contains(carter); } + @Test // GH-5123 + void findBySkillsContainsSingleElement() { + + List result = repository.findBySkillsContains("Drums"); + assertThat(result).hasSize(1).contains(carter); + } + + @Test // GH-5123 + void findBySkillsContainsSingleElementWithIgnoreCase() { + + List result = repository.findBySkillsContainsIgnoreCase("drums"); + assertThat(result).hasSize(1).contains(carter); + } + @Test // DATAMONGO-1425 void findBySkillsNotContains() { @@ -1349,6 +1363,14 @@ public abstract class AbstractPersonRepositoryIntegrationTests implements Dirtie assertThat(result).doesNotContain(carter); } + @Test // GH-5123 + void findBySkillsNotContainsSingleElement() { + + List result = repository.findBySkillsNotContains("Drums"); + assertThat(result).hasSize((int) (repository.count() - 1)); + assertThat(result).doesNotContain(carter); + } + @Test // DATAMONGO-1424 void findsPersonsByFirstnameNotLike() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java index cef1af1c3..f46c9153d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java @@ -117,8 +117,11 @@ public interface PersonRepository extends MongoRepository, Query List findByFirstnameLikeOrderByLastnameAsc(Pattern firstname, Sort sort); List findBySkillsContains(List skills); + List findBySkillsContains(String skill); + List findBySkillsContainsIgnoreCase(String skill); List findBySkillsNotContains(List skills); + List findBySkillsNotContains(String skill); @Query("{'age' : { '$lt' : ?0 } }") List findByAgeLessThan(int age, Sort sort); From 921d1b863dce73c97815bbd9e48ef9b114828898 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 15 Jan 2026 09:31:46 +0100 Subject: [PATCH 2/3] Polishing. Refactor duplicates, simplify placeholder unwrapping. See #5123 Original pull request: #5124 --- .../repository/aot/AotPlaceholders.java | 33 ++++- .../repository/aot/AotQueryCreator.java | 120 ++++++++++-------- .../repository/aot/AotStringQuery.java | 30 ++--- .../aot/DocumentSerializerUnitTests.java | 3 +- 4 files changed, 105 insertions(+), 81 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java index 0e5c6501b..906816517 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotPlaceholders.java @@ -53,7 +53,7 @@ class AotPlaceholders { * @param type * @return */ - static Shape geoJson(int index, String type) { + static Placeholder geoJson(int index, String type) { return new GeoJsonPlaceholder(index, type); } @@ -63,7 +63,7 @@ class AotPlaceholders { * @param index zero-based index referring to the bindable method parameter. * @return */ - static Point point(int index) { + static Placeholder point(int index) { return new PointPlaceholder(index); } @@ -73,7 +73,7 @@ class AotPlaceholders { * @param index zero-based index referring to the bindable method parameter. * @return */ - static Shape circle(int index) { + static Placeholder circle(int index) { return new CirclePlaceholder(index); } @@ -83,7 +83,7 @@ class AotPlaceholders { * @param index zero-based index referring to the bindable method parameter. * @return */ - static Shape box(int index) { + static Placeholder box(int index) { return new BoxPlaceholder(index); } @@ -93,7 +93,7 @@ class AotPlaceholders { * @param index zero-based index referring to the bindable method parameter. * @return */ - static Shape sphere(int index) { + static Placeholder sphere(int index) { return new SpherePlaceholder(index); } @@ -103,7 +103,7 @@ class AotPlaceholders { * @param index zero-based index referring to the bindable method parameter. * @return */ - static Shape polygon(int index) { + static Placeholder polygon(int index) { return new PolygonPlaceholder(index); } @@ -140,6 +140,17 @@ class AotPlaceholders { interface Placeholder { String getValue(); + + /** + * Unwrap the current {@link Placeholder} to the given target type if possible. + * + * @param targetType + * @return + * @param + */ + default @Nullable T unwrap(Class targetType) { + return targetType.isInstance(this) ? targetType.cast(this) : null; + } } /** @@ -317,6 +328,16 @@ class AotPlaceholders { record AsListPlaceholder(Placeholder placeholder) implements Placeholder { + @Override + public @Nullable T unwrap(Class targetType) { + + if (targetType.isInstance(placeholder)) { + return targetType.cast(placeholder); + } + + return Placeholder.super.unwrap(targetType); + } + @Override public String toString() { return getValue(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java index d37f78c60..d7b14a4ae 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java @@ -24,6 +24,7 @@ import java.util.regex.Pattern; import org.bson.conversions.Bson; import org.jspecify.annotations.NullUnmarked; import org.jspecify.annotations.Nullable; + import org.springframework.data.core.TypeInformation; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Range; @@ -61,7 +62,6 @@ import org.springframework.data.repository.query.parser.Part; import org.springframework.data.repository.query.parser.Part.IgnoreCaseType; import org.springframework.data.repository.query.parser.Part.Type; import org.springframework.data.repository.query.parser.PartTree; -import org.springframework.util.ClassUtils; import com.mongodb.DBRef; @@ -132,8 +132,8 @@ record AotQueryCreator(MappingContext mappingContext return criteria.raw("$regex", param); } - if (param instanceof AsListPlaceholder asListPlaceholder && !property.isCollectionLike()) { - return super.createContainingCriteria(part, property, criteria, asListPlaceholder.placeholder()); + if (param instanceof AsListPlaceholder asList && !property.isCollectionLike()) { + return super.createContainingCriteria(part, property, criteria, asList.placeholder()); } return super.createContainingCriteria(part, property, criteria, param); @@ -176,10 +176,11 @@ record AotQueryCreator(MappingContext mappingContext @NullUnmarked static class PlaceholderParameterAccessor implements MongoParameterAccessor { - private final List placeholders; + private final List placeholders; @Nullable Part getPartForIndex(PartTree partTree, Parameter parameter) { + if (!parameter.isBindable()) { return null; } @@ -197,62 +198,74 @@ record AotQueryCreator(MappingContext mappingContext public PlaceholderParameterAccessor(PartTree partTree, QueryMethod queryMethod) { - if (queryMethod.getParameters().getNumberOfParameters() == 0) { + Parameters parameters = queryMethod.getParameters(); + if (parameters.getNumberOfParameters() == 0) { placeholders = List.of(); } else { - placeholders = new ArrayList<>(); - Parameters parameters = queryMethod.getParameters(); - + placeholders = new ArrayList<>(parameters.getNumberOfParameters()); for (Parameter parameter : parameters.toList()) { - if (ClassUtils.isAssignable(GeoJson.class, parameter.getType())) { - placeholders.add(parameter.getIndex(), AotPlaceholders.geoJson(parameter.getIndex(), "")); - } else if (ClassUtils.isAssignable(Point.class, parameter.getType())) { - placeholders.add(parameter.getIndex(), AotPlaceholders.point(parameter.getIndex())); - } else if (ClassUtils.isAssignable(Circle.class, parameter.getType())) { - placeholders.add(parameter.getIndex(), AotPlaceholders.circle(parameter.getIndex())); - } else if (ClassUtils.isAssignable(Box.class, parameter.getType())) { - placeholders.add(parameter.getIndex(), AotPlaceholders.box(parameter.getIndex())); - } else if (ClassUtils.isAssignable(Sphere.class, parameter.getType())) { - placeholders.add(parameter.getIndex(), AotPlaceholders.sphere(parameter.getIndex())); - } else if (ClassUtils.isAssignable(Polygon.class, parameter.getType())) { - placeholders.add(parameter.getIndex(), AotPlaceholders.polygon(parameter.getIndex())); - } else if (ClassUtils.isAssignable(Pattern.class, parameter.getType())) { - placeholders.add(parameter.getIndex(), AotPlaceholders.regex(parameter.getIndex(), null)); - } else { - Part partForIndex = getPartForIndex(partTree, parameter); - if (partForIndex != null - && (partForIndex.getType().equals(Type.LIKE) || partForIndex.getType().equals(Type.NOT_LIKE))) { - placeholders - .add(parameter.getIndex(), - AotPlaceholders - .regex(parameter.getIndex(), - partForIndex.shouldIgnoreCase().equals(IgnoreCaseType.ALWAYS) - || partForIndex.shouldIgnoreCase().equals(IgnoreCaseType.WHEN_POSSIBLE) ? "i" - : null)); - } else if (partForIndex != null && (partForIndex.getType().equals(Type.IN) - || partForIndex.getType().equals(Type.NOT_IN) || partForIndex.getType().equals(Type.CONTAINING) - || partForIndex.getType().equals(Type.NOT_CONTAINING))) { - - if (partForIndex.getProperty().isCollection() - && !TypeInformation.of(parameter.getType()).isCollectionLike()) { - if (partForIndex.shouldIgnoreCase().equals(IgnoreCaseType.ALWAYS)) { - placeholders.add(parameter.getIndex(), - AotPlaceholders.asList(AotPlaceholders.regex(parameter.getIndex(), "i"))); - } else { - placeholders.add(parameter.getIndex(), AotPlaceholders.asList(parameter.getIndex())); - } - } else { - placeholders.add(parameter.getIndex(), AotPlaceholders.indexed(parameter.getIndex())); - } - } - else { - placeholders.add(parameter.getIndex(), AotPlaceholders.indexed(parameter.getIndex())); + int index = parameter.getIndex(); + placeholders.add(index, getPlaceholder(index, parameter, partTree)); + } + } + } + + private Placeholder getPlaceholder(int index, Parameter parameter, PartTree partTree) { + + Class type = parameter.getType(); + + if (GeoJson.class.isAssignableFrom(type)) { + return AotPlaceholders.geoJson(index, ""); + } else if (Point.class.isAssignableFrom(type)) { + return AotPlaceholders.point(index); + } else if (Circle.class.isAssignableFrom(type)) { + return AotPlaceholders.circle(index); + } else if (Box.class.isAssignableFrom(type)) { + return AotPlaceholders.box(index); + } else if (Sphere.class.isAssignableFrom(type)) { + return AotPlaceholders.sphere(index); + } else if (Polygon.class.isAssignableFrom(type)) { + return AotPlaceholders.polygon(index); + } else if (Pattern.class.isAssignableFrom(type)) { + return AotPlaceholders.regex(index, null); + } + + Part partForIndex = getPartForIndex(partTree, parameter); + if (partForIndex != null) { + + IgnoreCaseType ignoreCaseType = partForIndex.shouldIgnoreCase(); + if (isLike(partForIndex.getType())) { + + boolean ignoreCase = !ignoreCaseType.equals(IgnoreCaseType.NEVER); + return AotPlaceholders.regex(index, ignoreCase ? "i" : null); + } + + if (isContaining(partForIndex.getType())) { + + if (partForIndex.getProperty().isCollection() && !TypeInformation.of(type).isCollectionLike()) { + if (ignoreCaseType.equals(IgnoreCaseType.ALWAYS)) { + return AotPlaceholders.asList(AotPlaceholders.regex(index, "i")); + } else { + return AotPlaceholders.asList(index); } } + + return AotPlaceholders.indexed(index); } } + + return AotPlaceholders.indexed(index); + } + + private static boolean isContaining(Part.Type type) { + return type.equals(Type.IN) || type.equals(Type.NOT_IN) // + || type.equals(Type.CONTAINING) || type.equals(Type.NOT_CONTAINING); + } + + private static boolean isLike(Part.Type type) { + return type.equals(Type.LIKE) || type.equals(Type.NOT_LIKE); } @Override @@ -322,8 +335,7 @@ record AotQueryCreator(MappingContext mappingContext @Override public @Nullable Object getBindableValue(int index) { - return placeholders.get(index) instanceof Placeholder placeholder ? placeholder.getValue() - : placeholders.get(index); + return placeholders.get(index).getValue(); } @Override @@ -337,7 +349,7 @@ record AotQueryCreator(MappingContext mappingContext return ((List) placeholders).iterator(); } - public List getPlaceholders() { + public List getPlaceholders() { return placeholders; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotStringQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotStringQuery.java index b5c87011e..81b2bb35d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotStringQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotStringQuery.java @@ -22,12 +22,12 @@ import java.util.Set; import org.bson.Document; import org.jspecify.annotations.Nullable; + import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Field; import org.springframework.data.mongodb.core.query.Meta; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.data.mongodb.repository.aot.AotPlaceholders.AsListPlaceholder; import org.springframework.data.mongodb.repository.aot.AotPlaceholders.RegexPlaceholder; import org.springframework.util.StringUtils; @@ -47,9 +47,9 @@ class AotStringQuery extends Query { private @Nullable String sort; private @Nullable String fields; - private List placeholders = new ArrayList<>(); + private List placeholders = new ArrayList<>(); - public AotStringQuery(Query query, List placeholders) { + public AotStringQuery(Query query, List placeholders) { this.delegate = query; this.placeholders = placeholders; } @@ -79,34 +79,24 @@ class AotStringQuery extends Query { } boolean isRegexPlaceholderAt(int index) { - if (this.placeholders.isEmpty()) { - return false; - } - - return obtainAndPotentiallyUnwrapPlaceholder(index) instanceof RegexPlaceholder; + return getRegexPlaceholder(index) != null; } @Nullable String getRegexOptions(int index) { - if (this.placeholders.isEmpty()) { - return null; - } - Object placeholderValue = obtainAndPotentiallyUnwrapPlaceholder(index); - return placeholderValue instanceof RegexPlaceholder rgp ? rgp.regexOptions() : null; + RegexPlaceholder placeholder = getRegexPlaceholder(index); + return placeholder != null ? placeholder.regexOptions() : null; } - @Nullable Object obtainAndPotentiallyUnwrapPlaceholder(int index) { + @Nullable + RegexPlaceholder getRegexPlaceholder(int index) { - if (this.placeholders.isEmpty()) { + if (index >= this.placeholders.size()) { return null; } - Object placeholerValue = this.placeholders.get(index); - if (placeholerValue instanceof AsListPlaceholder asListPlaceholder) { - placeholerValue = asListPlaceholder.placeholder(); - } - return placeholerValue; + return this.placeholders.get(index).unwrap(RegexPlaceholder.class); } @Override diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/DocumentSerializerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/DocumentSerializerUnitTests.java index 449f1d671..7f6f17d60 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/DocumentSerializerUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/DocumentSerializerUnitTests.java @@ -22,6 +22,7 @@ import java.util.List; import org.bson.Document; import org.junit.jupiter.api.Test; +import org.springframework.data.geo.Shape; import org.springframework.data.mongodb.core.query.GeoCommand; /** @@ -35,7 +36,7 @@ class DocumentSerializerUnitTests { void writesGeoCommandToJson() { Document source = new Document(); - source.append("foo", new GeoCommand(AotPlaceholders.box(1))); + source.append("foo", new GeoCommand((Shape) AotPlaceholders.box(1))); String jsonString = DocumentSerializer.toJson(source); From 383a7aefbec5d55f8d454457b97d6019e05a86f3 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 8 Jan 2026 11:22:05 +0100 Subject: [PATCH 3/3] Update compatibility matrix. Closes #5136 Original pull request: #5138 --- .../antora/modules/ROOT/pages/preface.adoc | 83 +++++-------------- 1 file changed, 23 insertions(+), 60 deletions(-) diff --git a/src/main/antora/modules/ROOT/pages/preface.adoc b/src/main/antora/modules/ROOT/pages/preface.adoc index f1aa26025..df3ae4026 100644 --- a/src/main/antora/modules/ROOT/pages/preface.adoc +++ b/src/main/antora/modules/ROOT/pages/preface.adoc @@ -1,16 +1,20 @@ [[requirements]] = Requirements -The Spring Data MongoDB 4.x binaries require JDK level 17 and above and https://spring.io/docs[Spring Framework] {springVersion} and above. - -In terms of database and driver, you need at least version 4.x of https://www.mongodb.org/[MongoDB] and a compatible MongoDB Java Driver (5.6+). +The Spring Data MongoDB 5.x binaries require JDK level 17 and above and https://spring.io/docs[Spring Framework] {springVersion} and above. [[compatibility.matrix]] == Compatibility Matrix -The following compatibility matrix summarizes Spring Data versions to MongoDB driver/database versions. -Database versions show server generations that pass the Spring Data test suite. -You can use newer server versions unless your application uses functionality that is affected by xref:preface.adoc#compatibility.changes[changes in the MongoDB server]. +[TIP] +==== +Please visit https://spring.io/projects/spring-data-mongodb#support[OSS Support] for detailed Spring Data support timelines. + +For the MongoDB Server Support Policy please refer to the https://www.mongodb.com/legal/support-policy/lifecycles[MongoDB Software Lifecycle Schedule]. +==== + +The following compatibility matrix summarizes Spring Data versions and their required minimum MongoDB client version. +Database versions show server generations that pass the Spring Data test suite, older server versions might have difficulties dealing with new/changed commands. +You may use newer server versions unless your application uses functionality that is affected by xref:preface.adoc#compatibility.changes[changes in the MongoDB server]. See also the https://www.mongodb.com/docs/drivers/java/sync/current/compatibility/[official MongoDB driver compatibility matrix] for driver- and server version compatibility. ==== @@ -19,12 +23,22 @@ See also the https://www.mongodb.com/docs/drivers/java/sync/current/compatibilit |Spring Data Release Train |Spring Data MongoDB -|Driver Version -|Database Versions +|Minimum Driver Version +|Tested Database Versions + +|2026.0 +|5.1.x +|5.6.x +|6.x to 8.x + +|2025.1 +|5.0.x +|5.6.x +|6.x to 8.x |2025.0 |4.5.x -|5.6.x +|5.5.x |6.x to 8.x |2024.1 @@ -37,58 +51,7 @@ See also the https://www.mongodb.com/docs/drivers/java/sync/current/compatibilit |4.11.x & 5.x |4.4.x to 7.x -|2023.1 -|4.2.x -|4.9.x -|4.4.x to 7.x - -|2023.0 (*) -|4.1.x -|4.9.x -|4.4.x to 6.x - -|2022.0 (*) -|4.0.x -|4.7.x -|4.4.x to 6.x - -|2021.2 (*) -|3.4.x -|4.6.x -|4.4.x to 5.0.x - -|2021.1 (*) -|3.3.x -|4.4.x -|4.4.x to 5.0.x - -|2021.0 (*) -|3.2.x -|4.1.x -|4.4.x - -|2020.0 (*) -|3.1.x -|4.1.x -|4.4.x - -|Neumann (*) -|3.0.x -|4.0.x -|4.4.x - -|Moore (*) -|2.2.x -|3.11.x/Reactive Streams 1.12.x -|4.2.x - -|Lovelace (*) -|2.1.x -|3.8.x/Reactive Streams 1.9.x -|4.0.x - |=== -(*) https://spring.io/projects/spring-data-mongodb#support[End of OSS Support] ==== [[compatibility.changes]]