patternjavaMinor
Automagic (or not quite so) Domain Model Class Validation
Viewed 0 times
domainclassautomagicvalidationquitenotmodel
Problem
The Task
I was assigned a small task, concerning validation of Domain-Model classes.
The Validation for a String property of one of our Models was required to be unique across the whole Table. And as this already was halfway implemented after 20 minutes research, using a database constraint, it seemed fairly easy.
Obviously it wasn't, because what use does a validation have, when you're not able to show Error-Messages to your user in a feasible manner. The plan thusly changed slightly.
The only feasible approach to this seems to be a defensive select statement at persist time. This required me to use some additional features of JBoss, namely Seam and it's
So here's what I ended up with:
```
@ManagedBean
public class UniqueLocationValidator implements
ConstraintValidator {
@Inject
EntityManager entityManager;
private String message;
@Override
public void initialize(final UniqueLocation annotation) {
message = annotation.message();
}
@Override
public boolean isValid(final Location instance,
final ConstraintValidatorContext context) {
if (instance == null) {
// Recommended, instead use explicit @NotNull Annotation for
// validating non-nullable instances
return true;
}
final String checkedValue = instance.getLocationName();
final long id = instance.getId();
// must not return a result for name-equality on the same Id
String queryString = "SELECT * FROM Location WHERE locationName = :value AND id <> :id";
Query defensiveSelect = entityManager.createNativeQuery(queryString)
.setParameter("value", checkedValue).setParameter("id", id);
@SuppressWarnings("unchecked")
List results = defensiveSelect.getResultList();
I was assigned a small task, concerning validation of Domain-Model classes.
The Validation for a String property of one of our Models was required to be unique across the whole Table. And as this already was halfway implemented after 20 minutes research, using a database constraint, it seemed fairly easy.
Obviously it wasn't, because what use does a validation have, when you're not able to show Error-Messages to your user in a feasible manner. The plan thusly changed slightly.
The only feasible approach to this seems to be a defensive select statement at persist time. This required me to use some additional features of JBoss, namely Seam and it's
InjectingConstraintValidatorFactory. This allows me to access the current persistence context using an Injected EntityManager and run my defensive Select-Statement.So here's what I ended up with:
```
@ManagedBean
public class UniqueLocationValidator implements
ConstraintValidator {
@Inject
EntityManager entityManager;
private String message;
@Override
public void initialize(final UniqueLocation annotation) {
message = annotation.message();
}
@Override
public boolean isValid(final Location instance,
final ConstraintValidatorContext context) {
if (instance == null) {
// Recommended, instead use explicit @NotNull Annotation for
// validating non-nullable instances
return true;
}
final String checkedValue = instance.getLocationName();
final long id = instance.getId();
// must not return a result for name-equality on the same Id
String queryString = "SELECT * FROM Location WHERE locationName = :value AND id <> :id";
Query defensiveSelect = entityManager.createNativeQuery(queryString)
.setParameter("value", checkedValue).setParameter("id", id);
@SuppressWarnings("unchecked")
List results = defensiveSelect.getResultList();
Solution
-
Querying
-
As the value of
-
Renaming
-
Single Responsible Principle is violated as you are querying a database and constructing a
-
Introduce a
-
Inroduce a method for constructing the
So the
Querying
"SELECT * FROM Location is a little over the head. You don't need to get all columns.-
As the value of
queryString doesn't change, you should make it final -
Renaming
message to constraintValidationMessage for understandability regarding new staff -
Single Responsible Principle is violated as you are querying a database and constructing a
ConstraintViolation for the context. -
Introduce a
duplicateLocationExists() method which takes a Location as parameter and just returns a Boolean. The results object is only needed to check if it is empty, which should be returned by the duplicateLocationExists() method. private boolean duplicateLocationExists(final Location location){
if (location == null) {
// Recommended, instead use explicit @NotNull Annotation for
// validating non-nullable instances
return false;
}
final String checkedValue = location.getLocationName();
final long id = location.getId();
final String queryString = "SELECT id FROM Location WHERE locationName = :value AND id <> :id";
Query defensiveSelect = entityManager.createNativeQuery(queryString)
.setParameter("value", checkedValue).setParameter("id", id);
return !defensiveSelect.getResultList().IsEmpty();
}-
Inroduce a method for constructing the
ConstraintViolation for the context private void createConstraintViolations(final ConstraintValidatorContext context){
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(message)
.addNode("locationName").addConstraintViolation();
}So the
isValid() method will become @Override
public boolean isValid(final Location instance,
final ConstraintValidatorContext context) {
if (duplicateLocationExists(instance)) {
createConstraintViolations(context);
return false;
} else {
return true;
}
}Code Snippets
private boolean duplicateLocationExists(final Location location){
if (location == null) {
// Recommended, instead use explicit @NotNull Annotation for
// validating non-nullable instances
return false;
}
final String checkedValue = location.getLocationName();
final long id = location.getId();
final String queryString = "SELECT id FROM Location WHERE locationName = :value AND id <> :id";
Query defensiveSelect = entityManager.createNativeQuery(queryString)
.setParameter("value", checkedValue).setParameter("id", id);
return !defensiveSelect.getResultList().IsEmpty();
}private void createConstraintViolations(final ConstraintValidatorContext context){
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(message)
.addNode("locationName").addConstraintViolation();
}@Override
public boolean isValid(final Location instance,
final ConstraintValidatorContext context) {
if (duplicateLocationExists(instance)) {
createConstraintViolations(context);
return false;
} else {
return true;
}
}Context
StackExchange Code Review Q#64852, answer score: 3
Revisions (0)
No revisions yet.