Compare commits

..

21 Commits
4.0.4 ... 4.0.6

Author SHA1 Message Date
Christoph Strobl
b069279f9d Release version 4.0.6 (2022.0.6).
See #4367
2023-05-12 11:08:06 +02:00
Christoph Strobl
70c9a44382 Prepare 4.0.6 (2022.0.6).
See #4367
2023-05-12 11:07:46 +02:00
Mark Paluch
6a9163da08 Polishing.
Introduce has…() and getRequired…() methods for comment and max time limit to remove code duplications.

See #4374
Original pull request: #4378
2023-05-11 10:15:14 +02:00
Christoph Strobl
a21fca78dd Fix missing query options when calling MongoOperations#count.
This commit makes sure to forward maxTimeMsec and comment options from the query to the CountOptions.

Closes: #4374
Original pull request: #4378
2023-05-11 10:14:59 +02:00
Christoph Strobl
a35c4f2717 Fix regression in value to String mapping.
Previous versions allow arbitrary values to be mapped to an string property by calling the ObjectToString converter. This behaviour got lost and is not reestablished.

Closes #4371
Original pull request #4373
2023-05-10 14:53:19 +02:00
Greg L. Turnquist
8803e0383d After release cleanups.
See #4336
2023-04-14 10:25:04 -05:00
Greg L. Turnquist
0b4c8fd780 Prepare next development iteration.
See #4336
2023-04-14 10:24:58 -05:00
Greg L. Turnquist
53c56f6e20 Release version 4.0.5 (2022.0.5).
See #4336
2023-04-14 10:19:14 -05:00
Greg L. Turnquist
6af9b562f6 Prepare 4.0.5 (2022.0.5).
See #4336
2023-04-14 10:18:32 -05:00
Mark Paluch
a5f73850af Polishing.
Reformat code. Remove unused fields, modifiers and documentation artifacts.

See #4088
Original pull request: #4341
2023-04-14 08:55:32 +02:00
Christoph Strobl
68b4a09273 Skip output for void methods using declarative Aggregations having $out stage.
We now set the skipOutput flag if an annotated Aggregation defines an $out stage and when the method is declared to return no result (void / Mono<Void>, kotlin.Unit)

Closes: #4088
Original pull request: #4341
2023-04-14 08:55:17 +02:00
Christoph Strobl
7290d78053 Fix null value handling in ParameterBindingJsonReader#readStringFromExtendedJson.
This commit makes sure to return null for a null parameter value avoiding a potential NPE when parsing data.
In doing so we can ensure object creation is done with the intended value that may or may not lead to a downstream error eg. when trying to create an ObjectId with a null hexString.

Closes: #4282
Original pull request: #4334
2023-04-13 11:31:28 +02:00
Mark Paluch
4acbb1282f Polishing.
Tweak naming.

Original pull request: #4352
See #4351
2023-04-13 11:12:17 +02:00
Christoph Strobl
b89c4cd231 Fix AOT processing for lazy-loading Jdk proxies.
This commit makes sure to use the ProxyFactory for retrieving the proxied interfaces. This makes sure to capture the exact interface order required when finally loading the proxy at runtime.

Original pull request: #4352
Closes #4351
2023-04-13 11:12:16 +02:00
Greg L. Turnquist
8d650ca1d2 Test against Java 20 on CI.
See #4350.
2023-04-12 14:59:27 -05:00
Mark Paluch
b9a23baf16 Adopt to Mockito 5.1 changes.
See #4290
2023-04-11 08:34:37 +02:00
Mark Paluch
d1326a45fc Upgrade to Maven Wrapper 3.9.1.
See #4357
2023-04-06 16:17:30 +02:00
Greg L. Turnquist
ca24950014 Update CI properties.
See #4336
2023-03-28 14:01:00 -05:00
Christoph Strobl
f157560d1c Update visibility of ConversionContext.
The ConversionContext should not be package private due to its usage in protected method signatures.

Closes: #4345
2023-03-24 13:41:01 +01:00
Christoph Strobl
4aa12fd24b After release cleanups.
See #4314
2023-03-20 14:26:21 +01:00
Christoph Strobl
c4a99370d8 Prepare next development iteration.
See #4314
2023-03-20 14:26:19 +01:00
35 changed files with 411 additions and 267 deletions

View File

@@ -1,2 +1,2 @@
#Mon Feb 20 11:59:26 CET 2023
distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.0/apache-maven-3.9.0-bin.zip
#Thu Apr 06 16:17:30 CEST 2023
distributionUrl=https\://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.1/apache-maven-3.9.1-bin.zip

90
Jenkinsfile vendored
View File

