Sunday, March 15, 2009

Hibernate Unique validator with Seam



Have you ever wished , if your favorite framework provides some kind of functionality that validates unique constraints against your Database tables.

Not until now , most of us were catching exceptions and look for Sql error code for unique constraint or doing some kind of work around to handle unique constraint issues. Which is not so clean and nice looking.

Fortunately, with JBoss Seam and Hibernate validator we can handle unique constraint problem in a elegant way. And did i say that it will be Generic too. Yes it will work for all of your tables.

Lets see how Jboss Seam will handle Unique constraint problem and returns a nice looking message.

Suppose we have a table called Users with userName and some other property. Do i need to say userName should be unique.


CREATE TABLE Users(
id INT NOT NULL AUTO_INCREMENT
, userName VARCHAR(200) NOT NULL
, password VARCHAR(15) NOT NULL
....
//Some more properties
);

Before looking at User domain class , lets create an annotation called @Unique


@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ValidatorClass(UniqueValidator.class)
public @interface Unique {
String message() default " already exists";

String entityName();

String fieldName();
}

@Unique is a simple annotation with 2 properties
entityName - your mapped class
fieldName - property that should be unique for this entity.

And UniqueValidator class



@Name("UniqueValidator")
public class UniqueValidator implements Validator, PropertyConstraint {

//Entity for which validation is to be fired
private String targetEntity;

//Field for which validation is to be fired.
private String field;

public void initialize(Unique parameters) {
targetEntity = ((Unique)parameters).entityName();
field = ((Unique)parameters).fieldName();
}

public boolean isValid(Object value) {
EntityManager entityManager = (EntityManager) Component.getInstance("entityManager");
Query query = entityManager.createQuery("select t from " + targetEntity
+ " t where t." + field + " = :value");
query.setParameter("value", value);

try {
query.getSingleResult();
return false;
} catch (final NoResultException e) {
return true;
}
}

public void apply(Property arg0) {

}
}



Now we just have to apply this @Unique annotation to User class




public class User {

@Unique(entityName = "come.XXX.User", fieldName = "userName")
protected String userName;

protected String password;

............

}



Add <s:validateall> in your view

<s:validateall>
<h:outputtext value="User Name">

<ice:inputtext partialsubmit="true" id="ApplicationUser_userName" required="true" value="#{user.userName}">
</ice:inputtext>

<h:message styleclass="error errors" for="ApplicationUser_userName">
</h:message>
</h:outputtext>
</s:validateall>



That's it!! Now Seam will auto magically show you "already exist" message as you will tab out of user name text box. Thats Seam Magic.

16 comments:

Anonymous said...

excellent ... thanks for the tips

Anonymous said...

Thanks, It was very helpful!

Anonymous said...

It is a wonderful example, however, it is useless in an editable datagrid because all values of the column will be marked as existing.

Anonymous said...

great!

Anonymous said...

thx a lot, but hoe launch validator?

Don,t validate and don't show system.out.println that are inside methods initialize and isValid.

Sahacara said...
This comment has been removed by the author.
Sahacara said...

Is there some way I can do this without specifying the fieldName attribute with the annotation. In other words is it possible to get the attribute or method name which is annotated? For example the hibernate @Column annotation has a name attribute which defaults to the attribute name. Any ideas how this can be done?

Anonymous said...

Thanks! it helps me a lot~

Anonymous said...

doesn't work... isValid() flushes the session, which causes isValid() to be called again, which again flushes the session... so I get a StackOverflowError.

caru said...

"flushes the session"

Solution: go in the method that actually calls persist() on the entity manager or entity home. Before such call, change the flush mode on the entity manager: em.setFlushMode(javax.persistence.FlushModeType.COMMIT)

Anonymous said...

works great for inserts, also is getting fired wile updating the same record. It should get fired fired for only those records that have a different primary key/

Anonymous said...

To make this work you need to add idProvider="userHome" to the annotation, passing it the name of the Home component that manages the entity.
Then in UniqueValidator.initialize() also retrieve idProvider = ((Unique)parameters).idProvider();

In isValid() add to the query: " and id<>:id"
and set the id param to be the id of the entity that we're validating ie:
BaseHome home = (BaseHome) Component.getInstance(idProvider);
Long id= (Long)home.getId();

Now it will only fire "invalid" for records with a different primary key.

Rgds,
Mike Burton.

Quest for writing said...

Hi,
I read this article and it seems it will help a lot,but i am not sure what should be the necessary imports for class "UniqueValidator " and interface "Unique".
Plz. help me.

Anonymous said...

You need to write them as per the original example above ie:
@Name("UniqueValidator")
public class UniqueValidator implements Validator, PropertyConstraint {
...}

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ValidatorClass(UniqueValidator.class)
public @interface Unique {
....}

Unknown said...

thanks for this help

NameFILIP said...

<"flushes the session"
Solution: go in the method that actually calls persist() on the entity manager or entity home. Before such call, change the flush mode on the entity manager: em.setFlushMode(javax.persistence.FlushModeType.COMMIT)>

I've added em.setFlushMode(FlushModeType.COMMIT) to isValid() method, I think it is more suitable place