Search code examples
javahibernatehibernate-mapping

Hibernate: Mapping ManyToOne only to foreign key entities with particular column value


I think this is a bit weird, I am not sure if this is even possible, but my requirement is as follows:

I have two entities: Dataset and Organization. The Dataset entity has a many-to-one mapping to the Organization entity. Organizations are of two types: Customer and Partner. The business requirement is that only Partner type organizations can have datasets. So, is there any way to map the Dataset entity to Organization such that all foreign keys in Dataset only contain ids of Organization entities that are of type Partner?

The organization entity is defined as follows:

@Entity
@Table(name = "organization")
public class Organization {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;    
    private String type;
    private String name;
    private String address;
    private String status;
    private String subtype;
    
    @Column(name = "created_date")
    @Temporal(value =  TemporalType.TIMESTAMP)
    private Date createdDate;

    @OneToMany(fetch = FetchType.LAZY)
    @JoinColumn(name = "organization_id", insertable = false, updatable = false)
    private List<User> users;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumns({ @JoinColumn(name = "subtype", referencedColumnName = "name", insertable = false, updatable = false),
            @JoinColumn(name = "type", referencedColumnName = "organization_type", insertable = false, updatable = false) })
    private OrganizationSubType organizationSubType;

    // Getters and setters
.
.
.

Here, the type column will contain either PARTNER or CUSTOMER.

And here's the Dataset entity I am currently designing:

@Entity
@Table(name="datasets")
public class Dataset {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    
    @Column(name="partner_id", nullable = false)
    private long partnerId;
    
    @Column(nullable = false, unique = true)
    private String name;
    
    private String description;
    
    @Column(nullable = false)
    @Temporal(TemporalType.TIMESTAMP)
    private Date createdDate;
    
    @Column(nullable = false)
    @Enumerated(EnumType.STRING)
    private DatasetStatus datasetStatus;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="partner_id", referencedColumnName="id", insertable = false, updatable = false)
    private Organization partner;
    
    //Getters and setters

So, is there any way to set a constraint on the mapping in such a way that when a new Dataset entity is persisted, the organization id always belongs to an entity of type partner and not customer? Or do I have to separate the customer and partner organizations as separate entities?


Solution

  • There are several option how you can achieve it:

    1. By adding validation at business layer in the place when you build new Dataset object prior to persisting it. Such validation would check if given Organisation can be associated with created Dataset entity based on organisation type.

    2. By utilizing Bean Validation API & defining custom constraint validator. Such validator will be invoked by each change on the entity.

    Add hibernate-validator to the classpath

    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>5.3.4.Final</version>
    </dependency>
    

    Define annotation for new validation

    @Constraint(validatedBy = PartnerOrganisationValidator.class)
    @Target(FIELD)
    @Retention(RUNTIME)
    @Documented
    public @interface PartnerOrganisation {
        String message() default "Organisation should be of type PARTNER";
        Class<?>[] groups() default { };
        Class<? extends Payload>[] payload() default { };
    }
    

    Define contraint validator

    public class PartnerOrganisationValidator implements ContraintValidator<PartnerOrganisation, Organisation> {
        @Override
        public boolean isValid(Organisation organisation, ConstraintValidatorContext constraintValidatorContext) {
            return organisation == null || "PARTNER".equals(organisation.type);
        }
    }
    

    Register validator in hibernate validator by adding fully qualified name of your validator to META-INF/services/javax.validation.ConstraintValidator file. Last step is to use validator in your entity:

        @PartnerOrganisation
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name="partner_id", referencedColumnName="id", insertable = false, updatable = false)
        private Organization partner;
    
    1. By using SQL CHECK CONSTRAINT if database you uses supports it - just like in the example in https://stackoverflow.com/a/55379219/14231619

    2. By modeling PARTNER Organisation and CUSTOMER Organisation as separate entity classes.

    Since structure of Organisation for PARTNER and CUSTOMER is the same approach from option 4. seems overcomplicated for the situation. I would recommend going with option 1.