Using database constraints in Java

It is very common to define constraints on databases to ensure that only “valid” data is stored. Yet we do very little with these (powerful) database constraints in our Java applications. Often we duplicate the constraints in our application, for example by using JSR-303 constraint annotations.

If for some reason our database does throw a constraint violation, there is little we can do other than to log it and/or show a generic error page. Wouldn’t it be nice if we could properly integrate database constraints into our Java application? In this blog post I will demonstrate how you can by using the Java Repository Bridge “JaRB” .



Read the idea behind JaRB, if you want to know more about the background of the project.


Validation



Before storing content inside the database, our application often performs a “validation” step. This validation step is to ensure that the content being stored is valid inside the application domain. Also, as previously stated, we try to reject content that would cause a database constraint violation. As a result, we duplicate database constraint information inside our application, which is bad for obvious reasons.



Fortunately JDBC provides full access to our database metadata, meaning we could create a generic validation step that checks simple database constraints, such as column length and not-null restrictions. When using JaRB we can simply enable database constraint validation by adding a @DatabaseConstrained JSR-303 constraint annotation to our entity:




@Entity
@DatabaseConstrained
public class Person {
private String name;
}


Whenever a person instance is validated we will now check all bean properties on database constraints. So if the person.name column has a maximum length of 6 characters, we can expect the following:




Person jan = new Person();
jan.setName("Jan de Klusjesman");
validator.validate(jan);


“Name cannot be longer than 6 characters”



We no longer have to explicitly define a @Length(max=6) annotation on our name property to ensure the database constraint is met. Database constraints are checked automatically, resulting in cleaner and more flexible domain classes defining only domain constraints. Physical constraints, such as column length, are defined inside the database, away from our domain. By using @DatabaseConstrained we no longer have to use @NotNull @Length(max) @Digits on our entities.



Database constraint information is, by default, cached to optimize application performance and minimize database connectivity.



Violations



Some database constraints, e.g. unique keys or check constraints, are not checked during validation as they demand additional database connectivity. Rather than preventing the violations, we just let the violations occur and then act on them. Databases are a lot better at checking database constraints than the Java application, so why not leave this matter to the specialists?



Whenever a constraint violation is triggered inside the database, our JDBC driver will inform us with a runtime exception. Because there is no standard for violation exception messages, each JDBC driver provides a different message, making it extremely hard for our application to recognize what happened. Spring DAO already recognized this problem and made an attempt at creating more generic violation exceptions, but never fully solved the problem. Thus we created our own exception translator that is capable of translating exceptions, for any type of supported JDBC driver, into more descriptive constraint violation exceptions. Enabling us to do the following:




try {
personRepository.create(new Person("henk"));
} catch(UniqueKeyAlreadyExistsException e) {
if(e.getViolation().getColumnName().equals("name)) {
error("Person name '" + e.getViolation().getValue() + "' already exists!");
}
}


Rather than receiving some JDBC batch exception, we now receive a proper unique key exception, enabling us to act properly inside the application. Constraint violation exceptions also have full access to the violation, meaning we can access metadata such as column name and expression value.



But things get even better! Whenever our database constraint has a name, e.g. “uk_persons_name”, we can register a custom exception (factory), resulting into even more obvious exceptions:




try {
personRepository.create(new Person("henk"));
} catch(PersonNameAlreadyExists e) {
error("Person name '" + e.getPersonName() + "' already exists!");
}


Custom violation exceptions can be any type of runtime exception, meaning a custom exception hierarchy can be used without problems. By defining a constraint violation constructor in our exception class, the violation will automatically be provided during construction:




public class PersonNameAlreadyExists extends RuntimeException {
public PersonNameAlreadyExists(ConstraintViolation violation) {
super("Person name '" + violation.getValue() + "' already exists");
}
}


JDBC driver exceptions are translated into constraint violation exceptions using Aspect Oriented Programming (AOP). Our bean post processor wraps repository beans in a proxy that intercepts method invocations. Whenever some method invocation results in a runtime exception, our translator will attempt to translate the exception before rethrowing it. To experience constraint violation exceptions, simply define the translating post processor:




<bean class="org.jarb.violations.integration.ConstraintViolationExceptionTranslatingBeanPostProcessor">
<property name="translator">
<bean class="org.jarb.violations.integration.JpaConstraintViolationExceptionTranslatorFactoryBean">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
<!-- Custom exception classes, these are optional -->
<property name="exceptionClasses">
<map>
<entry key="uk_persons_name" value="com.myproject.domain.PersonNameAlreadyExists"/>
</map>
</property>
</bean>
</property>
</bean>


For more information you can visit the project website.

1 comment: