1

I'm exploring options for being able to store specific addresses (home, work etc.) for multiple entities, and having a single table that holds all addresses, probably with some kind of discriminator per record. The primary keys for all tables are UUIDs.

I'm using Spring Boot 2.3.6 and JPA/Hibernate.

Ideally I'd like to use named properties per entity, rather than holding a collection of entities as it will make DTO mapping and updates easier.

It's not an issue for me if there are entries in the shared Address table with all NULL values for each entity & property pair if no data is entered.

In pseudo code, I'd like to be able to define the entities as:

@Entity
class Person {
   private Address homeAddress;
   private Address workAddress;
}

@Entity
class Incident {
   private Address incidentLocation;
}

@Entity
class Address {
   private String street;
   private String zip;
}

I've researched using JPA options such as @Embeddable's and the options I have seen are to either a) have a single embeddable per entity (I want multiples) b) use @CollectionTable (I want specific named properties) or c) use @AttributeOverride which will mean repeated & renamed columns in the table for each property.

I've also looked at @JoinTable and @OneToMany but again this is geared towards using collections.

I get the feeling that @Embeddable is what I need, but need to be able to specify a discriminator for each property that uses this type (homeAddress, workAddress, incidentLocation) so that the data in the Address table follows the format

id        type      street          zip
=========================================
UUID-1    HOME      1 Main St       30002
UUID-1    WORK      10 North St     30005
UUID-2    INCIDENT  5 West Ave      30008   

As a bonus, I'd also like (if I could) to be able to create a JpaRepository<Address> that allows me to query/update the addresses independently of the parent entity.

With all the options available I wondered if anyone knew if there was a way to achieve what I want, or will I have to go down the collection route in order to achieve this? Thanks

Jens Schauder
  • 77,657
  • 34
  • 181
  • 348
Steve Davis
  • 167
  • 14
  • Could you please provide ER diagram of your schema. – SternK Dec 05 '20 at 13:07
  • It will be easier to achieve if you put the foreign keys in the parent tables, and let `Address` have their own ids. You can then use a simple unidirectional `@OneToOne`. As for the `type` column, you can either map it in `Address` as a regular property or introduce a `SINGLE_TABLE` inheritance hierarchy for `Address`es, with `type` being the discriminator. If that doesn't work for you, then I'm afraid you'll need to use collections (perhaps a map with `@MapKeyColumn` will make more sense). Also, if you need a repository, then you'll want `@OneToMany`, not `@ElementCollection` – crizzis Dec 05 '20 at 21:00
  • @crizzis - this looks like it may be a promising solution. I'll give it a try and report back. Thanks! – Steve Davis Dec 05 '20 at 21:23

3 Answers3

0

Just have a bunch of properties (homeAddress, workAddress ...) each referencing an Address as a one-to-one relationship and in the setter set the discriminator.

Jens Schauder
  • 77,657
  • 34
  • 181
  • 348
0

Thanks to for the assistance, I think a combination of crizzis and latterly Jens's suggestions led me to this JPA implementation.

@Data
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "address_type", discriminatorType = DiscriminatorType.STRING)
@Table(name = "address")
@TypeDef(name = UUID_CUSTOM_TYPE_NAME, typeClass = com.example.onetoone.domain.entity.UUIDCustomType.class, defaultForType = UUID.class)
public class AddressEntity
{
    @Id
    private UUID uuid;

    private String address1;

    private String city;

    private String zip;
}


@Data
@EqualsAndHashCode(callSuper = true)
@Entity
@DiscriminatorValue("HOME")
public class HomeAddressEntity extends AddressEntity
{
    @OneToOne(mappedBy = "homeAddress", fetch = FetchType.LAZY)
    private PersonEntity personHome;
}


@Data
@EqualsAndHashCode(callSuper = true)
@Entity
@DiscriminatorValue("WORK")
public class WorkAddressEntity extends AddressEntity
{
    @OneToOne(mappedBy = "workAddress", fetch = FetchType.LAZY)
    private PersonEntity personWork;
}



