Wednesday, January 6, 2010

Bookmark and Share

Version 2 of the Java Persistence API (JPA) comes with an out-of-the-box integration of the Bean Validation API (BV). If a BV implementation is found in the runtime environment, any BV constraints placed at JPA entities will be validated whenever an entity is written to the database. In the following I'm going to show how the both APIs play together.

Project setup

Assuming you are working with Apache Maven, begin by adding the following repositories to your settings.xml or the pom.xml of your project:

1
2
3
4
5
6
7
8
9
10
...
<repository>
    <id>JBoss Repo</id>
    <url>http://repository.jboss.com/maven2</url>
</repository>
<repository>
    <id>EclipseLink Repo</id>
    <url>http://www.eclipse.org/downloads/download.php?r=1&nf=1&file=/rt/eclipselink/maven.repo</url>
</repository>   
...

In order to use the reference implementations of both APIs (EclipseLink resp. Hibernate Validator) add the following dependencies to you pom.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
...
<!-- JPA API and RI -->
<dependency>
    <groupId>javax.persistence</groupId>
    <artifactId>javax.persistence</artifactId>
    <version>2.0-SNAPSHOT</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>org.eclipse.persistence</groupId>
    <artifactId>eclipselink</artifactId>
    <version>2.0.0</version>
    <scope>runtime</scope>
</dependency>
...
<!-- Bean Validation API and RI -->
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>1.0.0.GA</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>4.0.2.GA</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-jdk14</artifactId>
    <version>1.5.6</version>
    <scope>runtime</scope>
</dependency>
...

Instead of EclipseLink you might also use Hibernate 3.5 (currently beta) as JPA provider:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
<dependency>
    <groupId>org.hibernate.java-persistence</groupId>
    <artifactId>jpa-api</artifactId>
    <version>2.0-cr-1</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-entitymanager</artifactId>
    <version>3.5.0-Beta-2</version>
    <scope>runtime</scope>
</dependency>
...

Finally we need a database to execute some unit tests against it. I recommend using Apache Derby as it provides an in-memory mode, which is perfectly suited for unit tests:

1
2
3
4
5
6
7
8
...
<dependency>
    <groupId>org.apache.derby</groupId>
    <artifactId>derby</artifactId>
    <version>10.5.3.0_1</version>
    <scope>test</scope>
</dependency>
...

The model class

Now let's create a simple model class. It's just a POJO with some annotations stemming from JPA (@Entity, @Id etc.) and some from Bean Validation (@NotNull, @Size):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Entity
@NamedQueries( { @NamedQuery(name = Customer.FIND_ALL_CUSTOMERS, query = "SELECT c FROM Customer c") })
public class Customer {

    public final static String FIND_ALL_CUSTOMERS = "findAllCustomers";

    @Id
    @GeneratedValue
    @NotNull
    private Long id;

    @NotNull
    @Size(min = 3, max = 80)
    private String name;

    private boolean archived;

    public Customer() {

    }

    public Customer(String name) {
        this.name = name;
        archived = false;
    }

    //getters and setters ...

}

Configuration

Next we have to setup the file persistence.xml which defines the persistence unit to be used for our module tests:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<persistence 
    xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
    version="2.0">

    <persistence-unit name="testPU" transaction-type="RESOURCE_LOCAL">

        <class>de.gmorling.jpa2.model.Customer</class>

        <properties>
            <property name="eclipselink.target-database" value="DERBY" />
            <property name="eclipselink.ddl-generation" value="create-tables" />
            <property name="javax.persistence.jdbc.driver" value="org.apache.derby.jdbc.EmbeddedDriver" />
            <property name="javax.persistence.jdbc.url" value="jdbc:derby:memory:testDB;create=true" />
            <property name="javax.persistence.jdbc.user" value="APP" />
            <property name="javax.persistence.jdbc.password" value="APP" />
        </properties>
    </persistence-unit>
</persistence>

JPA 2 standardizes the property names for driver, user etc. When working with EclipseLink therefore only two provider-specific properties for the target database and the DDL strategy are required.

