Skip to content

Commit 529a9ce

Browse files
$near with list of entities returned
1 parent a8153be commit 529a9ce

File tree

9 files changed

+199
-53
lines changed

9 files changed

+199
-53
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,33 +46,37 @@ public interface CriteriaDefinition {
4646
* @since 5.0
4747
* @author Christoph Strobl
4848
*/
49-
class Placeholder {
50-
51-
private final Object expression;
49+
interface Placeholder {
5250

5351
/**
5452
* Create a new placeholder for index bindable parameter.
5553
*
5654
* @param position the index of the parameter to bind.
5755
* @return new instance of {@link Placeholder}.
5856
*/
59-
public static Placeholder indexed(int position) {
60-
return new Placeholder("?%s".formatted(position));
57+
static Placeholder indexed(int position) {
58+
return new PlaceholderImpl("?%s".formatted(position));
6159
}
6260

63-
public static Placeholder placeholder(String expression) {
64-
return new Placeholder(expression);
61+
static Placeholder placeholder(String expression) {
62+
return new PlaceholderImpl(expression);
6563
}
6664

67-
Placeholder(Object value) {
68-
this.expression = value;
65+
Object getValue();
66+
}
67+
68+
static class PlaceholderImpl implements Placeholder {
69+
private final Object expression;
70+
71+
public PlaceholderImpl(Object expression) {
72+
this.expression = expression;
6973
}
7074

75+
@Override
7176
public Object getValue() {
7277
return expression;
7378
}
7479

75-
@Override
7680
public String toString() {
7781
return getValue().toString();
7882
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.data.mongodb.repository.aot;
1717

18+
import java.util.ArrayList;
1819
import java.util.Iterator;
1920
import java.util.List;
2021
import java.util.stream.Collectors;
@@ -42,11 +43,16 @@
4243
import org.springframework.data.mongodb.core.query.UpdateDefinition;
4344
import org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor;
4445
import org.springframework.data.mongodb.repository.query.MongoParameterAccessor;
46+
import org.springframework.data.mongodb.repository.query.MongoParameters;
4547
import org.springframework.data.mongodb.repository.query.MongoQueryCreator;
48+
import org.springframework.data.repository.query.Parameter;
49+
import org.springframework.data.repository.query.Parameters;
50+
import org.springframework.data.repository.query.QueryMethod;
4651
import org.springframework.data.repository.query.parser.PartTree;
4752
import org.springframework.data.util.TypeInformation;
4853

4954
import com.mongodb.DBRef;
55+
import org.springframework.util.ClassUtils;
5056

5157
/**
5258
* @author Christoph Strobl
@@ -68,10 +74,10 @@ public AotQueryCreator() {
6874
}
6975

7076
@SuppressWarnings("NullAway")
71-
StringQuery createQuery(PartTree partTree, int parameterCount) {
77+
StringQuery createQuery(PartTree partTree, QueryMethod queryMethod) {
7278

7379
Query query = new MongoQueryCreator(partTree,
74-
new PlaceholderConvertingParameterAccessor(new PlaceholderParameterAccessor(parameterCount)), mappingContext)
80+
new PlaceholderConvertingParameterAccessor(new PlaceholderParameterAccessor(queryMethod)), mappingContext)
7581
.createQuery();
7682

7783
if (partTree.isLimiting()) {
@@ -118,17 +124,25 @@ static class PlaceholderParameterAccessor implements MongoParameterAccessor {
118124

119125
private final List<Placeholder> placeholders;
120126

121-
public PlaceholderParameterAccessor(int parameterCount) {
122-
if (parameterCount == 0) {
127+
public PlaceholderParameterAccessor(QueryMethod queryMethod) {
128+
if (queryMethod.getParameters().getNumberOfParameters() == 0) {
123129
placeholders = List.of();
124130
} else {
125-
placeholders = IntStream.range(0, parameterCount).mapToObj(Placeholder::indexed).collect(Collectors.toList());
131+
placeholders = new ArrayList<>();
132+
Parameters<?, ?> parameters = queryMethod.getParameters();
133+
for(Parameter parameter : parameters.toList()) {
134+
if(ClassUtils.isAssignable(Point.class, parameter.getType())) {
135+
placeholders.add(parameter.getIndex(), new GeoPlaceholder(parameter.getIndex()));
136+
} else {
137+
placeholders.add(parameter.getIndex(), Placeholder.indexed(parameter.getIndex()));
138+
}
139+
}
126140
}
127141
}
128142

129143
@Override
130144
public Range<Distance> getDistanceRange() {
131-
return null;
145+
return Range.unbounded();
132146
}
133147

134148
@Override
@@ -207,4 +221,24 @@ public Iterator<Object> iterator() {
207221
return ((List) placeholders).iterator();
208222
}
209223
}
224+
225+
static class GeoPlaceholder extends Point implements Placeholder {
226+
227+
int index;
228+
229+
public GeoPlaceholder(int index) {
230+
super(Double.NaN, Double.NaN);
231+
this.index = index;
232+
}
233+
234+
@Override
235+
public Object getValue() {
236+
return "?" + index;
237+
}
238+
239+
@Override
240+
public String toString() {
241+
return getValue().toString();
242+
}
243+
}
210244
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,8 @@ private QueryInteraction createStringQuery(RepositoryInformation repositoryInfor
154154
} else {
155155

156156
PartTree partTree = new PartTree(queryMethod.getName(), repositoryInformation.getDomainType());
157-
query = new QueryInteraction(queryCreator.createQuery(partTree, parameterCount), partTree.isCountProjection(),
158-
partTree.isDelete(), partTree.isExistsProjection());
157+
query = new QueryInteraction(queryCreator.createQuery(partTree, queryMethod),
158+
partTree.isCountProjection(), partTree.isDelete(), partTree.isExistsProjection());
159159
}
160160

161161
if (queryAnnotation != null && StringUtils.hasText(queryAnnotation.sort())) {
@@ -171,8 +171,8 @@ private QueryInteraction createStringQuery(RepositoryInformation repositoryInfor
171171
private static boolean backoff(MongoQueryMethod method) {
172172

173173
// TODO: namedQuery, Regex queries, queries accepting Shapes (e.g. within) or returning arrays.
174-
boolean skip = method.isGeoNearQuery() || method.isSearchQuery()
175-
|| method.getName().toLowerCase(Locale.ROOT).contains("regex") || method.getReturnType().getType().isArray();
174+
boolean skip = method.isSearchQuery() || method.getName().toLowerCase(Locale.ROOT).contains("regex")
175+
|| method.getReturnType().getType().isArray();
176176

177177
if (skip && logger.isDebugEnabled()) {
178178
logger.debug("Skipping AOT generation for [%s]. Method is either returning an array or a geo-near, regex query"

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
import org.springframework.data.repository.query.parser.Part.Type;
5555
import org.springframework.data.repository.query.parser.PartTree;
5656
import org.springframework.data.util.Streamable;
57+
import org.springframework.lang.NonNull;
5758
import org.springframework.util.Assert;
5859
import org.springframework.util.ClassUtils;
5960
import org.springframework.util.ObjectUtils;
@@ -235,35 +236,7 @@ private Criteria from(Part part, MongoPersistentProperty property, Criteria crit
235236
return criteria.is(false);
236237
case NEAR:
237238

238-
Range<Distance> range = accessor.getDistanceRange();
239-
Optional<Distance> distance = range.getUpperBound().getValue();
240-
Optional<Distance> minDistance = range.getLowerBound().getValue();
241-
242-
Point point = accessor.getGeoNearLocation();
243-
Point pointToUse = point == null ? nextAs(parameters, Point.class) : point;
244-
245-
boolean isSpherical = isSpherical(property);
246-
247-
return distance.map(it -> {
248-
249-
if (isSpherical || !Metrics.NEUTRAL.equals(it.getMetric())) {
250-
criteria.nearSphere(pointToUse);
251-
} else {
252-
criteria.near(pointToUse);
253-
}
254-
255-
if (pointToUse instanceof GeoJson) { // using GeoJson distance is in meters.
256-
257-
criteria.maxDistance(MetricConversion.getDistanceInMeters(it));
258-
minDistance.map(MetricConversion::getDistanceInMeters).ifPresent(criteria::minDistance);
259-
} else {
260-
criteria.maxDistance(it.getNormalizedValue());
261-
minDistance.map(Distance::getNormalizedValue).ifPresent(criteria::minDistance);
262-
}
263-
264-
return criteria;
265-
266-
}).orElseGet(() -> isSpherical ? criteria.nearSphere(pointToUse) : criteria.near(pointToUse));
239+
return createNearCriteria(property, criteria, parameters);
267240

268241
case WITHIN:
269242

@@ -283,6 +256,40 @@ private Criteria from(Part part, MongoPersistentProperty property, Criteria crit
283256
}
284257
}
285258

259+
@NonNull
260+
private Criteria createNearCriteria(MongoPersistentProperty property, Criteria criteria, Iterator<Object> parameters) {
261+
262+
263+
Range<Distance> range = accessor.getDistanceRange();
264+
Optional<Distance> distance = range.getUpperBound().getValue();
265+
Optional<Distance> minDistance = range.getLowerBound().getValue();
266+
267+
Point point = accessor.getGeoNearLocation();
268+
Point pointToUse = point == null ? nextAs(parameters, Point.class) : point;
269+
270+
boolean isSpherical = isSpherical(property);
271+
272+
return distance.map(it -> {
273+
274+
if (isSpherical || !Metrics.NEUTRAL.equals(it.getMetric())) {
275+
criteria.nearSphere(pointToUse);
276+
} else {
277+
criteria.near(pointToUse);
278+
}
279+
280+
if (pointToUse instanceof GeoJson) { // using GeoJson distance is in meters.
281+
282+
criteria.maxDistance(MetricConversion.getDistanceInMeters(it));
283+
minDistance.map(MetricConversion::getDistanceInMeters).ifPresent(criteria::minDistance);
284+
} else {
285+
criteria.maxDistance(it.getNormalizedValue());
286+
minDistance.map(Distance::getNormalizedValue).ifPresent(criteria::minDistance);
287+
}
288+
289+
return criteria;
290+
}).orElseGet(() -> isSpherical ? criteria.nearSphere(pointToUse) : criteria.near(pointToUse));
291+
}
292+
286293
private boolean isSimpleComparisonPossible(Part part) {
287294

288295
return switch (part.shouldIgnoreCase()) {

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,11 @@
6060
import org.bson.codecs.DocumentCodec;
6161
import org.bson.codecs.EncoderContext;
6262
import org.bson.codecs.configuration.CodecConfigurationException;
63+
import org.bson.codecs.configuration.CodecProvider;
6364
import org.bson.codecs.configuration.CodecRegistries;
6465
import org.bson.codecs.configuration.CodecRegistry;
6566
import org.bson.conversions.Bson;
67+
import org.bson.internal.ProvidersCodecRegistry;
6668
import org.bson.json.JsonParseException;
6769
import org.bson.types.Binary;
6870
import org.bson.types.Decimal128;
@@ -74,6 +76,7 @@
7476
import org.springframework.data.mongodb.core.mapping.FieldName;
7577
import org.springframework.data.mongodb.core.mapping.FieldName.Type;
7678
import org.springframework.data.mongodb.core.query.CriteriaDefinition.Placeholder;
79+
import org.springframework.data.mongodb.core.query.CriteriaDefinition.PlaceholderImpl;
7780
import org.springframework.lang.Contract;
7881
import org.springframework.util.Assert;
7982
import org.springframework.util.ClassUtils;
@@ -103,7 +106,7 @@ public class BsonUtils {
103106
public static final Document EMPTY_DOCUMENT = new EmptyDocument();
104107

105108
private static final CodecRegistry JSON_CODEC_REGISTRY = CodecRegistries.fromRegistries(
106-
MongoClientSettings.getDefaultCodecRegistry(), CodecRegistries.fromCodecs(new PlaceholderCodec()));
109+
MongoClientSettings.getDefaultCodecRegistry(), CodecRegistries.fromProviders(new PlaceholderCodecProvider()));
107110

108111
@SuppressWarnings("unchecked")
109112
@Contract("null, _ -> null")
@@ -377,7 +380,7 @@ public static BsonValue simpleToBsonValue(@Nullable Object source) {
377380
@Contract("null, _ -> !null")
378381
public static BsonValue simpleToBsonValue(@Nullable Object source, CodecRegistry codecRegistry) {
379382

380-
if(source == null) {
383+
if (source == null) {
381384
return BsonNull.VALUE;
382385
}
383386

@@ -1031,6 +1034,19 @@ public void flush() {
10311034
}
10321035
}
10331036

1037+
@NullUnmarked
1038+
public static class PlaceholderCodecProvider implements CodecProvider {
1039+
1040+
PlaceholderCodec placeholderCodec = new PlaceholderCodec();
1041+
1042+
@Override
1043+
public <T> Codec<T> get(Class<T> clazz, CodecRegistry registry) {
1044+
if(!ClassUtils.isAssignable(Placeholder.class, clazz)) {
1045+
return null;
1046+
}
1047+
return (Codec<T>) placeholderCodec;
1048+
}
1049+
}
10341050
/**
10351051
* Internal {@link Codec} implementation to write
10361052
* {@link org.springframework.data.mongodb.core.query.CriteriaDefinition.Placeholder placeholders}.
@@ -1060,4 +1076,26 @@ public Class<Placeholder> getEncoderClass() {
10601076
return Placeholder.class;
10611077
}
10621078
}
1079+
1080+
// @NullUnmarked
1081+
// static class PlaceholderImplCodec implements Codec<PlaceholderImpl> {
1082+
//
1083+
// PlaceholderCodec delegate = new PlaceholderCodec();
1084+
//
1085+
// @Override
1086+
// public PlaceholderImpl decode(BsonReader reader, DecoderContext decoderContext) {
1087+
// return null;
1088+
// }
1089+
//
1090+
// @Override
1091+
// public void encode(BsonWriter writer, PlaceholderImpl value, EncoderContext encoderContext) {
1092+
// delegate.encode(writer, value, encoderContext);
1093+
//
1094+
// }
1095+
//
1096+
// @Override
1097+
// public Class<PlaceholderImpl> getEncoderClass() {
1098+
// return PlaceholderImpl.class;
1099+
// }
1100+
// }
10631101
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 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+
* https://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 example.aot;
17+
18+
import org.springframework.data.geo.Point;
19+
20+
/**
21+
* @param planet
22+
* @param coordinates
23+
* @author Christoph Strobl
24+
*/
25+
public record Location(String planet, Point coordinates) {
26+
}

spring-data-mongodb/src/test/java/example/aot/User.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ public class User {
3232

3333
@Field("last_name") String lastname;
3434

35+
Location location;
36+
3537
Instant registrationDate;
3638
Instant lastSeen;
3739
Long visits;
@@ -91,4 +93,12 @@ public Long getVisits() {
9193
public void setVisits(Long visits) {
9294
this.visits = visits;
9395
}
96+
97+
public Location getLocation() {
98+
return location;
99+
}
100+
101+
public void setLocation(Location location) {
102+
this.location = location;
103+
}
94104
}

spring-data-mongodb/src/test/java/example/aot/UserRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.data.domain.Slice;
3333
import org.springframework.data.domain.Sort;
3434
import org.springframework.data.domain.Window;
35+
import org.springframework.data.geo.Point;
3536
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
3637
import org.springframework.data.mongodb.repository.Aggregation;
3738
import org.springframework.data.mongodb.repository.Hint;
@@ -103,6 +104,8 @@ public interface UserRepository extends CrudRepository<User, String> {
103104

104105
Window<User> findTop2WindowByLastnameStartingWithOrderByUsername(String lastname, ScrollPosition scrollPosition);
105106

107+
List<User> findByLocationCoordinatesNear(Point location);
108+
106109
// TODO: GeoQueries
107110
// TODO: TextSearch
108111

0 commit comments

Comments
 (0)