diff --git a/pom.xml b/pom.xml
index a6d5da9170..cffda2bc83 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.data
spring-data-mongodb-parent
- 3.3.0-SNAPSHOT
+ 3.3.0-GH-3633-SNAPSHOT
pom
Spring Data MongoDB
diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml
index 0033bd11d5..2f4068b6e8 100644
--- a/spring-data-mongodb-benchmarks/pom.xml
+++ b/spring-data-mongodb-benchmarks/pom.xml
@@ -7,7 +7,7 @@
org.springframework.data
spring-data-mongodb-parent
- 3.3.0-SNAPSHOT
+ 3.3.0-GH-3633-SNAPSHOT
../pom.xml
diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml
index f62c8dc7f4..a1fe90b907 100644
--- a/spring-data-mongodb-distribution/pom.xml
+++ b/spring-data-mongodb-distribution/pom.xml
@@ -14,7 +14,7 @@
org.springframework.data
spring-data-mongodb-parent
- 3.3.0-SNAPSHOT
+ 3.3.0-GH-3633-SNAPSHOT
../pom.xml
diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml
index c1efaea420..5719316cd7 100644
--- a/spring-data-mongodb/pom.xml
+++ b/spring-data-mongodb/pom.xml
@@ -11,7 +11,7 @@
org.springframework.data
spring-data-mongodb-parent
- 3.3.0-SNAPSHOT
+ 3.3.0-GH-3633-SNAPSHOT
../pom.xml
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java
index af93fdd634..5864897184 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java
@@ -751,12 +751,12 @@ protected boolean isKeyword(String candidate) {
* converted one by one.
*
* @param documentField the field and its meta data
- * @param value the actual value
+ * @param value the actual value. Can be {@literal null}.
* @return the potentially converted target value.
*/
- private Object applyFieldTargetTypeHintToValue(Field documentField, Object value) {
+ private Object applyFieldTargetTypeHintToValue(Field documentField, @Nullable Object value) {
- if (documentField.getProperty() == null || !documentField.getProperty().hasExplicitWriteTarget()) {
+ if (value == null || documentField.getProperty() == null || !documentField.getProperty().hasExplicitWriteTarget()) {
return value;
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java
index b0a1b49893..9b1e8df940 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java
@@ -29,6 +29,7 @@
import java.util.stream.Collectors;
import org.bson.BsonRegularExpression;
+import org.bson.BsonType;
import org.bson.Document;
import org.bson.types.Binary;
import org.springframework.data.domain.Example;
@@ -188,6 +189,42 @@ public Criteria is(@Nullable Object value) {
return this;
}
+ /**
+ * Creates a criterion using {@literal null} equality comparison which matches documents that either contain the item
+ * field whose value is {@literal null} or that do not contain the item field.
+ *
+ * Use {@link #isNullValue()} to only query for documents that contain the field whose value is equal to
+ * {@link org.bson.BsonType#NULL}.
+ * Use {@link #exists(boolean)} to query for documents that do (not) contain the field.
+ *
+ * @return this.
+ * @see Query for Null or
+ * Missing Fields: Equality Filter
+ * @since 3.3
+ */
+ public Criteria isNull() {
+ return is(null);
+ }
+
+ /**
+ * Creates a criterion using a {@link org.bson.BsonType} comparison which matches only documents that contain the item
+ * field whose value is equal to {@link org.bson.BsonType#NULL}.
+ *
+ * Use {@link #isNull()} to query for documents that contain the field with a {@literal null} value or do not contain the
+ * field at all.
+ * Use {@link #exists(boolean)} to query for documents that do (not) contain the field.
+ *
+ * @return this.
+ * @see Query for Null or Missing
+ * Fields: Type Check
+ * @since 3.3
+ */
+ public Criteria isNullValue() {
+
+ criteria.put("$type", BsonType.NULL.getValue());
+ return this;
+ }
+
private boolean lastOperatorWasNot() {
return !this.criteria.isEmpty() && "$not".equals(this.criteria.keySet().toArray()[this.criteria.size() - 1]);
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java
index e2f69260b1..69096aeabd 100755
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java
@@ -1257,6 +1257,28 @@ void resolvesFieldNameWithUnderscoreOnNestedMappedFieldnameWithUnderscoresCorrec
assertThat(document).isEqualTo(new org.bson.Document("double_underscore.renamed", new org.bson.Document("$exists", true)));
}
+ @Test // GH-3633
+ void mapsNullValueForFieldWithCustomTargetType() {
+
+ Query query = query(where("stringAsOid").isNull());
+
+ org.bson.Document document = mapper.getMappedObject(query.getQueryObject(),
+ context.getPersistentEntity(NonIdFieldWithObjectIdTargetType.class));
+
+ assertThat(document).isEqualTo(new org.bson.Document("stringAsOid", null));
+ }
+
+ @Test // GH-3633
+ void mapsNullBsonTypeForFieldWithCustomTargetType() {
+
+ Query query = query(where("stringAsOid").isNullValue());
+
+ org.bson.Document document = mapper.getMappedObject(query.getQueryObject(),
+ context.getPersistentEntity(NonIdFieldWithObjectIdTargetType.class));
+
+ assertThat(document).isEqualTo(new org.bson.Document("stringAsOid", new org.bson.Document("$type", 10)));
+ }
+
class WithDeepArrayNesting {
List level0;
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 74a48fc679..2f43bc0425 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
@@ -60,7 +60,9 @@
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
import org.springframework.data.mongodb.core.query.BasicQuery;
+import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.mongodb.repository.Person.Sex;
import org.springframework.data.mongodb.repository.SampleEvaluationContextExtension.SampleSecurityContextHolder;
import org.springframework.data.mongodb.test.util.EnableIfMongoServerVersion;
@@ -1422,4 +1424,13 @@ void annotatedQueryShouldAllowAggregationInProjection() {
Person target = repository.findWithAggregationInProjection(alicia.getId());
assertThat(target.getFirstname()).isEqualTo(alicia.getFirstname().toUpperCase());
}
+
+ @Test // GH-3633
+ void annotatedQueryWithNullEqualityCheckShouldWork() {
+
+ operations.updateFirst(Query.query(Criteria.where("id").is(dave.getId())), Update.update("age", null), Person.class);
+
+ Person byQueryWithNullEqualityCheck = repository.findByQueryWithNullEqualityCheck();
+ assertThat(byQueryWithNullEqualityCheck.getId()).isEqualTo(dave.getId());
+ }
}
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 c3b765c910..5d821a941f 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
@@ -410,4 +410,7 @@ Person findPersonByManyArguments(String firstname, String lastname, String email
List findByUnwrappedUserUsername(String username);
List findByUnwrappedUser(User user);
+
+ @Query("{ 'age' : null }")
+ Person findByQueryWithNullEqualityCheck();
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReaderUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReaderUnitTests.java
index 72ab2b454b..1a684af164 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReaderUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReaderUnitTests.java
@@ -383,6 +383,13 @@ void shouldParseNestedArrays() {
.parse("{ 'stores.location' : { $geoWithin: { $centerSphere: [ [ 1.948516, 48.799029 ] , 0.004 ] } } }"));
}
+ @Test // GH-3633
+ void parsesNullValue() {
+
+ Document target = parse("{ 'parent' : null }");
+ assertThat(target).isEqualTo(new Document("parent", null));
+ }
+
private static Document parse(String json, Object... args) {
ParameterBindingJsonReader reader = new ParameterBindingJsonReader(json, args);