|

Hibernate Tips: Validate that only 1 of 2 associations is not null


Take your skills to the next level!

The Persistence Hub is the place to be for every Java developer. It gives you access to all my premium video courses, monthly Java Persistence News, monthly coding problems, and regular expert sessions.


Hibernate Tips is a series of posts in which I describe a quick and easy solution for common Hibernate questions. If you have a question for a future Hibernate Tip, please post a comment below.

Question:

In my domain model, a City can be part of a Province or a Country but not of both of them. I modeled this with 2 Many-To-One relationships, and I’m now looking for the best way to ensure that only one of them is set.

Solution:

With JPA and Hibernate, all attribute mappings are independent of each other. So, you can’t define 2 association mappings that exclude each other. But you can specify 2 many-to-one relationships and add a BeanValidation rule to your class.

Bean Validation is a Java EE specification that standardizes the validation of classes, properties, and method parameters. As I explained in one of the previous tutorials, JPA defines an integration with the BeanValidation specification.

If you add the Hibernate Validator project to your classpath, Hibernate will automatically trigger the validation before it persists or updates the entity. So, you can use it to automatically check that only one of the associations is set on your entity.

A standard entity mapping

Here you can see the City entity. It specifies 2 @ManyToOne associations. One to the Country and another one to the Province entity.

@Entity
@EitherOr
public class City {

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private Long id;

	@Version
	private int version;

	@ManyToOne
	private Province province;

	@ManyToOne
	private Country country;

	...
}

A custom validation

As you can see, there is nothing special about the two relationships. I only added the @EitherOr annotation in the second line of the code snippet. It’s a custom annotation which you can see in the following code snippet. It defines a validation constraint that Hibernate Validator will check before Hibernate ORM persists or updates the City entity.

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {EitherOrValidator.class})
public @interface EitherOr {
 
    String message() default "A city can only be linked to a country or a province.";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
 
}

The @Constraint validation is defined by the BeanValidation specification, and it references the class that implements the validation.

The implementation of that class is pretty simple. You only need to implement the ConstraintValidator interface. In this example, I want to ensure that a City is either associated with a Province or a Country. So, that’s what I check in the isValid method. It returns true if either the getProvince or the getCity method return null and the other method returns a value that’s not null.

public class EitherOrValidator implements ConstraintValidator<EitherOr, City>{

	@Override
	public void initialize(EitherOr arg0) { }

	@Override
	public boolean isValid(City city, ConstraintValidatorContext ctx) {
		return (city.getProvince() == null && city.getCountry() != null) 
				|| (city.getProvince() != null && city.getCountry() == null);
	}

	
}

Your business code

That’s all you need to do to implement the validation. If you now try to persist a City entity that’s associated to a Province and a Country, Hibernate Validator will throw a ConstraintViolationException which causes a transaction rollback.

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

Province province = new Province();
em.persist(province);

Country country = new Country();
em.persist(country);

City city = new City();
city.setProvince(province);
city.setCountry(country);

try {
	em.persist(city);
	em.getTransaction().commit();
} catch (RollbackException e) {
	Assert.assertTrue(e.getCause() instanceof ConstraintViolationException);
}
em.close();
17:46:49,300 DEBUG [org.hibernate.SQL] - select nextval ('hibernate_sequence')
17:46:49,331 DEBUG [org.hibernate.SQL] - select nextval ('hibernate_sequence')
17:46:49,333 DEBUG [org.hibernate.SQL] - select nextval ('hibernate_sequence')
17:46:49,367 DEBUG [org.hibernate.SQL] - insert into Province (version, id) values (?, ?)
17:46:49,378 DEBUG [org.hibernate.SQL] - insert into Country (version, id) values (?, ?)
17:46:49,422 ERROR [org.hibernate.internal.ExceptionMapperStandardImpl] - HHH000346: Error during managed flush [Validation failed for classes [org.thoughts.on.java.model.City] during persist time for groups [javax.validation.groups.Default, ]
List of constraint violations:[
	ConstraintViolationImpl{interpolatedMessage='A city can only be linked to a country or a province.', propertyPath=, rootBeanClass=class org.thoughts.on.java.model.City, messageTemplate='A city can only be linked to a country or a province.'}
]]
17:46:49,426 ERROR [org.thoughts.on.java.model.TestValidation] - javax.persistence.RollbackException: Error while committing the transaction

Learn more:

Here are 2 other articles that show you how to use JPA together with the BeanValidation specification:

Hibernate Tips Book

Get more recipes like this one in my new book Hibernate Tips: More than 70 solutions to common Hibernate problems.

It gives you more than 70 ready-to-use recipes for topics like basic and advanced mappings, logging, Java 8 support, caching, and statically and dynamically defined queries.

Get it now!

2 Comments

  1. I´m once again coming back to this article and it was/is really helpful, so thank you for that!

    (ps: there is minor mistake in sentence “I want to ensure that a City is either associated with a Province or a City.” where second City should be Country I suppose.)

    1. Avatar photo Thorben Janssen says:

      That’s right. Thanks!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.