Retrieving the specific MongoDB key from DuplicateKeyException that was actually duplicated (Java/Spring)

StackOverflow https://stackoverflow.com/questions/23094415

Question

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.

Was it helpful?

Solution

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;
}

EDIT

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.

OTHER TIPS

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;
  }

}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top