Using Hibernate the properties would look like the following (at the time of writing this post the standard properties don't seem to be supported yet by Hibernate):

1
2
3
4
5
6
7
8
...
<property name="hibernate.dialect" value="org.hibernate.dialect.DerbyDialect" />
<property name="hibernate.hbm2ddl.auto" value="create" />
<property name="hibernate.connection.driver_class" value="org.apache.derby.jdbc.EmbeddedDriver" />
<property name="hibernate.connection.username" value="APP" />
<property name="hibernate.connection.password" value="APP" />
<property name="hibernate.connection.url" value="jdbc:derby:memory:testDB;create=true" />
...

Note that not a single line of configuration is required to enable the Bean Validation integration, it's working out of the box. If JPA detects a BV implementation on the classpath, it will use it by default to validate any BV constraints, whenever an entity is persisted or updated.

And action ...

To try that, let's write a simple unit test for our model class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class CustomerTest {

    private static EntityManagerFactory emf;

    private EntityManager em;

    @BeforeClass
    public static void createEntityManagerFactory() {
        emf = Persistence.createEntityManagerFactory("testPU");
    }

    @AfterClass
    public static void closeEntityManagerFactory() {
        emf.close();
    }

    @Before
    public void beginTransaction() {
        em = emf.createEntityManager();
        em.getTransaction().begin();
    }

    @After
    public void rollbackTransaction() {

        if (em.getTransaction().isActive())
            em.getTransaction().rollback();

        if (em.isOpen())
            em.close();
    }

    @Test
    public void nameTooShort() {

        try {
            Customer customer = new Customer("Bo");
            em.persist(customer);
            fail("Expected ConstraintViolationException wasn't thrown.");
        } 
        catch (ConstraintViolationException e) {
            assertEquals(1, e.getConstraintViolations().size());
            ConstraintViolation<?> violation = 
                e.getConstraintViolations().iterator().next();

            assertEquals("name", violation.getPropertyPath().toString());
            assertEquals(
                Size.class, 
                violation.getConstraintDescriptor().getAnnotation().annotationType());
        }
    }

    @Test
    public void validCustomer() {

        Customer customer = new Customer("Bob");
        em.persist(customer);

        Query query = em.createNamedQuery(Customer.FIND_ALL_CUSTOMERS);

        assertEquals(1, query.getResultList().size());
    }
}

The first four methods are just for setting up resp. closing down an entity manager factory for our persistence unit as well as the entity manager to be used.

In the test method nameTooShort() a ConstraintViolationException is raised, as the @Size constraint of the "name" attribute isn't fulfilled. The method validCustomer() on the other hand shows how a valid Customer instance is persisted and successfully retrieved later on.

Specifying validation groups

By default the constraint annotations of the Default validation group are validated upon inserting a new entity. The same holds true when an existing entity is updated, while no validation takes place by default if an entity is deleted.

If needed this behavior can be overridden by specifying the following properties in the persistence.xml (where values must be comma separated lists of fully-qualified class names of the groups to be validated):

1
2
3
javax.persistence.validation.group.pre-persist
javax.persistence.validation.group.pre-update
javax.persistence.validation.group.pre-remove

Let's assume for example that when a customer account is closed the concerned record from the Customer table is moved to some kind of archive storage. For all migrated records the "archived" flag should be set to true. Using a constraint annotation we would like to ensure now, that only archived Customer records are allowed to be deleted.

To do so we first define a new validation group by declaring an empty interface:

1
public interface DeletionAttributes {}

Now we add an @AssertTrue constraint to the "archived" attribute, which is part of this validation group:

1
2
3
4
5
6
7
public class Customer {

    ...
    @AssertTrue(groups = DeletionAttributes.class)
    private boolean archived;
    ...
}

Then we specify that constraints of the DeletionAttributes group shall be evaluated upon entity removal:

1
2
3
...
<property name="javax.persistence.validation.group.pre-remove" value="de.gmorling.jpa2.model.DeletionAttributes" />
...

As an unit test shows, a ConstraintViolationException will be raised now, if we try to delete a non-archived customer:

