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..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); } @@ -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. * @@ -120,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; + } } /** @@ -295,4 +326,27 @@ 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(); + } + + @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..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 @@ -48,6 +48,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; @@ -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,6 +132,10 @@ record AotQueryCreator(MappingContext mappingContext return criteria.raw("$regex", param); } + if (param instanceof AsListPlaceholder asList && !property.isCollectionLike()) { + return super.createContainingCriteria(part, property, criteria, asList.placeholder()); + } + return super.createContainingCriteria(part, property, criteria, param); } } @@ -172,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; } @@ -193,45 +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)); + + 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 { - placeholders.add(parameter.getIndex(), AotPlaceholders.indexed(parameter.getIndex())); + 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 @@ -301,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 @@ -316,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 0e8fae6f8..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 @@ -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,20 +79,24 @@ class AotStringQuery extends Query { } boolean isRegexPlaceholderAt(int index) { - if (this.placeholders.isEmpty()) { - return false; - } - - return this.placeholders.get(index) instanceof RegexPlaceholder; + return getRegexPlaceholder(index) != null; } @Nullable String getRegexOptions(int index) { - if (this.placeholders.isEmpty()) { + + RegexPlaceholder placeholder = getRegexPlaceholder(index); + return placeholder != null ? placeholder.regexOptions() : null; + } + + @Nullable + RegexPlaceholder getRegexPlaceholder(int index) { + + if (index >= this.placeholders.size()) { return null; } - return this.placeholders.get(index) instanceof RegexPlaceholder rgp ? rgp.regexOptions() : null; + return this.placeholders.get(index).unwrap(RegexPlaceholder.class); } @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); 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); 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]]