41

I'd like to have a @Unique constraint with Bean Validation, but that is not provided by the standard. If I would use JPA's @UniqueConstraint I wouldn't have a unique validation and error reporting mechanism.

Is there a way to define @Unique as a Bean Validation constraint and combine it with JPA, such that JPA creates a column with an unique constraint and checks wheter a value is unique or not?

deamon
  • 89,107
  • 111
  • 320
  • 448

5 Answers5

56

Unless you acquire a lock on a whole table, it is basically not possible to check for unicity using a SQL query (any concurrent transaction could modify data after a manual check but before the commit of the ongoing transaction). In other words, it isn't possible to implement a valid unique verification at the Java level and thus to provide a validation implementation. The only reliable way to check for unicity is while committing the transaction.

The BV spec summarizes it like this:

Appendix D. Java Persistence 2.0 integration

Question: should we add @Unique that would map to @Column(unique=true)?

@Unique cannot be tested at the Java level reliably but could generate a database unique constraint generation. @Unique is not part of the BV spec today.

So while I agree that it would be nice to have unique (and non null) constraint violations wrapped in a Bean Validation exception, this is currently not the case.

References

Pascal Thivent
  • 562,542
  • 136
  • 1,062
  • 1,124
  • 13
    So what's the proper way of checking and telling the user that the entered value already exists? The exception (and its message) thrown on commit is not something that can be displayed to the user. People will implement their own @Unique anyway (eg. here http://lucasterdev.altervista.org/wordpress/2012/07/28/unique-constraint-validation-part-1/ ), even if there is small risk that a concurrent transaction modifies data between the check and commit. – Adam Dyga Dec 05 '13 at 10:14
  • 4
    This feels like it's being too idealistic. Plenty of other web frameworks (e.g. Rails, Django, etc..) include checks for uniqueness and they face this exact same problem. It's completely correct that it IS a problem, but it should still be an available option that comes with a warning. If other frameworks can manage it I don't think Java is special enough to think it's above them. – user2490003 Aug 28 '18 at 02:24
  • @user2490003 is correct about being idealistic; thank you for that comment. The unique constraint on the DBMS is the final backstop for introducing invalid data. It makes complete sense to put in the effort to check if a unique constraint is about to be violated ... and produce a coherent/human-readable validation message. For shame Java. This isn't the 90's. – Dan Dec 04 '19 at 00:04
5

More information on how to implement a @Unique and the problematic around it can be found here - http://community.jboss.org/wiki/AccessingtheHibernateSessionwithinaConstraintValidator

Hardy
  • 18,659
  • 3
  • 49
  • 65
3

Well you CAN do it, but it's not trivial. The problem is: the validator requires database access to perform some queries to check, if the value you want to insert is already there or not. And this can't be really done from the validator, as it doesn't have access to the sessionFactory/session. Of course you could instantiate it (session/sessionFactory) inside the validator, but it's not a good coding practice.

peterh
  • 11,875
  • 18
  • 85
  • 108
Mateusz Dymczyk
  • 14,969
  • 10
  • 59
  • 94
2

You can make a validator read the JPA annotations and apply it. Here is somewhat of an example using spring validators that can be used as an idea to expand on.

JPA JSR303 Spring Form Validation

You can also inject (@Inject or Spring's @Autowired) your session bean in a custom validator and the container should know how to wire it up. I only know this as a Spring example:

import javax.validation.ConstraintValidator;

public class MyConstraintValidator implements ConstraintValidator {

@Autowired //@Inject
private Foo aDependency;

...
}
Gus
  • 6,719
  • 6
  • 37
  • 58
dukethrash
  • 1,449
  • 4
  • 15
  • 25
0

You should try (insert or update), catch the exception and do some action. For example in a JSF backing bean :

try {
   dao.create(record);//or dao.modify(record)
   //add message success
} catch(EJBException e) {
   //look for origin of error (duplicate label, duplicate code, ...)
   var err = dao.isUnique(record);
   if(err == null) throw e;//other error
   String clientId = null;
   String message = null;
   switch(err) {
      case CODE:
        clientId = "client_id_of_input_code";
        message = "duplicate code";
        break;
      case LABEL:
        clientId = "client_id_of_input_label";
        message = "duplicate label";
        break;
      default:
        throw new AssertionError();//or something else
   }
   facesContext.addMessage(clientId, new FacesMessage(FacesMessage.SEVERITY_ERROR, message));
   facesContext.validationFailed();
}

Another option is to check before the insertion/modification. This can be time consuming and doesn't prevent the error to happen in the end.

grigouille
  • 511
  • 3
  • 14