4

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.

1

2 Answers 2

3

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.

Sign up to request clarification or add additional context in comments.

3 Comments

Hey, thank you for your reply. I have not had use for Exception Translation until now, so I am rather unfamiliar with it. However, I have been doing enough reading and am still unable to get the Translator to actually get invoked. I have followed the steps outlined above, any thing that would be missing outside the scope of your response?
Thank you! That will do just fine. So what exactly is the difference between MongoDbFactory/SimpleMongoDbFactory and MongoFactoryBean? I have used them both to the same end yet don't understand the difference. Moreover, if you have time, do you think you could explain what you mean by being "shadowed"?
Well for the difference, you can consult the reference or the API documentation. "shadowed" was a bad choice of words. I didn't mean anything to do with variable shadowing. What I meant is that the exception translator configured with the MongoFactoryBean is never propagated to be used with the template. MongoFactoryBean implements PersistenceExceptionTranslator, but since it's just a factory for creating Mongo beans, I can't see the reason behind it.
1

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

}

2 Comments

Where is the WordUtils.capitalize method ?
It was in org.apache.commons.lang3.text.WordUtils at the time of this answer, but now appears to be in a different dependency. See stackoverflow.com/a/44599958/2464657.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.