diff --git a/pom.xml b/pom.xml
index f785c3872d..9f9e11b3c4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.data
spring-data-mongodb-parent
- 4.4.0-SNAPSHOT
+ 4.4.0-DATAMONGO-2073-SNAPSHOT
pom
Spring Data MongoDB
diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml
index a3dc49f892..4a4f560269 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
- 4.4.0-SNAPSHOT
+ 4.4.0-DATAMONGO-2073-SNAPSHOT
../pom.xml
diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml
index acdc13437d..e10c2d3312 100644
--- a/spring-data-mongodb-distribution/pom.xml
+++ b/spring-data-mongodb-distribution/pom.xml
@@ -15,7 +15,7 @@
org.springframework.data
spring-data-mongodb-parent
- 4.4.0-SNAPSHOT
+ 4.4.0-DATAMONGO-2073-SNAPSHOT
../pom.xml
diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml
index fafe9c8793..45744cda94 100644
--- a/spring-data-mongodb/pom.xml
+++ b/spring-data-mongodb/pom.xml
@@ -13,7 +13,7 @@
org.springframework.data
spring-data-mongodb-parent
- 4.4.0-SNAPSHOT
+ 4.4.0-DATAMONGO-2073-SNAPSHOT
../pom.xml
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientClientSessionException.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientClientSessionException.java
new file mode 100644
index 0000000000..153240a79c
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientClientSessionException.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2018 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;
+
+import org.springframework.dao.TransientDataAccessException;
+
+/**
+ * {@link TransientDataAccessException} specific to MongoDB {@link com.mongodb.session.ClientSession} related data
+ * access failures such as reading data using an already closed session.
+ *
+ * @author Christoph Strobl
+ * @since 4.4
+ */
+public class TransientClientSessionException extends TransientMongoDbException {
+
+ /**
+ * Constructor for {@link TransientClientSessionException}.
+ *
+ * @param msg the detail message.
+ * @param cause the root cause.
+ */
+ public TransientClientSessionException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientMongoDbException.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientMongoDbException.java
new file mode 100644
index 0000000000..af3bd0d326
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientMongoDbException.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018 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;
+
+import org.springframework.dao.TransientDataAccessException;
+
+/**
+ * Root of the hierarchy of MongoDB specific data access exceptions that are considered transient such as
+ * {@link com.mongodb.MongoException MongoExceptions} carrying {@link com.mongodb.MongoException#hasErrorLabel(String)
+ * specific labels}.
+ *
+ * @author Christoph Strobl
+ * @since 4.4
+ */
+public class TransientMongoDbException extends TransientDataAccessException {
+
+ /**
+ * Constructor for {@link TransientMongoDbException}.
+ *
+ * @param msg the detail message.
+ * @param cause the root cause.
+ */
+ public TransientMongoDbException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientFactoryBean.java
index 64a12e9c0f..231a4df4a4 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientFactoryBean.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientFactoryBean.java
@@ -55,8 +55,6 @@
*/
public class MongoClientFactoryBean extends AbstractFactoryBean implements PersistenceExceptionTranslator {
- private static final PersistenceExceptionTranslator DEFAULT_EXCEPTION_TRANSLATOR = new MongoExceptionTranslator();
-
private @Nullable MongoClientSettings mongoClientSettings;
private @Nullable String host;
private @Nullable Integer port;
@@ -64,7 +62,7 @@ public class MongoClientFactoryBean extends AbstractFactoryBean imp
private @Nullable ConnectionString connectionString;
private @Nullable String replicaSet = null;
- private PersistenceExceptionTranslator exceptionTranslator = DEFAULT_EXCEPTION_TRANSLATOR;
+ private PersistenceExceptionTranslator exceptionTranslator = MongoExceptionTranslator.DEFAULT_EXCEPTION_TRANSLATOR;
/**
* Set the {@link MongoClientSettings} to be used when creating {@link MongoClient}.
@@ -116,23 +114,34 @@ public void setReplicaSet(@Nullable String replicaSet) {
* @param exceptionTranslator
*/
public void setExceptionTranslator(@Nullable PersistenceExceptionTranslator exceptionTranslator) {
- this.exceptionTranslator = exceptionTranslator == null ? DEFAULT_EXCEPTION_TRANSLATOR : exceptionTranslator;
- }
-
- public Class extends MongoClient> getObjectType() {
- return MongoClient.class;
+ this.exceptionTranslator = exceptionTranslator == null ? MongoExceptionTranslator.DEFAULT_EXCEPTION_TRANSLATOR
+ : exceptionTranslator;
}
+ @Override
@Nullable
public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
return exceptionTranslator.translateExceptionIfPossible(ex);
}
+ @Override
+ public Class extends MongoClient> getObjectType() {
+ return MongoClient.class;
+ }
+
@Override
protected MongoClient createInstance() throws Exception {
return createMongoClient(computeClientSetting());
}
+ @Override
+ protected void destroyInstance(@Nullable MongoClient instance) throws Exception {
+
+ if (instance != null) {
+ instance.close();
+ }
+ }
+
/**
* Create {@link MongoClientSettings} based on configuration and priority (lower is better).
*
@@ -324,14 +333,6 @@ private T computeSettingsValue(T defaultValue, T fromSettings, T fromConnect
return !fromConnectionStringIsDefault ? fromConnectionString : defaultValue;
}
- @Override
- protected void destroyInstance(@Nullable MongoClient instance) throws Exception {
-
- if (instance != null) {
- instance.close();
- }
- }
-
private MongoClient createMongoClient(MongoClientSettings settings) throws UnknownHostException {
return MongoClients.create(settings, SpringDataMongoDB.driverInformation());
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoDatabaseFactorySupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoDatabaseFactorySupport.java
index 7e363632df..a73b426dc1 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoDatabaseFactorySupport.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoDatabaseFactorySupport.java
@@ -32,8 +32,7 @@
/**
* Common base class for usage with both {@link com.mongodb.client.MongoClients} defining common properties such as
- * database name and exception translator.
- *
+ * database name and exception translator.
* Not intended to be used directly.
*
* @author Christoph Strobl
@@ -47,8 +46,8 @@ public abstract class MongoDatabaseFactorySupport implements MongoDatabaseFac
private final C mongoClient;
private final String databaseName;
private final boolean mongoInstanceCreated;
- private final PersistenceExceptionTranslator exceptionTranslator;
+ private PersistenceExceptionTranslator exceptionTranslator;
private @Nullable WriteConcern writeConcern;
/**
@@ -75,15 +74,31 @@ protected MongoDatabaseFactorySupport(C mongoClient, String databaseName, boolea
this.exceptionTranslator = exceptionTranslator;
}
+ /**
+ * Configures the {@link PersistenceExceptionTranslator} to be used.
+ *
+ * @param exceptionTranslator the exception translator to set.
+ * @since 4.4
+ */
+ public void setExceptionTranslator(PersistenceExceptionTranslator exceptionTranslator) {
+ this.exceptionTranslator = exceptionTranslator;
+ }
+
+ @Override
+ public PersistenceExceptionTranslator getExceptionTranslator() {
+ return this.exceptionTranslator;
+ }
+
/**
* Configures the {@link WriteConcern} to be used on the {@link MongoDatabase} instance being created.
*
- * @param writeConcern the writeConcern to set
+ * @param writeConcern the writeConcern to set.
*/
public void setWriteConcern(WriteConcern writeConcern) {
this.writeConcern = writeConcern;
}
+ @Override
public MongoDatabase getMongoDatabase() throws DataAccessException {
return getMongoDatabase(getDefaultDatabaseName());
}
@@ -116,10 +131,7 @@ public void destroy() throws Exception {
}
}
- public PersistenceExceptionTranslator getExceptionTranslator() {
- return this.exceptionTranslator;
- }
-
+ @Override
public MongoDatabaseFactory withSession(ClientSession session) {
return new MongoDatabaseFactorySupport.ClientSessionBoundMongoDbFactory(session, this);
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java
index 4775a4a4d2..8fa4503058 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java
@@ -18,6 +18,7 @@
import java.util.Set;
import org.bson.BsonInvalidOperationException;
+
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.dao.DataIntegrityViolationException;
@@ -27,7 +28,7 @@
import org.springframework.dao.PermissionDeniedDataAccessException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
import org.springframework.data.mongodb.ClientSessionException;
-import org.springframework.data.mongodb.MongoTransactionException;
+import org.springframework.data.mongodb.TransientClientSessionException;
import org.springframework.data.mongodb.UncategorizedMongoDbException;
import org.springframework.data.mongodb.util.MongoDbErrorCodes;
import org.springframework.lang.Nullable;
@@ -51,6 +52,8 @@
*/
public class MongoExceptionTranslator implements PersistenceExceptionTranslator {
+ public static final MongoExceptionTranslator DEFAULT_EXCEPTION_TRANSLATOR = new MongoExceptionTranslator();
+
private static final Set DUPLICATE_KEY_EXCEPTIONS = Set.of("MongoException.DuplicateKey",
"DuplicateKeyException");
@@ -65,8 +68,14 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
private static final Set SECURITY_EXCEPTIONS = Set.of("MongoCryptException");
+ @Override
@Nullable
public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
+ return doTranslateException(ex);
+ }
+
+ @Nullable
+ DataAccessException doTranslateException(RuntimeException ex) {
// Check for well-known MongoException subclasses.
@@ -94,13 +103,13 @@ public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
if (DATA_INTEGRITY_EXCEPTIONS.contains(exception)) {
- if (ex instanceof MongoServerException mse) {
- if (mse.getCode() == 11000) {
+ if (ex instanceof MongoServerException) {
+ if (MongoDbErrorCodes.isDataDuplicateKeyError(ex)) {
return new DuplicateKeyException(ex.getMessage(), ex);
}
if (ex instanceof MongoBulkWriteException bulkException) {
- for (BulkWriteError x : bulkException.getWriteErrors()) {
- if (x.getCode() == 11000) {
+ for (BulkWriteError writeError : bulkException.getWriteErrors()) {
+ if (MongoDbErrorCodes.isDuplicateKeyCode(writeError.getCode())) {
return new DuplicateKeyException(ex.getMessage(), ex);
}
}
@@ -115,20 +124,34 @@ public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
int code = mongoException.getCode();
- if (MongoDbErrorCodes.isDuplicateKeyCode(code)) {
+ if (MongoDbErrorCodes.isDuplicateKeyError(mongoException)) {
return new DuplicateKeyException(ex.getMessage(), ex);
- } else if (MongoDbErrorCodes.isDataAccessResourceFailureCode(code)) {
+ }
+ if (MongoDbErrorCodes.isDataAccessResourceError(mongoException)) {
return new DataAccessResourceFailureException(ex.getMessage(), ex);
- } else if (MongoDbErrorCodes.isInvalidDataAccessApiUsageCode(code) || code == 10003 || code == 12001
- || code == 12010 || code == 12011 || code == 12012) {
+ }
+ if (MongoDbErrorCodes.isInvalidDataAccessApiUsageError(mongoException) || code == 12001 || code == 12010
+ || code == 12011 || code == 12012) {
return new InvalidDataAccessApiUsageException(ex.getMessage(), ex);
- } else if (MongoDbErrorCodes.isPermissionDeniedCode(code)) {
+ }
+ if (MongoDbErrorCodes.isPermissionDeniedError(mongoException)) {
return new PermissionDeniedDataAccessException(ex.getMessage(), ex);
- } else if (MongoDbErrorCodes.isClientSessionFailureCode(code)) {
- return new ClientSessionException(ex.getMessage(), ex);
- } else if (MongoDbErrorCodes.isTransactionFailureCode(code)) {
- return new MongoTransactionException(ex.getMessage(), ex);
- } else if(ex.getCause() != null && SECURITY_EXCEPTIONS.contains(ClassUtils.getShortName(ex.getCause().getClass()))) {
+ }
+ if (MongoDbErrorCodes.isDataIntegrityViolationError(mongoException)) {
+ return new DataIntegrityViolationException(mongoException.getMessage(), mongoException);
+ }
+ if (MongoDbErrorCodes.isClientSessionFailure(mongoException)) {
+ return isTransientFailure(mongoException) ? new TransientClientSessionException(ex.getMessage(), ex)
+ : new ClientSessionException(ex.getMessage(), ex);
+ }
+ if (MongoDbErrorCodes.isDataIntegrityViolationError(mongoException)) {
+ return new DataIntegrityViolationException(mongoException.getMessage(), mongoException);
+ }
+ if (MongoDbErrorCodes.isClientSessionFailure(mongoException)) {
+ return isTransientFailure(mongoException) ? new TransientClientSessionException(ex.getMessage(), ex)
+ : new ClientSessionException(ex.getMessage(), ex);
+ }
+ if (ex.getCause() != null && SECURITY_EXCEPTIONS.contains(ClassUtils.getShortName(ex.getCause().getClass()))) {
return new PermissionDeniedDataAccessException(ex.getMessage(), ex);
}
@@ -150,4 +173,23 @@ public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
// that translation should not occur.
return null;
}
+
+ /**
+ * Check if a given exception holds an error label indicating a transient failure.
+ *
+ * @param e the exception to inspect.
+ * @return {@literal true} if the given {@link Exception} is a {@link MongoException} holding one of the transient
+ * exception error labels.
+ * @see MongoException#hasErrorLabel(String)
+ * @since 4.4
+ */
+ public boolean isTransientFailure(Exception e) {
+
+ if (!(e instanceof MongoException mongoException)) {
+ return false;
+ }
+
+ return mongoException.hasErrorLabel(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL)
+ || mongoException.hasErrorLabel(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL);
+ }
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoClientFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoClientFactoryBean.java
index f7755773d9..615599de36 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoClientFactoryBean.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoClientFactoryBean.java
@@ -36,13 +36,11 @@
public class ReactiveMongoClientFactoryBean extends AbstractFactoryBean
implements PersistenceExceptionTranslator {
- private static final PersistenceExceptionTranslator DEFAULT_EXCEPTION_TRANSLATOR = new MongoExceptionTranslator();
-
private @Nullable String connectionString;
private @Nullable String host;
private @Nullable Integer port;
private @Nullable MongoClientSettings mongoClientSettings;
- private PersistenceExceptionTranslator exceptionTranslator = DEFAULT_EXCEPTION_TRANSLATOR;
+ private PersistenceExceptionTranslator exceptionTranslator = MongoExceptionTranslator.DEFAULT_EXCEPTION_TRANSLATOR;
/**
* Configures the host to connect to.
@@ -86,7 +84,13 @@ public void setMongoClientSettings(@Nullable MongoClientSettings mongoClientSett
* @param exceptionTranslator
*/
public void setExceptionTranslator(@Nullable PersistenceExceptionTranslator exceptionTranslator) {
- this.exceptionTranslator = exceptionTranslator == null ? DEFAULT_EXCEPTION_TRANSLATOR : exceptionTranslator;
+ this.exceptionTranslator = exceptionTranslator == null ? MongoExceptionTranslator.DEFAULT_EXCEPTION_TRANSLATOR
+ : exceptionTranslator;
+ }
+
+ @Override
+ public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
+ return exceptionTranslator.translateExceptionIfPossible(ex);
}
@Override
@@ -123,8 +127,4 @@ protected void destroyInstance(@Nullable MongoClient instance) throws Exception
instance.close();
}
- @Override
- public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
- return exceptionTranslator.translateExceptionIfPossible(ex);
- }
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleMongoClientDatabaseFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleMongoClientDatabaseFactory.java
index e1e77c75e9..6d61d8a8b4 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleMongoClientDatabaseFactory.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleMongoClientDatabaseFactory.java
@@ -72,7 +72,7 @@ public SimpleMongoClientDatabaseFactory(MongoClient mongoClient, String database
* @param mongoInstanceCreated
*/
SimpleMongoClientDatabaseFactory(MongoClient mongoClient, String databaseName, boolean mongoInstanceCreated) {
- super(mongoClient, databaseName, mongoInstanceCreated, new MongoExceptionTranslator());
+ super(mongoClient, databaseName, mongoInstanceCreated, MongoExceptionTranslator.DEFAULT_EXCEPTION_TRANSLATOR);
}
@Override
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactory.java
index 65e97831e4..d3a3e1556a 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactory.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactory.java
@@ -51,8 +51,7 @@ public class SimpleReactiveMongoDatabaseFactory implements DisposableBean, React
private final String databaseName;
private final boolean mongoInstanceCreated;
- private final PersistenceExceptionTranslator exceptionTranslator;
-
+ private PersistenceExceptionTranslator exceptionTranslator = MongoExceptionTranslator.DEFAULT_EXCEPTION_TRANSLATOR;
private @Nullable WriteConcern writeConcern;
/**
@@ -85,7 +84,21 @@ private SimpleReactiveMongoDatabaseFactory(MongoClient client, String databaseNa
this.mongo = client;
this.databaseName = databaseName;
this.mongoInstanceCreated = mongoInstanceCreated;
- this.exceptionTranslator = new MongoExceptionTranslator();
+ }
+
+ /**
+ * Configures the {@link PersistenceExceptionTranslator} to be used.
+ *
+ * @param exceptionTranslator the exception translator to set.
+ * @since 4.4
+ */
+ public void setExceptionTranslator(PersistenceExceptionTranslator exceptionTranslator) {
+ this.exceptionTranslator = exceptionTranslator;
+ }
+
+ @Override
+ public PersistenceExceptionTranslator getExceptionTranslator() {
+ return this.exceptionTranslator;
}
/**
@@ -97,10 +110,12 @@ public void setWriteConcern(WriteConcern writeConcern) {
this.writeConcern = writeConcern;
}
+ @Override
public Mono getMongoDatabase() throws DataAccessException {
return getMongoDatabase(databaseName);
}
+ @Override
public Mono getMongoDatabase(String dbName) throws DataAccessException {
Assert.hasText(dbName, "Database name must not be empty");
@@ -118,6 +133,7 @@ public Mono getMongoDatabase(String dbName) throws DataAccessExce
*
* @see DisposableBean#destroy()
*/
+ @Override
public void destroy() throws Exception {
if (mongoInstanceCreated) {
@@ -125,10 +141,6 @@ public void destroy() throws Exception {
}
}
- public PersistenceExceptionTranslator getExceptionTranslator() {
- return this.exceptionTranslator;
- }
-
@Override
public CodecRegistry getCodecRegistry() {
return this.mongo.getDatabase(databaseName).getCodecRegistry();
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java
index fa201d40ea..4f4b9b72e5 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java
@@ -28,7 +28,6 @@
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.context.MappingContextEvent;
import org.springframework.data.mongodb.MongoDatabaseFactory;
-import org.springframework.data.mongodb.UncategorizedMongoDbException;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolver.IndexDefinitionHolder;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
@@ -152,7 +151,7 @@ void createIndex(IndexDefinitionHolder indexDefinition) {
IndexOperations indexOperations = indexOperationsProvider.indexOps(indexDefinition.getCollection());
indexOperations.ensureIndex(indexDefinition);
- } catch (UncategorizedMongoDbException ex) {
+ } catch (DataIntegrityViolationException ex) {
if (ex.getCause() instanceof MongoException mongoException
&& MongoDbErrorCodes.isDataIntegrityViolationCode(mongoException.getCode())) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListener.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListener.java
index 8683ba2439..9f642c1b64 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListener.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/IndexEnsuringQueryCreationListener.java
@@ -21,10 +21,10 @@
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.annotation.AnnotatedElementUtils;
+import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.Order;
-import org.springframework.data.mongodb.UncategorizedMongoDbException;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.index.Index;
import org.springframework.data.mongodb.core.index.IndexOperationsProvider;
@@ -111,7 +111,7 @@ public void onCreation(PartTreeMongoQuery query) {
MongoEntityMetadata> metadata = query.getQueryMethod().getEntityInformation();
try {
indexOperationsProvider.indexOps(metadata.getCollectionName(), metadata.getJavaType()).ensureIndex(index);
- } catch (UncategorizedMongoDbException e) {
+ } catch (DataIntegrityViolationException e) {
if (e.getCause() instanceof MongoException mongoException) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoDbErrorCodes.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoDbErrorCodes.java
index f2e02ae7b9..30cee7b950 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoDbErrorCodes.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoDbErrorCodes.java
@@ -19,6 +19,8 @@
import org.springframework.lang.Nullable;
+import com.mongodb.MongoException;
+
/**
* {@link MongoDbErrorCodes} holds MongoDB specific error codes outlined in {@literal mongo/base/error_codes.yml}.
*
@@ -97,6 +99,7 @@ public final class MongoDbErrorCodes {
invalidDataAccessApiUsageException.put(72, "InvalidOptions");
invalidDataAccessApiUsageException.put(115, "CommandNotSupported");
invalidDataAccessApiUsageException.put(116, "DocTooLargeForCapped");
+ invalidDataAccessApiUsageException.put(10003, "CannotGrowDocumentInCappedNamespace");
invalidDataAccessApiUsageException.put(130, "SymbolNotFound");
invalidDataAccessApiUsageException.put(17280, "KeyTooLong");
invalidDataAccessApiUsageException.put(13334, "ShardKeyTooBig");
@@ -114,20 +117,21 @@ public final class MongoDbErrorCodes {
clientSessionCodes = new HashMap<>(4, 1f);
clientSessionCodes.put(206, "NoSuchSession");
clientSessionCodes.put(213, "DuplicateSession");
+ clientSessionCodes.put(217, "IncompleteTransactionHistory");
+ clientSessionCodes.put(225, "TransactionTooOld");
clientSessionCodes.put(228, "SessionTransferIncomplete");
+ clientSessionCodes.put(244, "TransactionAborted");
+ clientSessionCodes.put(251, "NoSuchTransaction");
+ clientSessionCodes.put(256, "TransactionCommitted");
+ clientSessionCodes.put(257, "TransactionToLarge");
+ clientSessionCodes.put(261, "TooManyLogicalSessions");
+ clientSessionCodes.put(263, "OperationNotSupportedInTransaction");
clientSessionCodes.put(264, "TooManyLogicalSessions");
- transactionCodes = new HashMap<>(8, 1f);
- transactionCodes.put(217, "IncompleteTransactionHistory");
- transactionCodes.put(225, "TransactionTooOld");
- transactionCodes.put(244, "TransactionAborted");
- transactionCodes.put(251, "NoSuchTransaction");
- transactionCodes.put(256, "TransactionCommitted");
- transactionCodes.put(257, "TransactionToLarge");
- transactionCodes.put(263, "OperationNotSupportedInTransaction");
- transactionCodes.put(267, "PreparedTransactionInProgress");
-
- errorCodes = new HashMap<>();
+ errorCodes = new HashMap<>(
+ dataAccessResourceFailureCodes.size() + dataIntegrityViolationCodes.size() + duplicateKeyCodes.size()
+ + invalidDataAccessApiUsageException.size() + permissionDeniedCodes.size() + clientSessionCodes.size(),
+ 1f);
errorCodes.putAll(dataAccessResourceFailureCodes);
errorCodes.putAll(dataIntegrityViolationCodes);
errorCodes.putAll(duplicateKeyCodes);
@@ -136,29 +140,103 @@ public final class MongoDbErrorCodes {
errorCodes.putAll(clientSessionCodes);
}
+ @Nullable
+ public static String getErrorDescription(@Nullable Integer errorCode) {
+ return errorCode == null ? null : errorCodes.get(errorCode);
+ }
+
public static boolean isDataIntegrityViolationCode(@Nullable Integer errorCode) {
return errorCode != null && dataIntegrityViolationCodes.containsKey(errorCode);
}
+ /**
+ * @param exception can be {@literal null}.
+ * @return
+ * @since 4.4
+ */
+ public static boolean isDataIntegrityViolationError(Exception exception) {
+
+ if (exception instanceof MongoException me) {
+ return isDataIntegrityViolationCode(me.getCode());
+ }
+ return false;
+ }
+
public static boolean isDataAccessResourceFailureCode(@Nullable Integer errorCode) {
return errorCode != null && dataAccessResourceFailureCodes.containsKey(errorCode);
}
+ /**
+ * @param exception can be {@literal null}.
+ * @return
+ * @since 4.4
+ */
+ public static boolean isDataAccessResourceError(Exception exception) {
+
+ if (exception instanceof MongoException me) {
+ return isDataAccessResourceFailureCode(me.getCode());
+ }
+ return false;
+ }
+
public static boolean isDuplicateKeyCode(@Nullable Integer errorCode) {
return errorCode != null && duplicateKeyCodes.containsKey(errorCode);
}
+ /**
+ * @param exception can be {@literal null}.
+ * @return
+ * @since 4.4
+ */
+ public static boolean isDuplicateKeyError(Exception exception) {
+
+ if (exception instanceof MongoException me) {
+ return isDuplicateKeyCode(me.getCode());
+ }
+ return false;
+ }
+
+ /**
+ * @param exception can be {@literal null}.
+ * @return
+ * @since 4.4
+ */
+ public static boolean isDataDuplicateKeyError(Exception exception) {
+ return isDuplicateKeyError(exception);
+ }
+
public static boolean isPermissionDeniedCode(@Nullable Integer errorCode) {
return errorCode != null && permissionDeniedCodes.containsKey(errorCode);
}
+ /**
+ * @param exception can be {@literal null}.
+ * @return
+ * @since 4.4
+ */
+ public static boolean isPermissionDeniedError(Exception exception) {
+
+ if (exception instanceof MongoException) {
+ return isPermissionDeniedCode(((MongoException) exception).getCode());
+ }
+ return false;
+ }
+
public static boolean isInvalidDataAccessApiUsageCode(@Nullable Integer errorCode) {
return errorCode != null && invalidDataAccessApiUsageException.containsKey(errorCode);
}
- @Nullable
- public static String getErrorDescription(@Nullable Integer errorCode) {
- return errorCode == null ? null : errorCodes.get(errorCode);
+ /**
+ * @param exception can be {@literal null}.
+ * @return
+ * @since 4.4
+ */
+ public static boolean isInvalidDataAccessApiUsageError(Exception exception) {
+
+ if (exception instanceof MongoException me) {
+ return isInvalidDataAccessApiUsageCode(me.getCode());
+ }
+ return false;
}
/**
@@ -182,4 +260,17 @@ public static boolean isClientSessionFailureCode(@Nullable Integer errorCode) {
public static boolean isTransactionFailureCode(@Nullable Integer errorCode) {
return errorCode != null && transactionCodes.containsKey(errorCode);
}
+
+ /**
+ * @param exception can be {@literal null}.
+ * @return
+ * @since 4.4
+ */
+ public static boolean isClientSessionFailure(Exception exception) {
+
+ if (exception instanceof MongoException me) {
+ return isClientSessionFailureCode(me.getCode());
+ }
+ return false;
+ }
}
diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java
index ff74786cb4..908e128e43 100644
--- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java
+++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java
@@ -20,8 +20,8 @@
import org.bson.BsonDocument;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-
import org.mockito.Mockito;
+
import org.springframework.core.NestedRuntimeException;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataAccessResourceFailureException;
@@ -80,15 +80,13 @@ void translateSocketException() {
void translateSocketExceptionSubclasses() {
expectExceptionWithCauseMessage(
- translator.translateExceptionIfPossible(
- new MongoSocketWriteException("intermediate message", new ServerAddress(), new Exception(EXCEPTION_MESSAGE))
- ),
+ translator.translateExceptionIfPossible(new MongoSocketWriteException("intermediate message",
+ new ServerAddress(), new Exception(EXCEPTION_MESSAGE))),
DataAccessResourceFailureException.class, EXCEPTION_MESSAGE);
expectExceptionWithCauseMessage(
- translator.translateExceptionIfPossible(
- new MongoSocketReadTimeoutException("intermediate message", new ServerAddress(), new Exception(EXCEPTION_MESSAGE))
- ),
+ translator.translateExceptionIfPossible(new MongoSocketReadTimeoutException("intermediate message",
+ new ServerAddress(), new Exception(EXCEPTION_MESSAGE))),
DataAccessResourceFailureException.class, EXCEPTION_MESSAGE);
}
@@ -172,6 +170,27 @@ void translateTransactionExceptions() {
checkTranslatedMongoException(MongoTransactionException.class, 267);
}
+ @Test // DATAMONGO-2073
+ public void translateTransientTransactionExceptions() {
+
+ MongoException source = new MongoException(267, "PreparedTransactionInProgress");
+ source.addLabel(MongoException.TRANSIENT_TRANSACTION_ERROR_LABEL);
+
+ expectExceptionWithCauseMessage(translator.translateExceptionIfPossible(source),
+ UncategorizedMongoDbException.class,
+ "PreparedTransactionInProgress");
+ }
+
+ @Test // DATAMONGO-2073
+ public void translateMongoExceptionWithTransientLabel() {
+
+ MongoException exception = new MongoException(0, "");
+ exception.addLabel(MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL);
+ DataAccessException translatedException = translator.translateExceptionIfPossible(exception);
+
+ expectExceptionWithCauseMessage(translatedException, UncategorizedMongoDbException.class);
+ }
+
private void checkTranslatedMongoException(Class extends Exception> clazz, int code) {
DataAccessException translated = translator.translateExceptionIfPossible(new MongoException(code, ""));
diff --git a/src/main/antora/modules/ROOT/pages/mongodb/template-api.adoc b/src/main/antora/modules/ROOT/pages/mongodb/template-api.adoc
index ab34706180..f2a7a19bd6 100644
--- a/src/main/antora/modules/ROOT/pages/mongodb/template-api.adoc
+++ b/src/main/antora/modules/ROOT/pages/mongodb/template-api.adoc
@@ -31,7 +31,8 @@ The `execute` callbacks gives you a reference to either a `MongoCollection` or a
* ` T` *execute* `(String collectionName, CollectionCallback action)`: Runs the given `CollectionCallback` on the collection of the given name.
-* ` T` *execute* `(DbCallback action)`: Runs a DbCallback, translating any exceptions as necessary. Spring Data MongoDB provides support for the Aggregation Framework introduced to MongoDB in version 2.2.
+* ` T` *execute* `(DbCallback action)`: Runs a DbCallback, translating any exceptions as necessary.
+Spring Data MongoDB provides support for the Aggregation Framework introduced to MongoDB in version 2.2.
* ` T` *execute* `(String collectionName, DbCallback action)`: Runs a `DbCallback` on the collection of the given name translating any exceptions as necessary.
@@ -90,6 +91,7 @@ List all = template.query(SWCharacter.class) <1>
.matching(query(where("jedi").is(true))) <4>
.all();
----
+
<1> The type used to map fields used in the query to.
<2> The collection name to use if not defined on the domain type.
<3> Result type if not using the original domain type.
@@ -107,9 +109,8 @@ Flux all = template.query(SWCharacter.class)
----
======
-NOTE: Using projections allows `MongoTemplate` to optimize result mapping by limiting the actual response to fields required
-by the projection target type. This applies as long as the javadoc:org.springframework.data.mongodb.core.query.Query[] itself does not contain any field restriction and the
-target type is a closed interface or DTO projection.
+NOTE: Using projections allows `MongoTemplate` to optimize result mapping by limiting the actual response to fields required by the projection target type.
+This applies as long as the javadoc:org.springframework.data.mongodb.core.query.Query[] itself does not contain any field restriction and the target type is a closed interface or DTO projection.
WARNING: Projections must not be applied to xref:mongodb/mapping/document-references.adoc[DBRefs].
@@ -143,8 +144,8 @@ Flux> results = template.query(SWCharacter.class)
[[mongo-template.exception-translation]]
== Exception Translation
-The Spring framework provides exception translation for a wide variety of database and mapping technologies. T
-his has traditionally been for JDBC and JPA.
+The Spring framework provides exception translation for a wide variety of database and mapping technologies.
+This has traditionally been for JDBC and JPA.
The Spring support for MongoDB extends this feature to the MongoDB Database by providing an implementation of the `org.springframework.dao.support.PersistenceExceptionTranslator` interface.
The motivation behind mapping to Spring's link:{springDocsUrl}/data-access.html#dao-exceptions[consistent data access exception hierarchy] is that you are then able to write portable and descriptive exception handling code without resorting to coding against MongoDB error codes.
@@ -152,9 +153,25 @@ All of Spring's data access exceptions are inherited from the root `DataAccessEx
Note that not all exceptions thrown by the MongoDB driver inherit from the `MongoException` class.
The inner exception and message are preserved so that no information is lost.
-Some of the mappings performed by the `MongoExceptionTranslator` are `com.mongodb.Network to DataAccessResourceFailureException` and `MongoException` error codes 1003, 12001, 12010, 12011, and 12012 to `InvalidDataAccessApiUsageException`.
+Some of the mappings performed by the javadoc:org.springframework.data.mongodb.core.MongoExceptionTranslator[] are `com.mongodb.Network` to `DataAccessResourceFailureException` and `MongoException` error codes 1003, 12001, 12010, 12011, and 12012 to `InvalidDataAccessApiUsageException`.
Look into the implementation for more details on the mapping.
+Exception Translation can be configured by setting a customized javadoc:org.springframework.data.mongodb.core.MongoExceptionTranslator[] on your `MongoDatabaseFactory` or its reactive variant.
+You might also want to set the exception translator on the corresponding `MongoClientFactoryBean`.
+
+.Configuring `MongoExceptionTranslator`
+====
+[source,java]
+----
+ConnectionString uri = new ConnectionString("mongodb://username:password@localhost/database");
+SimpleMongoClientDatabaseFactory mongoDbFactory = new SimpleMongoClientDatabaseFactory(uri);
+mongoDbFactory.setExceptionTranslator(myCustomExceptionTranslator);
+----
+====
+
+A motivation to customize exception can be MongoDB's behavior during transactions where some failures (such as write conflicts) can become transient and where a retry could lead to a successful operation.
+In such a case, you could wrap exceptions with a specific MongoDB label and apply a different exception translation stragegy.
+
[[mongo-template.type-mapping]]
== Domain Type Mapping