What is the best way to implement mutually exclusive relationship in JPA?

Viewed 1232

I´m trying to build functional location hierarchy and I started by Country model/entity. One level down is Province entity with ManyToOne relationship to Country (bidirectional). Now I have City entity which is ManyToOne tied to Province (bidirectional) but this relationship is nullable. But now, if City-Province relationship is empty I need City-Country relationship (nullable as well). So at least one is required But if one of those two is already in place, other cannot be entered.

So basically, those two relationships in city (city-province, city-country) are mutually exclusive but at least one of them is required. I´m working with Java and JPA (Hibernate) and I haven´t found any special functionality for implementation of this. Is there any proven or "right" way how to do it?

2 Answers

You can't model mutually exclusive associations with JPA or Hibernate. But you can model 2 independent ones and add a BeanValidation constraint. JPA integrates the BeanValidation specification and automatically triggers the validation before persisting or updating and entity.

I explained it in greater details in one of my Hibernate Tips. Here's the short form.

You can model 2 associations on your City 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;

    ...
}

Please pay attention to the @EitherOr annotation in line 2. This is a custom constraint that I implemented based on the BeanValidation specification. The @Constraint annotation specifies the ConstraintValidator that will be triggered during the entity lifecycle transition.

@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 implementation of the ConstraintValidator is pretty easy. You need to implement the isValid method and check that either the province or the country attribute is 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);
    }


}

The least invasive way is to just model City accordingly by providing factory methods or constructors that only take one of the two (I usually prefer factory methods as they allow the client to read very nice: City.in(provice)):

public class City {

  public static City in(Province province) { … }

  public static City in(Country country) { … }

  …
}

Internally, you just null the other field and keep your JPA mappings, as shown by Thorben. Avoid setters for those fields, and you can never actually create instances of City with both values set. If you really do need setters, you want to reject invalid invocations right away, too. This can be unit tested, doesn't need any extra technology like JPA or BeanValidation.

Related