Embeddable Inheritance with JPA and Hibernate

Are you struggling with performance issues in your Spring, Jakarta EE, or Java EE application?

What if there were a tool that could automatically detect what caused performance issues in your JPA and Hibernate data access layer?

Wouldn’t it be awesome to have such a tool to watch your application and prevent performance issues during development, long before they affect production systems?

Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, Micronaut, or Play Framework.

So, rather than fixing performance issues in your production system on a Saturday night, you are better off using Hypersistence Optimizer to help you prevent those issues so that you can spend your time on the things that you love!

Introduction

In this article, we are going to see how we can map embeddable inheritance when using JPA and Hibernate.

The feature described in this article is available since version 6.6 of Hibernate ORM, so if you haven’t yet upgraded, then you have one more reason to consider it.

Domain Model

Let’s consider we have the following Subscriber entity that has a collection of Subscription objects, which can be of the EmailSubscription or SmsSubscription type:

Embeddable Inheritance Subscriber entity

The Subscription is the base class of the EmailSubscription and SmsSubscription types and is mapped as follows:

@Embeddable
@DiscriminatorColumn(name = "subscription_type")
public class Subscription<T extends Subscription<T>> {
⠀
    @Column(name = "opt_in")
    private boolean optIn;
⠀
    public boolean isOptIn() {
        return optIn;
    }
⠀
    public T setOptIn(boolean optIn) {
        this.optIn = optIn;
        return (T) this;
    }
}

The @Embeddable JPA annotation tells Hibernate that the type does not have a distinct identity, and so it depends on a parent entity to propagate the entity state transitions that trigger the CRUD SQL statements.

The @DiscriminatorColumn annotation instructs Hibernate that the subscription_type column will be used to save the actual type of the object being persisted and determine the type to instantiate when reading the associated column values from the database table record.

The EmailSubscription extends the Subscription and declares that the discriminator value of email will be saved in the subscription_type column when persisting an EmailSubscription object.

The EmailSubscription type defines the extra emailAddress property that will need to be persisted as well:

@Embeddable
@DiscriminatorValue("email")
public class EmailSubscription extends Subscription<EmailSubscription> {
⠀
    @Column(name = "email_address")
    private String emailAddress;
⠀
    public String getEmailAddress() {
        return emailAddress;
    }
⠀
    public EmailSubscription setEmailAddress(String emailAddress) {
        this.emailAddress = emailAddress;
        return this;
    }
}

The SmsSubscription extends the Subscription and defines a discriminator value of sms, as well as an extra phoneNumber property:

@Embeddable
@DiscriminatorValue("sms")
public class SmsSubscription extends Subscription<SmsSubscription> {
⠀
    @Column(name = "phone_number")
    private Long phoneNumber;
⠀
    public Long getPhoneNumber() {
        return phoneNumber;
    }
⠀
    public SmsSubscription setPhoneNumber(Long phoneNumber) {
        this.phoneNumber = phoneNumber;
        return this;
    }
}

Because the Subscription objects are of @Embeddable type, the Subscriber entity uses the @ElementCollection to map the Set of Subscription objects:

@Entity(name = "Subscriber")
@Table(name = "subscriber")
public class Subscriber {
⠀
    @Id
    private Long id;
⠀
    @Column(name = "first_name")
    private String firstName;
⠀
    @Column(name = "last_name")
    private String lastName;
⠀
    @Temporal(TemporalType.TIMESTAMP)
    @CreationTimestamp
    @Column(name = "created_on")
    private Date createdOn;
⠀
    @ElementCollection
    @CollectionTable(
        name = "subscriptions", 
        joinColumns = @JoinColumn(name = "parent_id")
    )
    private Set<Subscription> subscriptions = new HashSet<>();
⠀
    //Getters and setters omitted for brevity
⠀
    public Subscriber addSubscription(Subscription subscription) {
        subscriptions.add(subscription);
        return this;
    }
}

Testing Time

When persisting the following Subscriber entity:

entityManager.persist(
    new Subscriber()
    .setId(1L)
    .setFirstName("Vlad")
        .setLastName("Mihalcea")
        .addSubscription(
            new EmailSubscription()
                .setOptIn(true)
                .setEmailAddress("vm@acme.com")
        )
        .addSubscription(
            new SmsSubscription()
                .setOptIn(true)
                .setPhoneNumber(123_456_7890L)
        )
);

Hibernate executes the following SQL INSERT statements:

INSERT INTO subscriber (
    created_on,
    first_name,
    last_name,
    id
)
VALUES (
    '2024-09-09 21:29:55.916',
    'Vlad',
    'Mihalcea',
    1
)

INSERT INTO subscriptions (
    parent_id,
    email_address,
    opt_in,
    phone_number,
    subscription_type
) 
VALUES(
    1,
    'vm@acme.com',
    true,
    null,
    'email'
)

INSERT INTO subscriptions (
    parent_id,
    email_address,
    opt_in,
    phone_number,
    subscription_type
)
VALUES (
    1,
    null,
    true,
    1234567890,
    'sms'
)

When fetching the Subscriber entity, we can see that the EmailSubscription and SmsSubscription are fetched accordingly:

Subscriber subscriber = entityManager.createQuery("""
    select s
    from Subscriber s
    left join fetch s.subscriptions
    where s.id =:id
    """, Subscriber.class)
.setParameter("id", 1L)
.getSingleResult();

assertEquals(2, subscriber.getSubscriptions().size());

Map<Class, List<Object>> subscriptionMap = subscriber
    .getSubscriptions()
    .stream()
    .collect(groupingBy(Object::getClass));

EmailSubscription emailSubscription = (EmailSubscription)
    subscriptionMap.get(EmailSubscription.class).get(0);
assertEquals(
    "vm@acme.com", 
    emailSubscription.getEmailAddress()
);

SmsSubscription smsSubscription = (SmsSubscription)
    subscriptionMap.get(SmsSubscription.class).get(0);
assertEquals(
    123_456_7890L, 
    smsSubscription.getPhoneNumber().longValue()
);

Awesome, right?

If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.

Seize the deal! 33% discount. Seize the deal! 33% discount. Seize the deal! 33% discount.

Conclusion

While JPA supports inheritance for entities, starting with the 6.6 version, you can also use inheritance for embeddable types.

This will definitely help you map various corner case scenarios that, otherwise, would be much more difficult to map.

Transactions and Concurrency Control eBook

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.