@@ -18,69 +18,7 @@ pipeline {
}
stages {
stage("Docker images") {
parallel {
stage('Publish JDK (Java 17) + MongoDB 4.4') {
when {
anyOf {
changeset "ci/openjdk17-mongodb-4.4/**"
changeset "ci/pipeline.properties"
}
}
agent { label 'data' }
options { timeout(time: 30, unit: 'MINUTES') }
steps {
script {
def image = docker.build("springci/spring-data-with-mongodb-4.4:${p['java.main.tag']}", "--build-arg BASE=${p['docker.java.main.image']} --build-arg MONGODB=${p['docker.mongodb.4.4.version']} ci/openjdk17-mongodb-4.4/")
docker.withRegistry(p['docker.registry'], p['docker.credentials']) {
image.push()
}
}
}
}
stage('Publish JDK (Java 17) + MongoDB 5.0') {
when {
anyOf {
changeset "ci/openjdk17-mongodb-5.0/**"
changeset "ci/pipeline.properties"
}
}
agent { label 'data' }
options { timeout(time: 30, unit: 'MINUTES') }
steps {
script {
def image = docker.build("springci/spring-data-with-mongodb-5.0:${p['java.main.tag']}", "--build-arg BASE=${p['docker.java.main.image']} --build-arg MONGODB=${p['docker.mongodb.5.0.version']} ci/openjdk17-mongodb-5.0/")
docker.withRegistry(p['docker.registry'], p['docker.credentials']) {
image.push()
}
}
}
}
stage('Publish JDK (Java 17) + MongoDB 6.0') {
when {
anyOf {
changeset "ci/openjdk17-mongodb-6.0/**"
changeset "ci/pipeline.properties"
}
}
agent { label 'data' }
options { timeout(time: 30, unit: 'MINUTES') }
steps {
script {
def image = docker.build("springci/spring-data-with-mongodb-6.0:${p['java.main.tag']}", "--build-arg BASE=${p['docker.java.main.image']} --build-arg MONGODB=${p['docker.mongodb.6.0.version']} ci/openjdk17-mongodb-6.0/")
docker.withRegistry(p['docker.registry'], p['docker.credentials']) {
image.push()
}
}
}
}
}
}
stage("test: baseline (Java 17)") {
stage("test: baseline (main)") {
when {
beforeAgent(true)
anyOf {
@@ -119,7 +57,7 @@ pipeline {
}
parallel {
stage("test: MongoDB 5.0 (Java 17)") {
stage("test: MongoDB 5.0 (main)") {
agent {
label 'data'
}
@@ -141,7 +79,7 @@ pipeline {
}
}
stage("test: MongoDB 6.0 (Java 17)") {
stage("test: MongoDB 6.0 (main)") {
agent {
label 'data'
}
@@ -162,6 +100,28 @@ pipeline {
}
}
}
stage("test: MongoDB 6.0 (next)") {
agent {
label 'data'
}
options { timeout(time: 30, unit: 'MINUTES') }
environment {
ARTIFACTORY = credentials("${p['artifactory.credentials']}")
}
steps {
script {
docker.image("harbor-repo.vmware.com/dockerhub-proxy-cache/springci/spring-data-with-mongodb-6.0:${p['java.next.tag']}").inside(p['docker.java.inside.basic']) {
sh 'mkdir -p /tmp/mongodb/db /tmp/mongodb/log'
sh 'mongod --setParameter transactionLifetimeLimitSeconds=90 --setParameter maxTransactionLockRequestTimeoutMillis=10000 --dbpath /tmp/mongodb/db --replSet rs0 --fork --logpath /tmp/mongodb/log/mongod.log &'
sh 'sleep 10'
sh 'mongosh --eval "rs.initiate({_id: \'rs0\', members:[{_id: 0, host: \'127.0.0.1:27017\'}]});"'
sh 'sleep 15'
sh 'MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw -s settings.xml clean dependency:list test -Duser.name=jenkins -Dsort -U -B'
}
}
}
}
}
}

View File

@@ -1,22 +0,0 @@
ARG BASE
FROM ${BASE}
# Any ARG statements before FROM are cleared.
ARG MONGODB
ENV TZ=Etc/UTC
ENV DEBIAN_FRONTEND=noninteractive
RUN set -eux; \
sed -i -e 's/archive.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list && \
sed -i -e 's/security.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list && \
sed -i -e 's/ports.ubuntu.com/mirrors.ocf.berkeley.edu/g' /etc/apt/sources.list && \
sed -i -e 's/http/https/g' /etc/apt/sources.list && \
apt-get update && apt-get install -y apt-transport-https apt-utils gnupg2 && \
apt-key adv --keyserver hkps://keyserver.ubuntu.com:443 --recv 656408E390CFB1F5 && \
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/4.4 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-4.4.list && \
echo ${TZ} > /etc/timezone
RUN apt-get update && \
apt-get install -y mongodb-org=${MONGODB} mongodb-org-server=${MONGODB} mongodb-org-shell=${MONGODB} mongodb-org-mongos=${MONGODB} mongodb-org-tools=${MONGODB} && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

View File

@@ -1,24 +0,0 @@
ARG BASE
FROM ${BASE}
# Any ARG statements before FROM are cleared.
ARG MONGODB
ENV TZ=Etc/UTC
ENV DEBIAN_FRONTEND=noninteractive
RUN set -eux; \
sed -i -e 's/archive.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list && \
sed -i -e 's/security.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list && \
sed -i -e 's/ports.ubuntu.com/mirrors.ocf.berkeley.edu/g' /etc/apt/sources.list && \
sed -i -e 's/http/https/g' /etc/apt/sources.list && \
apt-get update && apt-get install -y apt-transport-https apt-utils gnupg2 wget && \
# MongoDB 5.0 release signing key
apt-key adv --keyserver hkps://keyserver.ubuntu.com:443 --recv B00A0BD1E2C63C11 && \
# Needed when MongoDB creates a 5.0 folder.
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/5.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-5.0.list && \
echo ${TZ} > /etc/timezone
RUN apt-get update && \
apt-get install -y mongodb-org=${MONGODB} mongodb-org-server=${MONGODB} mongodb-org-shell=${MONGODB} mongodb-org-mongos=${MONGODB} mongodb-org-tools=${MONGODB} && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

View File

@@ -1,24 +0,0 @@
ARG BASE
FROM ${BASE}
# Any ARG statements before FROM are cleared.
ARG MONGODB
ENV TZ=Etc/UTC
ENV DEBIAN_FRONTEND=noninteractive
RUN set -eux; \
sed -i -e 's/archive.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list && \
sed -i -e 's/security.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list && \
sed -i -e 's/ports.ubuntu.com/mirrors.ocf.berkeley.edu/g' /etc/apt/sources.list && \
sed -i -e 's/http/https/g' /etc/apt/sources.list && \
apt-get update && apt-get install -y apt-transport-https apt-utils gnupg2 wget && \
# MongoDB 6.0 release signing key
wget -qO - https://www.mongodb.org/static/pgp/server-6.0.asc | apt-key add - && \
# Needed when MongoDB creates a 6.0 folder.
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/6.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-6.0.list && \
echo ${TZ} > /etc/timezone
RUN apt-get update && \
apt-get install -y mongodb-org=${MONGODB} mongodb-org-server=${MONGODB} mongodb-org-shell=${MONGODB} mongodb-org-mongos=${MONGODB} mongodb-org-tools=${MONGODB} && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

View File

@@ -1,8 +1,10 @@
# Java versions
java.main.tag=17.0.6_10-jdk-focal
java.next.tag=20-jdk-jammy
# Docker container images - standard
docker.java.main.image=harbor-repo.vmware.com/dockerhub-proxy-cache/library/eclipse-temurin:${java.main.tag}
docker.java.next.image=harbor-repo.vmware.com/dockerhub-proxy-cache/library/eclipse-temurin:${java.next.tag}
# Supported versions of MongoDB
docker.mongodb.4.4.version=4.4.18

View File

@@ -5,7 +5,7 @@
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.0.4</version>
<version>4.0.6</version>
<packaging>pom</packaging>
<name>Spring Data MongoDB</name>
@@ -15,7 +15,7 @@
<parent>
<groupId>org.springframework.data.build</groupId>
<artifactId>spring-data-parent</artifactId>
<version>3.0.4</version>
<version>3.0.6</version>
</parent>
<modules>
@@ -26,7 +26,7 @@
<properties>
<project.type>multi</project.type>
<dist.id>spring-data-mongodb</dist.id>
<springdata.commons>3.0.4</springdata.commons>
<springdata.commons>3.0.6</springdata.commons>
<mongo>4.8.2</mongo>
<mongo.reactivestreams>${mongo}</mongo.reactivestreams>
<jmh.version>1.19</jmh.version>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.0.4</version>
<version>4.0.6</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -15,7 +15,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.0.4</version>
<version>4.0.6</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -13,7 +13,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>4.0.4</version>
<version>4.0.6</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@@ -18,6 +18,7 @@ package org.springframework.data.mongodb.aot;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
@@ -25,7 +26,6 @@ import java.util.Set;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.TypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.data.annotation.Reference;
@@ -33,7 +33,6 @@ import org.springframework.data.mongodb.core.convert.LazyLoadingProxyFactory;
import org.springframework.data.mongodb.core.convert.LazyLoadingProxyFactory.LazyLoadingInterceptor;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.DocumentReference;
import org.springframework.data.util.TypeUtils;
/**
* @author Christoph Strobl
@@ -66,9 +65,7 @@ public class LazyLoadingProxyAotProcessor {
if (field.getType().isInterface()) {
List<Class<?>> interfaces = new ArrayList<>(
TypeUtils.resolveTypesInSignature(ResolvableType.forField(field, type)));
interfaces.add(0, org.springframework.data.mongodb.core.convert.LazyLoadingProxy.class);
Arrays.asList(LazyLoadingProxyFactory.prepareFactory(field.getType()).getProxiedInterfaces()));
interfaces.add(org.springframework.aop.SpringProxy.class);
interfaces.add(org.springframework.aop.framework.Advised.class);
interfaces.add(org.springframework.core.DecoratingProxy.class);
@@ -77,7 +74,7 @@ public class LazyLoadingProxyAotProcessor {
} else {
Class<?> proxyClass = LazyLoadingProxyFactory.resolveProxyType(field.getType(),
() -> LazyLoadingInterceptor.none());
LazyLoadingInterceptor::none);
// see: spring-projects/spring-framework/issues/29309
generationContext.getRuntimeHints().reflection().registerType(proxyClass,

View File

@@ -1818,7 +1818,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
if (query.getLimit() > 0 && mapReduceOptions != null && mapReduceOptions.getLimit() == null) {
mapReduce = mapReduce.limit(query.getLimit());
}
if (query.getMeta().getMaxTimeMsec() != null) {
if (query.getMeta().hasMaxTime()) {
mapReduce = mapReduce.maxTime(query.getMeta().getMaxTimeMsec(), TimeUnit.MILLISECONDS);
}
@@ -3159,12 +3159,12 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
if (meta.hasValues()) {
if (StringUtils.hasText(meta.getComment())) {
cursorToUse = cursorToUse.comment(meta.getComment());
if (meta.hasComment()) {
cursorToUse = cursorToUse.comment(meta.getRequiredComment());
}
if (meta.getMaxTimeMsec() != null) {
cursorToUse = cursorToUse.maxTime(meta.getMaxTimeMsec(), TimeUnit.MILLISECONDS);
if (meta.hasMaxTime()) {
cursorToUse = cursorToUse.maxTime(meta.getRequiredMaxTimeMsec(), TimeUnit.MILLISECONDS);
}
if (meta.getCursorBatchSize() != null) {

View File

@@ -21,6 +21,7 @@ import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -52,6 +53,7 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.mapping.ShardKey;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.Meta;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.UpdateDefinition;
import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter;
@@ -388,7 +390,7 @@ class QueryOperations {
for (Entry<String, Object> entry : fields.entrySet()) {
if (entry.getValue() instanceof MongoExpression) {
if (entry.getValue()instanceof MongoExpression) {
AggregationOperationContext ctx = entity == null ? Aggregation.DEFAULT_CONTEXT
: new RelaxedTypeBasedAggregationOperationContext(entity.getType(), mappingContext, queryMapper);
@@ -564,9 +566,23 @@ class QueryOperations {
if (query.getLimit() > 0) {
options.limit(query.getLimit());
}
if (query.getSkip() > 0) {
options.skip((int) query.getSkip());
}
Meta meta = query.getMeta();
if (meta.hasValues()) {
if (meta.hasMaxTime()) {
options.maxTime(meta.getRequiredMaxTimeMsec(), TimeUnit.MILLISECONDS);
}
if (meta.hasComment()) {
options.comment(meta.getComment());
}
}
if (StringUtils.hasText(query.getHint())) {
String hint = query.getHint();

View File

@@ -1971,8 +1971,9 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
publisher.sort(mappedSort);
}
if (filterQuery.getMeta().getMaxTimeMsec() != null) {
publisher.maxTime(filterQuery.getMeta().getMaxTimeMsec(), TimeUnit.MILLISECONDS);
Meta meta = filterQuery.getMeta();
if (meta.hasMaxTime()) {
publisher.maxTime(meta.getRequiredMaxTimeMsec(), TimeUnit.MILLISECONDS);
}
if (filterQuery.getLimit() > 0 || (options.getLimit() != null)) {
@@ -3062,12 +3063,12 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
if (meta.hasValues()) {
if (StringUtils.hasText(meta.getComment())) {
findPublisherToUse = findPublisherToUse.comment(meta.getComment());
if (meta.hasComment()) {
findPublisherToUse = findPublisherToUse.comment(meta.getRequiredComment());
}
if (meta.getMaxTimeMsec() != null) {
findPublisherToUse = findPublisherToUse.maxTime(meta.getMaxTimeMsec(), TimeUnit.MILLISECONDS);
if (meta.hasMaxTime()) {
findPublisherToUse = findPublisherToUse.maxTime(meta.getRequiredMaxTimeMsec(), TimeUnit.MILLISECONDS);
}
if (meta.getCursorBatchSize() != null) {

View File

@@ -72,7 +72,7 @@ public final class LazyLoadingProxyFactory {
/**
* Predict the proxy target type. This will advice the infrastructure to resolve as many pieces as possible in a
* potential AOT scenario without necessarily resolving the entire object.
*
*
* @param propertyType the type to proxy
* @param interceptor the interceptor to be added.
* @return the proxy type.
@@ -90,16 +90,30 @@ public final class LazyLoadingProxyFactory {
.getProxyClass(LazyLoadingProxy.class.getClassLoader());
}
private ProxyFactory prepareProxyFactory(Class<?> propertyType, Supplier<LazyLoadingInterceptor> interceptor) {
/**
* Create the {@link ProxyFactory} for the given type, already adding required additional interfaces.
*
* @param targetType the type to proxy.
* @return the prepared {@link ProxyFactory}.
* @since 4.0.5
*/
public static ProxyFactory prepareFactory(Class<?> targetType) {
ProxyFactory proxyFactory = new ProxyFactory();
for (Class<?> type : propertyType.getInterfaces()) {
for (Class<?> type : targetType.getInterfaces()) {
proxyFactory.addInterface(type);
}
proxyFactory.addInterface(LazyLoadingProxy.class);
proxyFactory.addInterface(propertyType);
proxyFactory.addInterface(targetType);
return proxyFactory;
}
private ProxyFactory prepareProxyFactory(Class<?> propertyType, Supplier<LazyLoadingInterceptor> interceptor) {
ProxyFactory proxyFactory = prepareFactory(propertyType);
proxyFactory.addAdvice(interceptor.get());
return proxyFactory;

View File

@@ -2147,7 +2147,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
*
* @since 3.4.3
*/
interface ConversionContext {
protected interface ConversionContext {
/**
* Converts a source object into {@link TypeInformation target}.
@@ -2315,8 +2315,10 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
if (source instanceof Collection) {
Class<?> rawType = typeHint.getType();
if (!Object.class.equals(rawType)) {
if (!Object.class.equals(rawType) && !String.class.equals(rawType)) {
if (!rawType.isArray() && !ClassUtils.isAssignable(Iterable.class, rawType)) {
throw new MappingException(
String.format(INCOMPATIBLE_TYPES, source, source.getClass(), rawType, getPath()));
}
@@ -2345,11 +2347,6 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return (S) dbRefConverter.convert(context, (DBRef) source, typeHint);
}
if (source instanceof Collection) {
throw new MappingException(
String.format(INCOMPATIBLE_TYPES, source, BasicDBList.class, typeHint.getType(), getPath()));
}
if (BsonUtils.supportsBson(source)) {
return (S) documentConverter.convert(context, BsonUtils.asBson(source), typeHint);
}

View File

@@ -69,6 +69,19 @@ public class Meta {
this.allowDiskUse = source.allowDiskUse;
}
/**
* Return whether the maximum time limit for processing operations is set.
*
* @return {@code true} if set; {@code false} otherwise.
* @since 4.0.6
*/
public boolean hasMaxTime() {
Long maxTimeMsec = getMaxTimeMsec();
return maxTimeMsec != null && maxTimeMsec > 0;
}
/**
* @return {@literal null} if not set.
*/
@@ -77,6 +90,26 @@ public class Meta {
return getValue(MetaKey.MAX_TIME_MS.key);
}
/**
* Returns the required maximum time limit in milliseconds or throws {@link IllegalStateException} if the maximum time
* limit is not set.
*
* @return the maximum time limit in milliseconds for processing operations.
* @throws IllegalStateException if the maximum time limit is not set
* @see #hasMaxTime()
* @since 4.0.6
*/
public Long getRequiredMaxTimeMsec() {
Long maxTimeMsec = getMaxTimeMsec();
if (maxTimeMsec == null) {
throw new IllegalStateException("Maximum time limit in milliseconds not set");
}
return maxTimeMsec;
}
/**
* Set the maximum time limit in milliseconds for processing operations.
*
@@ -99,12 +132,13 @@ public class Meta {
}
/**
* Add a comment to the query that is propagated to the profile log.
* Return whether the comment is set.
*
* @param comment
* @return {@code true} if set; {@code false} otherwise.
* @since 4.0.6
*/
public void setComment(String comment) {
setValue(MetaKey.COMMENT.key, comment);
public boolean hasComment() {
return StringUtils.hasText(getComment());
}
/**
@@ -115,6 +149,34 @@ public class Meta {
return getValue(MetaKey.COMMENT.key);
}
/**
* Returns the required comment or throws {@link IllegalStateException} if the comment is not set.
*
* @return the comment.
* @throws IllegalStateException if the comment is not set
* @see #hasComment()
* @since 4.0.6
*/
public String getRequiredComment() {
String comment = getComment();
if (comment == null) {
throw new IllegalStateException("Comment not set");
}
return comment;
}
/**
* Add a comment to the query that is propagated to the profile log.
*
* @param comment
*/
public void setComment(String comment) {
setValue(MetaKey.COMMENT.key, comment);
}
/**
* @return {@literal null} if not set.
* @since 2.1

View File

@@ -127,6 +127,7 @@ public abstract class AbstractMongoQuery implements RepositoryQuery {
* @param accessor for providing invocation arguments. Never {@literal null}.
* @param typeToRead the desired component target type. Can be {@literal null}.
*/
@Nullable
protected Object doExecute(MongoQueryMethod method, ResultProcessor processor, ConvertingParameterAccessor accessor,
@Nullable Class<?> typeToRead) {

View File

@@ -16,7 +16,6 @@
package org.springframework.data.mongodb.repository.query;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.function.IntUnaryOperator;
import java.util.function.LongUnaryOperator;
@@ -25,8 +24,8 @@ import org.bson.Document;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
import org.springframework.data.mongodb.core.aggregation.AggregationPipeline;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.query.Meta;
@@ -36,7 +35,6 @@ import org.springframework.expression.ExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Internal utility class to help avoid duplicate code required in both the reactive and the sync {@link Aggregation}
@@ -83,7 +81,7 @@ abstract class AggregationUtils {
Meta meta = queryMethod.getQueryMetaAttributes();
if (StringUtils.hasText(meta.getComment())) {
if (meta.hasComment()) {
builder.comment(meta.getComment());
}
@@ -91,8 +89,8 @@ abstract class AggregationUtils {
builder.cursorBatchSize(meta.getCursorBatchSize());
}
if (meta.getMaxTimeMsec() != null && meta.getMaxTimeMsec() > 0) {
builder.maxTime(Duration.ofMillis(meta.getMaxTimeMsec()));
if (meta.hasMaxTime()) {
builder.maxTime(Duration.ofMillis(meta.getRequiredMaxTimeMsec()));
}
if (meta.getAllowDiskUse() != null) {
@@ -109,7 +107,7 @@ abstract class AggregationUtils {
* @param accessor
* @param targetType
*/
static void appendSortIfPresent(List<AggregationOperation> aggregationPipeline, ConvertingParameterAccessor accessor,
static void appendSortIfPresent(AggregationPipeline aggregationPipeline, ConvertingParameterAccessor accessor,
Class<?> targetType) {
if (accessor.getSort().isUnsorted()) {
@@ -134,7 +132,7 @@ abstract class AggregationUtils {
* @param aggregationPipeline
* @param accessor
*/
static void appendLimitAndOffsetIfPresent(List<AggregationOperation> aggregationPipeline,
static void appendLimitAndOffsetIfPresent(AggregationPipeline aggregationPipeline,
ConvertingParameterAccessor accessor) {
appendLimitAndOffsetIfPresent(aggregationPipeline, accessor, LongUnaryOperator.identity(),
IntUnaryOperator.identity());
@@ -150,7 +148,7 @@ abstract class AggregationUtils {
* @param limitOperator
* @since 3.3
*/
static void appendLimitAndOffsetIfPresent(List<AggregationOperation> aggregationPipeline,
static void appendLimitAndOffsetIfPresent(AggregationPipeline aggregationPipeline,
ConvertingParameterAccessor accessor, LongUnaryOperator offsetOperator, IntUnaryOperator limitOperator) {
Pageable pageable = accessor.getPageable();

View File

@@ -38,6 +38,7 @@ import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.UpdateDefinition;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -55,6 +56,7 @@ import com.mongodb.client.result.DeleteResult;
@FunctionalInterface
interface MongoQueryExecution {
@Nullable
Object execute(Query query);
/**
@@ -291,7 +293,6 @@ interface MongoQueryExecution {
final class UpdateExecution implements MongoQueryExecution {
private final ExecutableUpdate<?> updateOps;
private final MongoQueryMethod method;
private Supplier<UpdateDefinition> updateDefinitionSupplier;
private final MongoParameterAccessor accessor;
@@ -299,7 +300,6 @@ interface MongoQueryExecution {
MongoParameterAccessor accessor) {
this.updateOps = updateOps;
this.method = method;
this.updateDefinitionSupplier = updateSupplier;
this.accessor = accessor;
}

View File

@@ -61,7 +61,7 @@ interface ReactiveMongoQueryExecution {
*
* @author Mark Paluch
*/
class GeoNearExecution implements ReactiveMongoQueryExecution {
final class GeoNearExecution implements ReactiveMongoQueryExecution {
private final ReactiveMongoOperations operations;
private final MongoParameterAccessor accessor;
@@ -83,7 +83,7 @@ interface ReactiveMongoQueryExecution {
}
@SuppressWarnings({ "unchecked", "rawtypes" })
protected Flux<GeoResult<Object>> doExecuteQuery(@Nullable Query query, Class<?> type, String collection) {
private Flux<GeoResult<Object>> doExecuteQuery(@Nullable Query query, Class<?> type, String collection) {
Point nearLocation = accessor.getGeoNearLocation();
NearQuery nearQuery = NearQuery.near(nearLocation);
@@ -154,7 +154,6 @@ interface ReactiveMongoQueryExecution {
final class UpdateExecution implements ReactiveMongoQueryExecution {
private final ReactiveUpdate<?> updateOps;
private final MongoQueryMethod method;
private final MongoParameterAccessor accessor;
private Mono<UpdateDefinition> update;
@@ -162,7 +161,6 @@ interface ReactiveMongoQueryExecution {
Mono<UpdateDefinition> update) {
this.updateOps = updateOps;
this.method = method;
this.accessor = accessor;
this.update = update;
}

View File

@@ -26,12 +26,15 @@ import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
import org.springframework.data.mongodb.core.aggregation.AggregationPipeline;
import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.data.util.TypeInformation;
import org.springframework.expression.ExpressionParser;
import org.springframework.util.ClassUtils;
@@ -68,10 +71,6 @@ public class ReactiveStringBasedAggregation extends AbstractReactiveMongoQuery {
this.evaluationContextProvider = evaluationContextProvider;
}
/*
* (non-Javascript)
* @see org.springframework.data.mongodb.repository.query.AbstractReactiveMongoQuery#doExecute(org.springframework.data.mongodb.repository.query.ReactiveMongoQueryMethod, org.springframework.data.repository.query.ResultProcessor, org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor, java.lang.Class)
*/
@Override
protected Publisher<Object> doExecute(ReactiveMongoQueryMethod method, ResultProcessor processor,
ConvertingParameterAccessor accessor, Class<?> typeToRead) {
@@ -81,7 +80,7 @@ public class ReactiveStringBasedAggregation extends AbstractReactiveMongoQuery {
Class<?> sourceType = method.getDomainClass();
Class<?> targetType = typeToRead;
List<AggregationOperation> pipeline = it;
AggregationPipeline pipeline = new AggregationPipeline(it);
AggregationUtils.appendSortIfPresent(pipeline, accessor, typeToRead);
AggregationUtils.appendLimitAndOffsetIfPresent(pipeline, accessor);
@@ -93,10 +92,13 @@ public class ReactiveStringBasedAggregation extends AbstractReactiveMongoQuery {
targetType = Document.class;
}
AggregationOptions options = computeOptions(method, accessor);
TypedAggregation<?> aggregation = new TypedAggregation<>(sourceType, pipeline, options);
AggregationOptions options = computeOptions(method, accessor, pipeline);
TypedAggregation<?> aggregation = new TypedAggregation<>(sourceType, pipeline.getOperations(), options);
Flux<?> flux = reactiveMongoOperations.aggregate(aggregation, targetType);
if (ReflectionUtils.isVoid(typeToRead)) {
return flux.then();
}
if (isSimpleReturnType && !isRawReturnType) {
flux = flux.handle((item, sink) -> {
@@ -121,7 +123,8 @@ public class ReactiveStringBasedAggregation extends AbstractReactiveMongoQuery {
return parseAggregationPipeline(getQueryMethod().getAnnotatedAggregation(), accessor);
}
private AggregationOptions computeOptions(MongoQueryMethod method, ConvertingParameterAccessor accessor) {
private AggregationOptions computeOptions(MongoQueryMethod method, ConvertingParameterAccessor accessor,
AggregationPipeline pipeline) {
AggregationOptions.Builder builder = Aggregation.newAggregationOptions();
@@ -129,49 +132,37 @@ public class ReactiveStringBasedAggregation extends AbstractReactiveMongoQuery {
expressionParser, evaluationContextProvider);
AggregationUtils.applyMeta(builder, method);
TypeInformation<?> returnType = method.getReturnType();
if (returnType.getComponentType() != null) {
returnType = returnType.getRequiredComponentType();
}
if (ReflectionUtils.isVoid(returnType.getType()) && pipeline.isOutOrMerge()) {
builder.skipOutput();
}
return builder.build();
}
/*
* (non-Javascript)
* @see org.springframework.data.mongodb.repository.query.AbstractReactiveMongoQuery#createQuery(org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor)
*/
@Override
protected Mono<Query> createQuery(ConvertingParameterAccessor accessor) {
throw new UnsupportedOperationException("No query support for aggregation");
}
/*
* (non-Javascript)
* @see org.springframework.data.mongodb.repository.query.AbstractReactiveMongoQuery#isCountQuery()
*/
@Override
protected boolean isCountQuery() {
return false;
}
/*
* (non-Javascript)
* @see org.springframework.data.mongodb.repository.query.AbstractReactiveMongoQuery#isExistsQuery()
*/
@Override
protected boolean isExistsQuery() {
return false;
}
/*
* (non-Javascript)
* @see org.springframework.data.mongodb.repository.query.AbstractReactiveMongoQuery#isDeleteQuery()
*/
@Override
protected boolean isDeleteQuery() {
return false;
}
/*
* (non-Javascript)
* @see org.springframework.data.mongodb.repository.query.AbstractReactiveMongoQuery#isLimiting()
*/
@Override
protected boolean isLimiting() {
return false;

View File

@@ -21,14 +21,13 @@ import java.util.function.LongUnaryOperator;
import java.util.stream.Stream;
import org.bson.Document;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.SliceImpl;
import org.springframework.data.mongodb.InvalidMongoDbApiUsageException;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperation;
import org.springframework.data.mongodb.core.aggregation.AggregationOptions;
import org.springframework.data.mongodb.core.aggregation.AggregationPipeline;
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
import org.springframework.data.mongodb.core.convert.MongoConverter;
@@ -36,7 +35,9 @@ import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.ResultProcessor;
import org.springframework.data.util.ReflectionUtils;
import org.springframework.expression.ExpressionParser;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
/**
@@ -60,8 +61,8 @@ public class StringBasedAggregation extends AbstractMongoQuery {
*
* @param method must not be {@literal null}.
* @param mongoOperations must not be {@literal null}.
* @param expressionParser
* @param evaluationContextProvider
* @param expressionParser must not be {@literal null}.
* @param evaluationContextProvider must not be {@literal null}.
*/
public StringBasedAggregation(MongoQueryMethod method, MongoOperations mongoOperations,
ExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) {
@@ -79,18 +80,15 @@ public class StringBasedAggregation extends AbstractMongoQuery {
this.evaluationContextProvider = evaluationContextProvider;
}
/*
* (non-Javascript)
* @see org.springframework.data.mongodb.repository.query.AbstractReactiveMongoQuery#doExecute(org.springframework.data.mongodb.repository.query.MongoQueryMethod, org.springframework.data.repository.query.ResultProcessor, org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor, java.lang.Class)
*/
@Override
@Nullable
protected Object doExecute(MongoQueryMethod method, ResultProcessor resultProcessor,
ConvertingParameterAccessor accessor, Class<?> typeToRead) {
Class<?> sourceType = method.getDomainClass();
Class<?> targetType = typeToRead;
List<AggregationOperation> pipeline = computePipeline(method, accessor);
AggregationPipeline pipeline = computePipeline(method, accessor);
AggregationUtils.appendSortIfPresent(pipeline, accessor, typeToRead);
if (method.isSliceQuery()) {
@@ -111,8 +109,8 @@ public class StringBasedAggregation extends AbstractMongoQuery {
targetType = method.getReturnType().getRequiredActualType().getRequiredComponentType().getType();
}
AggregationOptions options = computeOptions(method, accessor);
TypedAggregation<?> aggregation = new TypedAggregation<>(sourceType, pipeline, options);
AggregationOptions options = computeOptions(method, accessor, pipeline);
TypedAggregation<?> aggregation = new TypedAggregation<>(sourceType, pipeline.getOperations(), options);
if (method.isStreamQuery()) {
@@ -126,6 +124,9 @@ public class StringBasedAggregation extends AbstractMongoQuery {
}
AggregationResults<Object> result = (AggregationResults<Object>) mongoOperations.aggregate(aggregation, targetType);
if (ReflectionUtils.isVoid(typeToRead)) {
return null;
}
if (isRawAggregationResult) {
return result;
@@ -167,11 +168,12 @@ public class StringBasedAggregation extends AbstractMongoQuery {
return MongoSimpleTypes.HOLDER.isSimpleType(targetType);
}
List<AggregationOperation> computePipeline(MongoQueryMethod method, ConvertingParameterAccessor accessor) {
return parseAggregationPipeline(method.getAnnotatedAggregation(), accessor);
AggregationPipeline computePipeline(MongoQueryMethod method, ConvertingParameterAccessor accessor) {
return new AggregationPipeline(parseAggregationPipeline(method.getAnnotatedAggregation(), accessor));
}
private AggregationOptions computeOptions(MongoQueryMethod method, ConvertingParameterAccessor accessor) {
private AggregationOptions computeOptions(MongoQueryMethod method, ConvertingParameterAccessor accessor,
AggregationPipeline pipeline) {
AggregationOptions.Builder builder = Aggregation.newAggregationOptions();
@@ -179,49 +181,33 @@ public class StringBasedAggregation extends AbstractMongoQuery {
expressionParser, evaluationContextProvider);
AggregationUtils.applyMeta(builder, method);
if (ReflectionUtils.isVoid(method.getReturnType().getType()) && pipeline.isOutOrMerge()) {
builder.skipOutput();
}
return builder.build();
}
/*
* (non-Javascript)
* @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery#createQuery(org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor)
*/
@Override
protected Query createQuery(ConvertingParameterAccessor accessor) {
throw new UnsupportedOperationException("No query support for aggregation");
}
/*
* (non-Javascript)
* @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery#isCountQuery()
*/
@Override
protected boolean isCountQuery() {
return false;
}
/*
* (non-Javascript)
* @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery#isExistsQuery()
*/
@Override
protected boolean isExistsQuery() {
return false;
}
/*
* (non-Javascript)
* @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery#isDeleteQuery()
*/
@Override
protected boolean isDeleteQuery() {
return false;
}
/*
* (non-Javascript)
* @see org.springframework.data.mongodb.repository.query.AbstractMongoQuery#isLimiting()
*/
@Override
protected boolean isLimiting() {
return false;

View File

@@ -1425,7 +1425,8 @@ public class ParameterBindingJsonReader extends AbstractBsonReader {
// Spring Data Customization START
if (patternToken.getType() == JsonTokenType.STRING || patternToken.getType() == JsonTokenType.UNQUOTED_STRING) {
return bindableValueFor(patternToken).getValue().toString();
Object value = bindableValueFor(patternToken).getValue();
return value != null ? value.toString() : null;
}
throw new JsonParseException("JSON reader expected a string but found '%s'.", patternToken.getValue());

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.aot;
import static org.assertj.core.api.Assertions.*;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.aot.generate.ClassNameGenerator;
import org.springframework.aot.generate.DefaultGenerationContext;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.generate.InMemoryGeneratedFiles;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.javapoet.ClassName;
/**
* Unit tests for {@link LazyLoadingProxyAotProcessor}.
*
* @author Christoph Strobl
*/
class LazyLoadingProxyAotProcessorUnitTests {
@Test // GH-4351
void registersProxyForLazyDbRefCorrectlyWhenTypeIsCollectionInterface() {
GenerationContext ctx = new DefaultGenerationContext(new ClassNameGenerator(ClassName.get(this.getClass())),
new InMemoryGeneratedFiles());
new LazyLoadingProxyAotProcessor().registerLazyLoadingProxyIfNeeded(A.class, ctx);
assertThat(ctx.getRuntimeHints())
.satisfies(RuntimeHintsPredicates.proxies().forInterfaces(java.util.Collection.class,
org.springframework.data.mongodb.core.convert.LazyLoadingProxy.class, java.util.List.class,
org.springframework.aop.SpringProxy.class, org.springframework.aop.framework.Advised.class,
org.springframework.core.DecoratingProxy.class)::test);
}
static class A {
String id;
@DBRef(lazy = true) //
List<B> listRef;
}
static class B {
String id;
}
}

View File

@@ -2300,6 +2300,26 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
verify(collection).countDocuments(any(Document.class), any());
}
@Test // GH-4374
void countConsidersMaxTimeMs() {
template.count(new BasicQuery("{ 'spring' : 'data-mongodb' }").maxTimeMsec(5000), Human.class);
ArgumentCaptor<CountOptions> options = ArgumentCaptor.forClass(CountOptions.class);
verify(collection).countDocuments(any(Document.class), options.capture());
assertThat(options.getValue().getMaxTime(TimeUnit.MILLISECONDS)).isEqualTo(5000);
}
@Test // GH-4374
void countPassesOnComment() {
template.count(new BasicQuery("{ 'spring' : 'data-mongodb' }").comment("rocks!"), Human.class);
ArgumentCaptor<CountOptions> options = ArgumentCaptor.forClass(CountOptions.class);
verify(collection).countDocuments(any(Document.class), options.capture());
assertThat(options.getValue().getComment()).isEqualTo(BsonUtils.simpleToBsonValue("rocks!"));
}
@Test // GH-3984
void templatePassesOnTimeSeriesOptionsWhenNoTypeGiven() {

View File

@@ -23,6 +23,7 @@ import static org.springframework.data.mongodb.test.util.Assertions.assertThat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.mongodb.util.BsonUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
@@ -1456,6 +1457,26 @@ public class ReactiveMongoTemplateUnitTests {
verify(collection).countDocuments(any(Document.class), any());
}
@Test // GH-4374
void countConsidersMaxTimeMs() {
template.count(new BasicQuery("{ 'spring' : 'data-mongodb' }").maxTimeMsec(5000), Person.class).subscribe();
ArgumentCaptor<CountOptions> options = ArgumentCaptor.forClass(CountOptions.class);
verify(collection).countDocuments(any(Document.class), options.capture());
assertThat(options.getValue().getMaxTime(TimeUnit.MILLISECONDS)).isEqualTo(5000);
}
@Test // GH-4374
void countPassesOnComment() {
template.count(new BasicQuery("{ 'spring' : 'data-mongodb' }").comment("rocks!"), Person.class).subscribe();
ArgumentCaptor<CountOptions> options = ArgumentCaptor.forClass(CountOptions.class);
verify(collection).countDocuments(any(Document.class), options.capture());
assertThat(options.getValue().getComment()).isEqualTo(BsonUtils.simpleToBsonValue("rocks!"));
}
@Test // GH-2911
void insertErrorsOnPublisher() {

View File

@@ -2831,6 +2831,18 @@ class MappingMongoConverterUnitTests {
assertThat(converter.read(Cyclic.class, source).cycle.value).isEqualTo("v2");
}
@Test // GH-4371
void shouldConvertTypesToStringTargetType() {
org.bson.Document source = org.bson.Document.parse("""
{
city : ["Gotham", "Metropolis"]
}
""");
assertThat(converter.read(Address.class, source).city).isEqualTo("Gotham,Metropolis");
}
static class GenericType<T> {
T content;
}

View File

@@ -274,7 +274,8 @@ public class MongoQueryMethodUnitTests {
void queryCreationForUpdateMethodFailsOnInvalidReturnType() throws Exception {
assertThatExceptionOfType(IllegalStateException.class) //
.isThrownBy(() -> queryMethod(InvalidUpdateMethodRepo.class, "findAndIncrementVisitsByFirstname", String.class).verify()) //
.isThrownBy(() -> queryMethod(InvalidUpdateMethodRepo.class, "findAndIncrementVisitsByFirstname", String.class)
.verify()) //
.withMessageContaining("Update") //
.withMessageContaining("numeric") //
.withMessageContaining("findAndIncrementVisitsByFirstname");
@@ -283,7 +284,8 @@ public class MongoQueryMethodUnitTests {
@Test // GH-3002
void readsCollationFromAtCollationAnnotation() throws Exception {
MongoQueryMethod method = queryMethod(PersonRepository.class, "findWithCollationFromAtCollationByFirstname", String.class);
MongoQueryMethod method = queryMethod(PersonRepository.class, "findWithCollationFromAtCollationByFirstname",
String.class);
assertThat(method.hasAnnotatedCollation()).isTrue();
assertThat(method.getAnnotatedCollation()).isEqualTo("en_US");
@@ -292,7 +294,8 @@ public class MongoQueryMethodUnitTests {
@Test // GH-3002
void readsCollationFromAtQueryAnnotation() throws Exception {
MongoQueryMethod method = queryMethod(PersonRepository.class, "findWithCollationFromAtQueryByFirstname", String.class);
MongoQueryMethod method = queryMethod(PersonRepository.class, "findWithCollationFromAtQueryByFirstname",
String.class);
assertThat(method.hasAnnotatedCollation()).isTrue();
assertThat(method.getAnnotatedCollation()).isEqualTo("en_US");
@@ -301,7 +304,8 @@ public class MongoQueryMethodUnitTests {
@Test // GH-3002
void annotatedCollationClashSelectsAtCollationAnnotationValue() throws Exception {
MongoQueryMethod method = queryMethod(PersonRepository.class, "findWithMultipleCollationsFromAtQueryAndAtCollationByFirstname", String.class);
MongoQueryMethod method = queryMethod(PersonRepository.class,
"findWithMultipleCollationsFromAtQueryAndAtCollationByFirstname", String.class);
assertThat(method.hasAnnotatedCollation()).isTrue();
assertThat(method.getAnnotatedCollation()).isEqualTo("de_AT");

View File

@@ -78,6 +78,7 @@ public class ReactiveStringBasedAggregationUnitTests {
private static final String RAW_SORT_STRING = "{ '$sort' : { 'lastname' : -1 } }";
private static final String RAW_GROUP_BY_LASTNAME_STRING = "{ '$group': { '_id' : '$lastname', 'names' : { '$addToSet' : '$firstname' } } }";
private static final String RAW_OUT = "{ '$out' : 'authors' }";
private static final String GROUP_BY_LASTNAME_STRING_WITH_PARAMETER_PLACEHOLDER = "{ '$group': { '_id' : '$lastname', names : { '$addToSet' : '$?0' } } }";
private static final String GROUP_BY_LASTNAME_STRING_WITH_SPEL_PARAMETER_PLACEHOLDER = "{ '$group': { '_id' : '$lastname', 'names' : { '$addToSet' : '$?#{[0]}' } } }";
@@ -188,6 +189,22 @@ public class ReactiveStringBasedAggregationUnitTests {
return new AggregationInvocation(aggregationCaptor.getValue(), targetTypeCaptor.getValue(), result);
}
@Test // GH-4088
void aggregateWithVoidReturnTypeSkipsResultOnOutStage() {
AggregationInvocation invocation = executeAggregation("outSkipResult");
assertThat(skipResultsOf(invocation)).isTrue();
}
@Test // GH-4088
void aggregateWithOutStageDoesNotSkipResults() {
AggregationInvocation invocation = executeAggregation("outDoNotSkipResult");
assertThat(skipResultsOf(invocation)).isFalse();
}
private ReactiveStringBasedAggregation createAggregationForMethod(String name, Class<?>... parameters) {
Method method = ClassUtils.getMethod(SampleRepository.class, name, parameters);
@@ -216,6 +233,11 @@ public class ReactiveStringBasedAggregationUnitTests {
: null;
}
private Boolean skipResultsOf(AggregationInvocation invocation) {
return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().isSkipResults()
: false;
}
private Class<?> targetTypeOf(AggregationInvocation invocation) {
return invocation.getTargetType();
}
@@ -243,6 +265,12 @@ public class ReactiveStringBasedAggregationUnitTests {
@Aggregation(pipeline = RAW_GROUP_BY_LASTNAME_STRING, collation = "de_AT")
Mono<PersonAggregate> aggregateWithCollation(Collation collation);
@Aggregation(pipeline = { RAW_GROUP_BY_LASTNAME_STRING, RAW_OUT })
Flux<Person> outDoNotSkipResult();
@Aggregation(pipeline = { RAW_GROUP_BY_LASTNAME_STRING, RAW_OUT })
Mono<Void> outSkipResult();
}
static class PersonAggregate {

View File

@@ -91,6 +91,7 @@ public class StringBasedAggregationUnitTests {
private static final String RAW_SORT_STRING = "{ '$sort' : { 'lastname' : -1 } }";
private static final String RAW_GROUP_BY_LASTNAME_STRING = "{ '$group': { '_id' : '$lastname', 'names' : { '$addToSet' : '$firstname' } } }";
private static final String RAW_OUT = "{ '$out' : 'authors' }";
private static final String GROUP_BY_LASTNAME_STRING_WITH_PARAMETER_PLACEHOLDER = "{ '$group': { '_id' : '$lastname', names : { '$addToSet' : '$?0' } } }";
private static final String GROUP_BY_LASTNAME_STRING_WITH_SPEL_PARAMETER_PLACEHOLDER = "{ '$group': { '_id' : '$lastname', 'names' : { '$addToSet' : '$?#{[0]}' } } }";
@@ -260,6 +261,22 @@ public class StringBasedAggregationUnitTests {
.withMessageContaining("Page");
}
@Test // GH-4088
void aggregateWithVoidReturnTypeSkipsResultOnOutStage() {
AggregationInvocation invocation = executeAggregation("outSkipResult");
assertThat(skipResultsOf(invocation)).isTrue();
}
@Test // GH-4088
void aggregateWithOutStageDoesNotSkipResults() {
AggregationInvocation invocation = executeAggregation("outDoNotSkipResult");
assertThat(skipResultsOf(invocation)).isFalse();
}
private AggregationInvocation executeAggregation(String name, Object... args) {
Class<?>[] argTypes = Arrays.stream(args).map(Object::getClass).toArray(Class[]::new);
@@ -302,6 +319,11 @@ public class StringBasedAggregationUnitTests {
: null;
}
private Boolean skipResultsOf(AggregationInvocation invocation) {
return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().isSkipResults()
: false;
}
private Class<?> targetTypeOf(AggregationInvocation invocation) {
return invocation.getTargetType();
}
@@ -350,6 +372,12 @@ public class StringBasedAggregationUnitTests {
@Aggregation(RAW_GROUP_BY_LASTNAME_STRING)
String simpleReturnType();
@Aggregation(pipeline = { RAW_GROUP_BY_LASTNAME_STRING, RAW_OUT })
List<Person> outDoNotSkipResult();
@Aggregation(pipeline = { RAW_GROUP_BY_LASTNAME_STRING, RAW_OUT })
void outSkipResult();
}
private interface UnsupportedRepository extends Repository<Person, Long> {

View File

@@ -28,7 +28,6 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
@@ -43,8 +42,6 @@ import org.springframework.data.mongodb.repository.query.MongoEntityInformation;
class SimpleReactiveMongoRepositoryUnitTests {
private SimpleReactiveMongoRepository<Object, String> repository;
@Mock Mono mono;
@Mock Flux flux;
@Mock ReactiveMongoOperations mongoOperations;
@Mock MongoEntityInformation<Object, String> entityInformation;
@@ -56,7 +53,7 @@ class SimpleReactiveMongoRepositoryUnitTests {
@Test // DATAMONGO-1854
void shouldAddDefaultCollationToCountForExampleIfPresent() {
when(mongoOperations.count(any(), any(), any())).thenReturn(mono);
when(mongoOperations.count(any(), any(), any())).thenReturn(Mono.just(0L));
Collation collation = Collation.of("en_US");
@@ -72,7 +69,7 @@ class SimpleReactiveMongoRepositoryUnitTests {
@Test // DATAMONGO-1854
void shouldAddDefaultCollationToExistsForExampleIfPresent() {
when(mongoOperations.exists(any(), any(), any())).thenReturn(mono);
when(mongoOperations.exists(any(), any(), any())).thenReturn(Mono.just(false));
Collation collation = Collation.of("en_US");
@@ -88,7 +85,7 @@ class SimpleReactiveMongoRepositoryUnitTests {
@Test // DATAMONGO-1854
void shouldAddDefaultCollationToFindForExampleIfPresent() {
when(mongoOperations.find(any(), any(), any())).thenReturn(flux);
when(mongoOperations.find(any(), any(), any())).thenReturn(Flux.empty());
Collation collation = Collation.of("en_US");
@@ -104,7 +101,7 @@ class SimpleReactiveMongoRepositoryUnitTests {
@Test // DATAMONGO-1854
void shouldAddDefaultCollationToFindWithSortForExampleIfPresent() {
when(mongoOperations.find(any(), any(), any())).thenReturn(flux);
when(mongoOperations.find(any(), any(), any())).thenReturn(Flux.empty());
Collation collation = Collation.of("en_US");
@@ -120,7 +117,7 @@ class SimpleReactiveMongoRepositoryUnitTests {
@Test // DATAMONGO-1854
void shouldAddDefaultCollationToFindOneForExampleIfPresent() {
when(mongoOperations.find(any(), any(), any())).thenReturn(flux);
when(mongoOperations.find(any(), any(), any())).thenReturn(Flux.empty());
Collation collation = Collation.of("en_US");

View File

@@ -209,6 +209,13 @@ class ParameterBindingJsonReaderUnitTests {
assertThat(target).isEqualTo(Document.parse("{ 'end_date' : { $gte : { $date : " + time + " } } } "));
}
@Test // GH-4282
public void shouldReturnNullAsSuch() {
String json = "{ 'value' : ObjectId(?0) }";
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> parse(json, new Object[] { null }));
}
@Test // DATAMONGO-2418
void shouldNotAccessSpElEvaluationContextWhenNoSpElPresentInBindableTarget() {

View File

@@ -37,6 +37,12 @@ public interface PersonRepository extends CrudRepository<Person, String> {
@Aggregation("{ '$project': { '_id' : '$lastname' } }")
List<String> findAllLastnames(); <9>
@Aggregation(pipeline = {
"{ $group : { _id : '$author', books: { $push: '$title' } } }",
"{ $out : 'authors' }"
})
void groupAndOutSkippingOutput(); <10>
}
----
[source,java]
@@ -75,6 +81,7 @@ Therefore, the `Sort` properties are mapped against the methods return type `Per
To gain more control, you might consider `AggregationResult` as method return type as shown in <7>.
<8> Obtain the raw `AggregationResults` mapped to the generic target wrapper type `SumValue` or `org.bson.Document`.
<9> Like in <6>, a single value can be directly obtained from multiple result ``Document``s.
<10> Skips the output of the `$out` stage when return type is `void`.
====
In some scenarios, aggregations might require additional options, such as a maximum run time, additional log comments, or the permission to temporarily write data to disk.

View File

@@ -1,4 +1,4 @@
Spring Data MongoDB 4.0.4 (2022.0.4)
Spring Data MongoDB 4.0.6 (2022.0.6)
Copyright (c) [2010-2019] Pivotal Software, Inc.
This product is licensed to you under the Apache License, Version 2.0 (the "License").
@@ -43,6 +43,8 @@ conditions of the subcomponent's license, as noted in the LICENSE file.