@Data
@EqualsAndHashCode(callSuper = true)
@Entity
@DiscriminatorValue("INCIDENT")
public class IncidentAddressEntity extends AddressEntity
{
    @OneToOne(mappedBy = "incidentAddress", fetch = FetchType.LAZY)
    private IncidentEntity incident;
}


@Data
@Entity
@Table(name = "person")
@TypeDef(name = UUIDCustomType.UUID_CUSTOM_TYPE_NAME, typeClass = com.example.onetoone.domain.entity.UUIDCustomType.class, defaultForType = UUID.class)
public class PersonEntity
{
    @Id
    private UUID uuid;

    private String name;

    @OneToOne(cascade = CascadeType.ALL)
    private HomeAddressEntity homeAddress;

    @OneToOne(cascade = CascadeType.ALL)
    private WorkAddressEntity workAddress;
}


@Data
@Entity
@Table(name = "incident")
@TypeDef(name = UUIDCustomType.UUID_CUSTOM_TYPE_NAME, typeClass = UUIDCustomType.class, defaultForType = UUID.class)
public class IncidentEntity
{
    @Id
    private UUID uuid;

    private String name;

    @OneToOne(cascade = CascadeType.ALL)
    private IncidentAddressEntity incidentAddress;
}

with the UUID type def defined as follows in case anyone also needs it

public class UUIDCustomType extends AbstractSingleColumnStandardBasicType<UUID> implements LiteralType<UUID>
{

    private static final long serialVersionUID = -540308541695243812L;

    public static final String UUID_CUSTOM_TYPE_NAME = "uuid-custom";

    public UUIDCustomType()
    {

        // https://stackoverflow.com/questions/42559938/hibernate-uuid-with-postgresql-and-sql-server
        super(VarcharTypeDescriptor.INSTANCE, UUIDTypeDescriptor.INSTANCE);

    }

    @Override
    public String getName()
    {

        return UUID_CUSTOM_TYPE_NAME;

    }

    @Override
    public String objectToSQLString(UUID value, Dialect dialect) throws Exception
    {

        return StringType.INSTANCE.objectToSQLString(value.toString(), dialect);

    }
}

This generates the following DDL in the MySQL database

CREATE TABLE `address` (
  `address_type` varchar(31) NOT NULL,
  `uuid` varchar(255) NOT NULL,
  `address1` varchar(255) DEFAULT NULL,
  `city` varchar(255) DEFAULT NULL,
  `zip` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`uuid`)
)

CREATE TABLE `person` (
  `uuid` varchar(255) NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  `home_address_uuid` varchar(255) DEFAULT NULL,
  `work_address_uuid` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`uuid`),
  KEY `FKoqa1ado547ntt2lc6ppx1lvr4` (`home_address_uuid`),
  KEY `FKjc3ayqtduyx0l342uu9ti32hl` (`work_address_uuid`)
)

CREATE TABLE `incident` (
  `uuid` varchar(255) NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  `incident_address_uuid` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`uuid`),
  KEY `FKosj0m7i6beq7ijwh68tjpfaa7` (`incident_address_uuid`)
)

alter table incident add constraint FKosj0m7i6beq7ijwh68tjpfaa7 foreign key (incident_address_uuid) references address (uuid)
alter table person add constraint FKoqa1ado547ntt2lc6ppx1lvr4 foreign key (home_address_uuid) references address (uuid)
alter table person add constraint FKjc3ayqtduyx0l342uu9ti32hl foreign key (work_address_uuid) references address (uuid)
Steve Davis
  • 167
  • 14
0

If Address has relation to many enitities, the best solution is use onedirect relation from any enitity to Address, as below:

    @Entity
class Person {
   @OneToOne
   private Address homeAddress;
   @OneToOne
   private Address workAddress;
}

@Entity
class Incident {
   @OneToOne
   private Address incidentLocation;
}

@Entity
class Address {
   private String street;
   private String zip;
}

you can use also @ManyToOne if this same address is used many times. For example homeAddress and workAddress is this same, and you want to no nullable field.

In this case entity Address doesn't know to which entity belong, but second side of relation (Person or Incident) knows which address is own. In tables Person and Incident will be column with id of address

Victor1125
  • 642
  • 5
  • 16