1
2
3
4
5
6
7
8
...
@Test(expected = ConstraintViolationException.class)
public void nonArchivedCustomerDeleted() {

    Customer customer = new Customer("Bob");
    em.persist(customer);
    em.remove(customer);
}...

But everything will be fine, if an archived customer gets deleted:

1
2
3
4
5
6
7
8
9
10
11
12
13
...
@Test
public void archivedCustomerDeleted() {

    Customer customer = new Customer("Bob");
    em.persist(customer);

    customer.setArchived(true);
    em.remove(customer);
    assertTrue(
        em.createNamedQuery(Customer.FIND_ALL_CUSTOMERS).getResultList().isEmpty());
}
...

Specifying the validation groups to be evaluated allows a very fine-grained configuration of the constraint checking process. But there might also be situations where you don't want any validation to take place at all. In that case the validation can be turned off completely by setting the validation mode to "NONE" within your persistence.xml:

1
2
3
4
5
6
...
<persistence-unit name="testPU" transaction-type="RESOURCE_LOCAL">
    <validation-mode>NONE</validation-mode> 
    ...
</persistence-unit>
...

Conclusion

As this post shows, JPA and BV API are integrated very well. Validating your entities before they are written to the database is a very powerful tool for improving data quality, as it ensures that all your persisted data adheres to the constraints of your object model.

I really recommend you to try it out yourself and of course I would be very happy on any feedback.

13 comments:

Anonymous said...

Great blogging on BV.
Can you use bean validation to validate session beans?
Can business logic be integrated into bean validation, e.g check account balance before doing a transaction?
Can use dependency injection in custom validators e.g for an entity manager to query a database?

Daniel

Unknown said...

Hi Daniel,

great to hear that you like the post :-)

* Generally the annotated fields/getter methods of any class can be validated, but there is no automatic validation of session beans as with entities, so you would have to do that on your own using the Validator API.

* You can implement arbitrary logic within custom validators, so you could call any existing business services upon validation.

* Using DI within custom validators can be realized by providing a custom ValidatorFactory.

Gunnar

Martin Kuhn said...

Is the source of your example downloadable?It would be really helpfull...
TIA
Martin

Unknown said...

Hi Martin,

I plan to put sources for some of my posts into my repo at github.org in the next time. What concerns this post, basically everything can be found in the listings. What part is missing for you?

Gunnar

Unknown said...

Martin,

I put the source code now into my github repo. You can find it here:


http://github.com/gunnarmorling/musings-of-a-programming-addict/tree/master/jpa2-bv-integration/


Gunnar

Anonymous said...

Helpful post, Thanks

Anonymous said...

Hello,

Thank you for your helpful article !
I have several things to ask if you dont mind :

1. Conditional validation inside my Entity
For example, in my entity, i have 2 fields, registerDate and registerReference, that can be validated if the field registered is false. How can i achieve that ?

2. Any suggestion on how to internationalized the error messages in the validations for the entities?

3. Is it possible to reuse the the validations for the entities for the JSF beans that actually delegates the method calls to the entities, so that i dont have to redefine the validations on the JSF beans ?

Unknown said...

Anonymous: A bit late, but maybe these answers are still helpful:

1. You can use a class-level constraint to perform a validation based on multiple fields of a type.

2. I18N is considered at the core of BV. The error texts are taken from a resource bundle ValidationMessages which you can provide in all languages you need.

3. Copying constraints from one model to another is not possible in a portable way atm. You might consider using your entities in JSF as well so you don't have the need for copying constraint definitions at all. Alternatively you might have a look at MyFaces ExtVal, I think they provide some sort support for this feature.

Piotr LA said...

Such descriptions are important. Each of your post brings a lot of good.
Destinations for you

aasik said...

yes, its really amazing to read, thanks for sharing this type of useful blogs. emaar beachfront

Ameer said...

its really very amazing to read this article, thanks for sharing. seo expert dubai

Perfumes Dubai said...

i found the best reading blogs on google, thanks for sharing. perfumes online

Aasik Creations said...

thank you for your useful article, i appreciate your wonderful work, this is very nice article to read, i bookmark yur website, do you need buy treadmill dubai