1

In my domain model, I would like to associate an entity with other entities based on a discriminator column. For example, an address can be associated to a company or to a person. I would like to have only one table for addresses and selectively associate the address to a company or person based on the address type and an id which references the PK of either the company or person table.

Here are the table definitions:

CREATE TABLE IF NOT EXISTS `address` (
  `entityId` CHAR(26) NOT NULL,
  `addrLine1` VARCHAR(255) NOT NULL,
  `addrLine2` VARCHAR(255) DEFAULT NULL,
  `city` VARCHAR(255) NOT NULL,
  `state` CHAR(2) NOT NULL,
  `zip` CHAR(5) NOT NULL,
  `isBilling` CHAR(1) DEFAULT 'N',
  `type` ENUM('PERSON', 'COMPANY') NOT NULL,
  `addressableId` CHAR(26) NOT NULL,
  `hash` INT UNSIGNED NOT NULL,
  `created` DATETIME DEFAULT CURRENT_TIMESTAMP,
  `updated` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`entityId`),
  CONSTRAINT `un_address_hash` UNIQUE (`hash`, `addressableId`),
  INDEX `idx_address_addressableId` (`addressableId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

CREATE TABLE IF NOT EXISTS `company` (
  `entityId` CHAR(26) NOT NULL,
  `groupId` CHAR(26) NOT NULL,
  `name` VARCHAR(255) NOT NULL,
  `type` ENUM('USER', 'CLIENT', 'SUPPLIER', 'CLIENT_SUPPORT', 'USER_SUPPORT') NOT NULL,
  `created` DATETIME DEFAULT CURRENT_TIMESTAMP,
  `updated` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`entityId`),
  CONSTRAINT `fk_company_groupId` FOREIGN KEY (`groupId`) REFERENCES `user_group` (`entityId`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

CREATE TABLE IF NOT EXISTS `person` (
  `entityId` CHAR(26) NOT NULL,
  `firstName` VARCHAR(255) NOT NULL,
  `lastName` VARCHAR(255) DEFAULT NULL,
  `companyId` CHAR(26) NOT NULL,
  `created` DATETIME DEFAULT CURRENT_TIMESTAMP,
  `updated` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`entityId`),
  CONSTRAINT `fk_person_companyId` FOREIGN KEY (`companyId`) REFERENCES `company` (`entityId`) ON DELETE CASCADE,
  CONSTRAINT `un_person_name` UNIQUE (`companyId`, `firstName`, `lastName`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

When I translate this to hibernate, I use single-table inheritance on a base Address class with sub-classes CompanyAddress and PersonAddress like so:

@Getter
@Setter
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "type", discriminatorType = DiscriminatorType.STRING)
@Table(name = "address")
public class Address extends BaseEntity {
    
    @Column(nullable = false)
    private String addrLine1;
    
    private String addrLine2;
    
    @Column(nullable = false)
    private String city;
    
    @Column(length = 2, nullable = false)
    @Enumerated(EnumType.STRING)
    private State state;
    
    @Column(length = 5, nullable = false)
    private String zip;
}

@Getter
@Setter
@Entity
@DiscriminatorValue("COMPANY")
public class CompanyAddress extends Address {

    @ManyToOne
    @JoinColumn(name = "addressableId", referencedColumnName = "entityId")
    private Company company;
    
    @Convert(converter = BooleanConverter.class)
    private Boolean isBilling;
    
}

@Getter
@Setter
@Entity
@DiscriminatorValue("PERSON")
public class PersonAddress extends Address {

    @ManyToOne
    @JoinColumn(name = "addressableId") 
    private Person person;
    
}

As above, I would like to use the addressableId to either reference a Company or a Person based on the subclass (discriminator COMPANY or PERSON). In the Company class, I would like to maintain a list of all CompanyAddress entities:

@Getter
@Setter
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "type", discriminatorType = DiscriminatorType.STRING)
@Table(name = "company")
public class Company extends BaseEntity {
    
    @ManyToOne
    @JoinColumn(name = "groupId")
    private UserGroup userGroup;
    
    @Column(nullable = false)
    private String name;
    
    //////////
    
    @OneToMany(mappedBy = "company", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    private List<Person> people;
    
    @OneToMany(mappedBy = "company", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    private List<CompanyAddress> addresses;
    
    @OneToMany(mappedBy = "company", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    private List<CompanyPhone> phoneNumbers;
    
    @OneToMany(mappedBy = "company", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    private List<CompanyEmail> emailAddresses;
    
}

The problem I have, is that when trying to create a Company with cascading persists on the associated CompanyAddress entities, the primary key of the associated Company is not propagating to the CompanyAddress addressableId, and the save fails since addressableId cannot be null in the address table.

Here is the output I am seeing in the debug logs when saving a Company entity with one CompanyAddress in the companyAddresses list:

11:54:33.992 [http-nio-8080-exec-11] DEBUG o.h.internal.util.EntityPrinter - Listing entities:
11:54:33.993 [http-nio-8080-exec-11] DEBUG o.h.internal.util.EntityPrinter - io.billme.webservice.data.domain.ClientSupportCompany{addresses=[com.test.webservice.data.domain.CompanyAddress#BKSR47H45CTB8PJXT31PH3YN6W], emailAddresses=[], created=null, name=Some Company Name, entityId=XP8MRLNPDRMTFK4J846G8VF6VC, people=null, updated=null, userGroup=com.test.webservice.data.domain.UserGroup#PWANTBWLBJFZCS9RSBG1ZX88YR, phoneNumbers=[]}
11:54:33.993 [http-nio-8080-exec-11] DEBUG o.h.internal.util.EntityPrinter - io.billme.webservice.data.domain.CompanyAddress{zip=*****, isBilling=false, city=Anywhere, addrLine2=null, created=null, addrLine1=Main St, entityId=BKSR47H45CTB8PJXT31PH3YN6W, company=null, state=**, updated=null}
11:54:33.996 [http-nio-8080-exec-11] DEBUG org.hibernate.SQL - insert into company (name, groupId, type, entityId) values (?, ?, 'CLIENT_SUPPORT', ?)
11:54:34.002 [http-nio-8080-exec-11] DEBUG org.hibernate.SQL - insert into address (addrLine1, addrLine2, city, isBilling, state, zip, addressableId, type, entityId) values (?, ?, ?, ?, ?, ?, ?, 'COMPANY', ?)
11:54:34.020 [
-nio-8080-exec-11] DEBUG o.h.e.jdbc.spi.SqlExceptionHelper - could not execute statement [n/a]
java.sql.SQLIntegrityConstraintViolationException: Column 'addressableId' cannot be null

Is there anything on the Hibernate side I can do to fit this relational model? It seems that Hibernate should be able to propagate the entityId from the Company entity to the "owning" Address entity.

I have seen this post: https://discourse.hibernate.org/t/two-relationships-to-single-table-inheritance-sub-classes-failing-with-hibernate-5-3/6012 There is a slight difference in the setup, but after upgrading my Hibernate version to 6.1 there was no change.

cskes
  • 11
  • 2

0 Answers0