Reputation: 965
I am working with Spring-Data/MongoDB and am properly catching duplicate keys upon a save/insert.
As an example, let's say I have a User being saved to a Collection. The User object is annotated with two @Indexed(unique=true)
(two unique keys). Let's say they are 'email' and 'username'. How do I retrieve which index was actually duplicated during the insert process.
The closest I get is when I execute this type of example code:
public boolean createNewUser() {
MongoTemplate operations = RepositoryFactory.getMongoOperationsInstance();
try {
log.debug("Saving new user to DB");
operations.save(this);
return true;
} catch (DuplicateKeyException dke) {
log.debug("User with same username or email found");
log.debug(operations.getDb().getLastError());
return false;
}
}
This prints the String:
{ "serverUsed" : "/127.0.0.1:27017" , "err" : "E11000 duplicate key error index: Collection.user.$username dup key: { : \"user\" }" , "code" : 11000 , "n" : 0 , "connectionId" : 17 , "ok" : 1.0}
Without silly String manipulation or a Json conversion, is there a way to extract the Collection.user.$username
via the Mongodriver API?
I have been searching unsuccessfully.
Upvotes: 4
Views: 3190
Reputation: 2269
If you run into this problem while using spring-data-rest
/spring-data-mongodb
, I wrote a @ControllerAdvice
class which uses an @ExceptionHandler
method to return errors in the same fashion as validation classes.
I didn't seem to have the classes used in the accepted answer, which is why I am posting this.
I am open to suggestions for better ways to solve this problem (within Spring Data) / implement this @ExceptionHandler
.
@ControllerAdvice
public class ControllerExceptionHandler {
@ExceptionHandler(DuplicateKeyException.class)
@ResponseStatus(value = HttpStatus.CONFLICT)
@ResponseBody
public Map<String, Object> handleDuplicateKeyException(DuplicateKeyException e) {
String entity = null;
String message = null;
String invalidValue = null;
String property = null;
String errorMessage = e.getMessage();
Pattern pattern = Pattern.compile("\\.(.*?) index: (.*?) dup key: \\{ : \\\\\"(.*?)\\\\\"");
Matcher matcher = pattern.matcher(errorMessage);
if (matcher.find()) {
entity = WordUtils.capitalize(matcher.group(1));
property = matcher.group(2);
invalidValue = matcher.group(3);
}
message = WordUtils.capitalize(property) + " must be unique";
Map<String, String> uniqueIndexViolation = new HashMap<>();
uniqueIndexViolation.put("entity", entity);
uniqueIndexViolation.put("message", message);
uniqueIndexViolation.put("invalidValue", invalidValue);
uniqueIndexViolation.put("property", property);
List<Object> errors = new ArrayList<Object>();
errors.add(uniqueIndexViolation);
Map<String, Object> responseBody = new HashMap<>();
responseBody.put("errors", errors);
return responseBody;
}
}
Upvotes: 1
Reputation: 19000
Not really, as the Mongo Java Driver already exposes the last error as a constructed String:
writeResult.getLastError().get("err")
returns something such as:
insertDocument :: caused by :: 11000 E11000 duplicate key error index: test.person.$username dup key: { : "joe" }
This is also true for the shell and every driver, I imagine.
A reasonable solution, I think, is to parse such duplicate key exception using a custom exception:
public class DetailedDuplicateKeyException extends DuplicateKeyException {
public DetailedDuplicateKeyException(String msg) {
// Instead of just calling super parse the message here.
super(msg);
}
}
... a custom exception translator:
public class DetailedDuplicateKeyExceptionTransaltor extends MongoExceptionTranslator {
@Override
public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
if (ex instanceof MongoException.DuplicateKey) {
return new DetailedDuplicateKeyException(ex.getMessage());
}
return super.translateExceptionIfPossible(ex);
}
}
... and setting the Spring configuration properly:
@Bean
public MongoFactoryBean mongo() {
MongoFactoryBean mongo = new MongoFactoryBean();
mongo.setExceptionTranslator(new DetailedDuplicateKeyExceptionTransaltor());
mongo.setHost("localhost");
return mongo;
}
After inspecting MongoTemplate
code (1.4.1.RELEASE), it seems that internally a SimpleMongoDbFactory
is used to retrieve a default MongoExceptionTranslator
, so the one created with MongoFactoryBean
is shadowed. Had missed that part.
The solution is to override SimpleMongoDbFactory
(forget about MongoFactoryBean
, it's useless in this context):
public class MySimpleMongoDbFactory extends SimpleMongoDbFactory {
PersistenceExceptionTranslator translator = new
DetailedDuplicateKeyExceptionTransaltor();
public MySimpleMongoDbFactory(Mongo mongo, String databaseName) {
super(mongo, databaseName);
}
@Override
public PersistenceExceptionTranslator getExceptionTranslator() {
return translator;
}
}
Now you can construct a template using the custom MongoDbFactory
:
template = new MongoTemplate
(new MySimpleMongoDbFactory(new MongoClient(), "test"));
Had tried, and this one works for me.
Upvotes: 3