Skip to content

Commit 84e7e51

Browse files
Update exception translation for bulk writes to multiple collections.
This commit makes sure to translate ClientBulkWriteExceptions to BulkOperationException. It also includes some nullability fixes and updates the documentation. See: #5087
1 parent 1a003db commit 84e7e51

22 files changed

Lines changed: 217 additions & 67 deletions

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/BulkOperationException.java

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,35 @@
1616
package org.springframework.data.mongodb;
1717

1818
import java.util.List;
19+
import java.util.Optional;
1920

2021
import org.jspecify.annotations.Nullable;
2122
import org.springframework.dao.DataAccessException;
23+
import org.springframework.data.util.Lazy;
24+
import org.springframework.util.NumberUtils;
2225

26+
import com.mongodb.ClientBulkWriteException;
2327
import com.mongodb.MongoBulkWriteException;
2428
import com.mongodb.bulk.BulkWriteError;
29+
import com.mongodb.bulk.BulkWriteInsert;
2530
import com.mongodb.bulk.BulkWriteResult;
31+
import com.mongodb.bulk.BulkWriteUpsert;
32+
import com.mongodb.client.model.bulk.ClientBulkWriteResult;
2633

2734
/**
2835
* Is thrown when errors occur during bulk operations.
2936
*
3037
* @author Tobias Trelle
3138
* @author Oliver Gierke
39+
* @author Christoph Strobl
3240
* @since 1.9
3341
*/
3442
public class BulkOperationException extends DataAccessException {
3543

3644
private static final long serialVersionUID = 73929601661154421L;
3745

38-
private final List<BulkWriteError> errors;
39-
private final BulkWriteResult result;
46+
private final Lazy<List<BulkWriteError>> errors;
47+
private final Lazy<BulkWriteResult> result;
4048

4149
/**
4250
* Creates a new {@link BulkOperationException} with the given message and source {@link MongoBulkWriteException}.
@@ -48,15 +56,78 @@ public BulkOperationException(@Nullable String message, MongoBulkWriteException
4856

4957
super(message, source);
5058

51-
this.errors = source.getWriteErrors();
52-
this.result = source.getWriteResult();
59+
this.errors = Lazy.of(source.getWriteErrors());
60+
this.result = Lazy.of(source.getWriteResult());
61+
}
62+
63+
/**
64+
* @param message
65+
* @param source
66+
*/
67+
public BulkOperationException(@Nullable String message, ClientBulkWriteException source) {
68+
super(message, source);
69+
70+
this.errors = Lazy.of(() -> source.getWriteErrors().values().stream()
71+
.map(error -> new BulkWriteError(error.getCode(), error.getMessage(), error.getDetails(), -1)).toList());
72+
this.result = Lazy.of(() -> convertToBulkWriteResult(source.getPartialResult()));
5373
}
5474

5575
public List<BulkWriteError> getErrors() {
56-
return errors;
76+
return errors.get();
5777
}
5878

5979
public BulkWriteResult getResult() {
60-
return result;
80+
return result.get();
6181
}
82+
83+
private static BulkWriteResult convertToBulkWriteResult(Optional<ClientBulkWriteResult> source) {
84+
85+
if (source.isEmpty()) {
86+
return BulkWriteResult.unacknowledged();
87+
}
88+
89+
ClientBulkWriteResult clientBulkWriteResult = source.get();
90+
if (!clientBulkWriteResult.isAcknowledged()) {
91+
return BulkWriteResult.unacknowledged();
92+
}
93+
94+
return new BulkWriteResult() {
95+
96+
@Override
97+
public boolean wasAcknowledged() {
98+
return true;
99+
}
100+
101+
@Override
102+
public int getInsertedCount() {
103+
return NumberUtils.convertNumberToTargetClass(clientBulkWriteResult.getInsertedCount(), Integer.class);
104+
}
105+
106+
@Override
107+
public int getMatchedCount() {
108+
return NumberUtils.convertNumberToTargetClass(clientBulkWriteResult.getMatchedCount(), Integer.class);
109+
}
110+
111+
@Override
112+
public int getDeletedCount() {
113+
return NumberUtils.convertNumberToTargetClass(clientBulkWriteResult.getDeletedCount(), Integer.class);
114+
}
115+
116+
@Override
117+
public int getModifiedCount() {
118+
return NumberUtils.convertNumberToTargetClass(clientBulkWriteResult.getModifiedCount(), Integer.class);
119+
}
120+
121+
@Override
122+
public List<BulkWriteInsert> getInserts() {
123+
throw new UnsupportedOperationException();
124+
}
125+
126+
@Override
127+
public List<BulkWriteUpsert> getUpserts() {
128+
throw new UnsupportedOperationException();
129+
}
130+
};
131+
}
132+
62133
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SessionAwareMethodInterceptor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,9 @@ public <T> SessionAwareMethodInterceptor(ClientSession session, T target, Class<
9898
this.sessionType = sessionType;
9999
}
100100

101-
Class<?> targetType(@Nullable Class<?> targetType) {
101+
Class<?> targetType(Class<?> targetType) {
102102

103-
if(ClassUtils.isAssignable(clientType, targetType)) {
103+
if (ClassUtils.isAssignable(clientType, targetType)) {
104104
return clientType;
105105
}
106106
return ClassUtils.isAssignable(databaseType, targetType) ? databaseType : collectionType;

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public BulkWriteResult write(String defaultDatabase, Bulk bulk, BulkWriteOptions
6767
return writeToMultipleCollections(defaultDatabase, bulk, options);
6868
}
6969

70+
@SuppressWarnings("NullAway")
7071
private BulkWriteResult writeToSingleCollection(String defaultDatabase, Bulk bulk,
7172
BulkWriteOptions options, TypedNamespace namespace) {
7273

@@ -96,6 +97,7 @@ private BulkWriteResult writeToSingleCollection(String defaultDatabase, Bulk bul
9697
}
9798
}
9899

100+
@SuppressWarnings("NullAway")
99101
private BulkWriteResult writeToMultipleCollections(String defaultDatabase, Bulk bulk,
100102
BulkWriteOptions options) {
101103

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2026 the original author or authors.
2+
* Copyright 2026-present the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -43,6 +43,7 @@
4343
* Base class for {@link BulkWriter} and {@link ReactiveBulkWriter}.
4444
*
4545
* @author Mark Paluch
46+
* @author Christoph Strobl
4647
* @since 5.1
4748
*/
4849
abstract class BulkWriterSupport {
@@ -81,13 +82,13 @@ MongoPersistentEntity<?> getPersistentEntity(BulkOperationContext context) {
8182
BulkOperationContext.TypedNamespace namespace = context.namespace();
8283

8384
if (namespace.type() != null) {
84-
return mappingContext.getRequiredPersistentEntity(namespace.type());
85+
return mappingContext.getPersistentEntity(namespace.type());
8586
}
8687

8788
if (namespace.hasCollectionName()) {
8889
CollectionName collectionName = namespace.getRequiredCollectionName();
8990
if (collectionName.getEntityClass() != Object.class) {
90-
return mappingContext.getRequiredPersistentEntity(collectionName.getEntityClass());
91+
return mappingContext.getPersistentEntity(collectionName.getEntityClass());
9192
}
9293
}
9394

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@
9292
* @see MongoTemplate
9393
* @see ReactiveMongoTemplate
9494
*/
95-
public class EntityOperations {
95+
class EntityOperations {
9696

9797
private static final String ID_FIELD = FieldName.ID.name();
9898

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.util.Set;
1919

20+
import com.mongodb.ClientBulkWriteException;
2021
import org.bson.BsonInvalidOperationException;
2122
import org.jspecify.annotations.Nullable;
2223
import org.springframework.dao.DataAccessException;
@@ -27,6 +28,7 @@
2728
import org.springframework.dao.InvalidDataAccessResourceUsageException;
2829
import org.springframework.dao.PermissionDeniedDataAccessException;
2930
import org.springframework.dao.support.PersistenceExceptionTranslator;
31+
import org.springframework.data.mongodb.BulkOperationException;
3032
import org.springframework.data.mongodb.ClientSessionException;
3133
import org.springframework.data.mongodb.TransientClientSessionException;
3234
import org.springframework.data.mongodb.UncategorizedMongoDbException;
@@ -63,7 +65,7 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
6365
private static final Set<String> RESOURCE_USAGE_EXCEPTIONS = Set.of("MongoInternalException");
6466

6567
private static final Set<String> DATA_INTEGRITY_EXCEPTIONS = Set.of("WriteConcernException", "MongoWriteException",
66-
"MongoBulkWriteException");
68+
"MongoBulkWriteException", "ClientBulkWriteException");
6769

6870
private static final Set<String> SECURITY_EXCEPTIONS = Set.of("MongoCryptException");
6971

@@ -112,6 +114,9 @@ DataAccessException doTranslateException(RuntimeException ex) {
112114
return new DuplicateKeyException(ex.getMessage(), ex);
113115
}
114116
}
117+
return new BulkOperationException(ex.getMessage(), bulkException);
118+
} else if (ex instanceof ClientBulkWriteException clientBulkWriteException) {
119+
return new BulkOperationException(ex.getMessage(), clientBulkWriteException);
115120
}
116121
}
117122

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,12 +172,14 @@ public interface MongoOperations extends FluentMongoOperations {
172172
<T> T execute(String collectionName, CollectionCallback<T> action);
173173

174174
/**
175-
* Executes the given {@link Bulk} to perform insert, update, and delete operations on multiple collections (requires
176-
* MongoDB 8.0+).
175+
* Executes the given {@link Bulk} to perform insert, update, and delete operations.
176+
* <p>
177+
* <strong>NOTE:</strong> targeting multiple collections requires MongoDB 8.0+.
177178
*
178179
* @param bulk the {@link Bulk} to write.
179180
* @param options additional options applied to the execution.
180-
* @return never {@literal null}.
181+
* @return the result of the bulk write operation. Never {@literal null}.
182+
* @since 5.1
181183
*/
182184
BulkWriteResult bulkWrite(Bulk bulk, BulkWriteOptions options);
183185

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ public void setApplicationContext(ApplicationContext applicationContext) throws
376376
setEntityCallbacks(EntityCallbacks.create(applicationContext));
377377
}
378378

379-
if (mappingContext instanceof ApplicationEventPublisherAware applicationEventPublisherAware) {
379+
if (eventPublisher != null && mappingContext instanceof ApplicationEventPublisherAware applicationEventPublisherAware) {
380380
applicationEventPublisherAware.setApplicationEventPublisher(eventPublisher);
381381
}
382382

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
* @author Mark Paluch
3232
* @since 2.1
3333
*/
34-
public class PropertyOperations {
34+
class PropertyOperations {
3535

3636
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
3737

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
* @author Florian Lüdiger
8282
* @since 3.0
8383
*/
84-
public class QueryOperations {
84+
class QueryOperations {
8585

8686
private final QueryMapper queryMapper;
8787
private final UpdateMapper updateMapper;

0 commit comments

Comments
 (0)