The Best Mapping for Shared Technical Attributes With Hibernate



Get access to all my video courses, 2 monthly Q&A calls, monthly coding challenges, a community of like-minded developers, and regular expert sessions.

Join the Persistence Hub!


Most domain models have a few technical attributes shared by most entity classes. Typical examples are the version attribute and the timestamp or user who performed the last update or persisted an entity. In these situations, many developers ask themselves what’s the best way to model these attributes. Oleg did the same recently in the comments here on the blog, and I will explain the 2 most popular options in this article.

I prepared the following table model to show you the different mapping options. The chessgame and the chesstournament table both contain the columns version, lastModifiedDateTime and lastModifiedUser, which are typical examples of shared technical columns.

At first glance, the mapping as a @MappedSuperclass and an @Embeddable seem to be a good option. But both have their downsides, as I will show you in the following sections.

@MappedSuperclass Mapping

The @MappedSuperclass is one of JPA’s inheritance mapping strategies. It tells your persistence provider to include the mapping information of the mapped superclass in all subclasses that are mapped as entities. But the superclass itself doesn’t become an entity.

I explain this mapping in more detail in the post Inheritance Strategies with JPA and Hibernate – The Complete Guide here on the blog and in the Inheritance Mapping lecture in the Persistence Hub.

Here you can see a @MappedSuperclass that defines the attributes id, version, lastModifiedDateTime, and lastModifiedUser.

@MappedSuperclass
public class MyAbstractEntity {
    
    @Transient
    Logger  log = Logger.getLogger(this.getClass().getName());

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

    @Version
    protected int version;

    @UpdateTimestamp
    protected LocalDateTime lastModifiedDateTime;

    protected String lastModifiedUser;
	
	...
	
	@PrePersist
    @PreUpdate
    private void setLastModifiedUser() {
        log.info("Set lastModifiedUser");
        this.lastModifiedUser = "Thorben";
    }
}

FoI use a typical primary key mapping for the id attribute. It tells Hibernate to use a database sequence to generate unique primary key values.

I annotated the version attribute with a @Version annotation. That tells Hibernate to use this attribute for its optimistic locking algorithmto detect concurrent modifications.

The @UpdateTimestamp annotation on the lastModifiedDateTime attribute tells Hibernate to set this timestamp when flushing any entity changes to the database. This is a proprietary and very comfortable way to track the timestamp of the last modification.

And I annotated the setLastModifiedUser method with the lifecycle callback annotations @PrePersist and @PreUpdate. They tell Hibernate to call this method before persisting or updating an entity object. This enables me to set and persist the lastModifiedUser attribute.

The ChessTournament class extends the MyAbstractEntity and inherits its attributes and their mapping definition.

@Entity
public class ChessTournament extends MyAbstractEntity {

    private String name;

    private LocalDate startDate;

    private LocalDate endDate;

    @Version
    private int version;

    @OneToMany
    private Set<ChessGame> games = new HashSet<>();
	
	...
}

Let’s use this entity class in a simple test case that persists a new ChessTournament entity.

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

ChessTournament t = new ChessTournament();
t.setName("World Chess Championship 2021");
t.setStartDate(LocalDate.of(2021, 11, 24));
t.setEndDate(LocalDate.of(2021, 12, 16));
em.persist(t);

em.flush();

assertThat(t.getLastModifiedDateTime()).isNotNull();
assertThat(t.getLastModifiedUser()).isNotNull();

em.getTransaction().commit();
em.close();

As you can see in the log output, the mapping works as expected. Hibernate uses all attribute mappings defined by the @MappedSuperclass when persisting the ChessTournament entity object.

14:41:37,080 INFO  [com.thorben.janssen.TestMapping] - ==== testMappedSuperclass ====
Nov. 30, 2021 2:41:37 PM com.thorben.janssen.model.MyAbstractEntity setLastModifiedUser
INFO: Set lastModifiedUser
14:41:37,143 DEBUG [org.hibernate.SQL] - 
    select
        nextval('tournament_seq')
14:41:37,149 DEBUG [org.hibernate.SQL] - 
    select
        nextval('tournament_seq')
14:41:37,179 DEBUG [org.hibernate.SQL] - 
    insert 
    into
        ChessTournament
        (endDate, lastModifiedDateTime, lastModifiedUser, name, startDate, version, id) 
    values
        (?, ?, ?, ?, ?, ?, ?)

Strengths and Weaknesses of the @MappedSuperclass mapping

As you saw in the previous example, the @MappedSuperclass mapping provides a very natural approach to define the mapping of shared attributes. It supports all mapping annotations, and you can even model attributes with specific semantics, e.g., primary keys and version attributes, on your superclass. As you will see in the next section, that’s not the case if you’re using an @Embeddable.

But I also want to point out that this mapping approach feels wrong when looking at it from a modeling perspective.

The ChessTournament isn’t an AbstractEntity. It only shares attributes defined by that class. When you analyze your application’s domain, something like an AbstractEntity will not come up in the analysis process because it doesn’t exist in the real world.

It’s also rather unlikely that we will actively use the AbstractEntity in the business code to implement any part of our business logic.

The only reason to introduce AbstractEntity as a superclass is to define the mapping of all shared technical attributes in 1 place. Based on object-oriented design principles, you should better use composition instead of inheritance to achieve this.

@Embeddable Mapping

The @Embeddable mapping applies the concept of composition to the domain model and might be considered as the better approach. But it introduces some restrictions to your mapping definitions.

The embeddable object itself has no identity in your persistence context. All its attributes and mappings become part of the entity and get mapped to the entity’s database table. You can learn more about this mapping in the lecture on @Embeddables in the Persistence Hub.

Here you can see an @Embeddable mapping based on this article’s example. In contrast to the MyAbstractEntity, the MetaData class doesn’t define the id and version attributes. The simple reason for that is that Hibernate doesn’t allow you to define these attributes on an @Embeddable. You need to define the primary key, and the version attributes on the entity class itself.

@Embeddable
public class MetaData {
    
    @Transient
    Logger  log = Logger.getLogger(this.getClass().getName());
    
    private LocalDateTime lastModifiedDateTime;

    private String lastModifiedUser;

	...

    @PrePersist
    @PreUpdate
    private void setLastModified() {
        log.info("Set lastModifiedUser and lastModifiedDateTime");
        this.lastModifiedUser = "Thorben";
		this.lastModifiedDateTime = LocalDateTime.now();
    }
}

I also don’t annotate the lastModifiedDateTime attribute with an @UpdateTimestamp annotation. Because if I do that, Hibernate 5 and 6 throw a NotYetImplementedException during deployment.

jakarta.persistence.PersistenceException:[PersistenceUnit:my-persistence-unit] Unable to build Hibernate SessionFactory
...
Caused by: org.hibernate.cfg.NotYetImplementedException: Still need to wire in composite in-memory value generation

But instead of using the @UpdateTimestamp annotation, you can set the lastModifiedDateTime attribute in the life cycle callback method setLastModified.

After you modeled the @Embeddable, you can use it as an attribute type in your entity class. Here you can see the ChessGame entity. Its metaData attribute is of type MetaData, and I annotated it with an @Embedded annotation. That tells Hibernate to include all attributes defined by the @Embeddable into the ChessGame entity.

@Entity
public class ChessGame {
    
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

    private LocalDate date;

    private int round;

    @ManyToOne
    private ChessTournament chessTournament;

    @Embedded
    private MetaData metaData;
	
    @Version
    private int version;

    ...
}

Let’s use this mapping in a simple test case that persists a new ChessGame entity object.

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

ChessGame g = new ChessGame();
g.setDate(LocalDate.of(2021, 11, 26));
g.setRound(1);
g.setMetaData(new MetaData());
em.persist(g);

assertThat(g.getMetaData().getLastModifiedDateTime()).isNotNull();
assertThat(g.getMetaData().getLastModifiedUser()).isNotNull();

em.getTransaction().commit();
em.close();

As you can see in the log output, the mapping worked as expected. All attributes of the MetaData embeddable became part of the ChessGame entity, and Hibernate mapped them to columns of the chessgame table.

15:04:51,692 INFO  [com.thorben.janssen.TestMapping] - ==== testEmbeddable ====
15:04:51,736 INFO  [com.thorben.janssen.model.MetaData] - Set lastModifiedUser and lastModifiedDateTime
15:04:51,742 DEBUG [org.hibernate.SQL] - 
    select
        nextval('ChessGame_SEQ')
15:04:51,749 DEBUG [org.hibernate.SQL] - 
    select
        nextval('ChessGame_SEQ')
15:04:51,807 DEBUG [org.hibernate.SQL] - 
    insert 
    into
        ChessGame
        (chessTournament_id, date, lastModifiedDateTime, lastModifiedUser, round, version, id) 
    values
        (?, ?, ?, ?, ?, ?, ?)

Strengths and Weaknesses of the @Embeddable mapping

As explained earlier, the @Embeddable mapping applies the concept of composition and is the better approach from an object-oriented design perspective.

But as you saw in the example, it also introduces several mapping restrictions. Even though you can use an @Embeddable with all its attributes as an @EmbeddedId, you can’t use it to model only one primary key and several other attributes.

You also can’t use the @Version or the @UpdateTimestamp annotations to map the attributes of an embedded class. Hibernate supports both of them only for entity classes.

If you don’t need these specific annotations, e.g., because you can provide all the required logic in a lifecycle callback method, an @Embeddable is a great way to model shared technical attributes.

Summary

Almost all domain models have technical attributes that are part of nearly all entity classes. You can, of course, map them on each entity class individually. But most teams decide to use a @MappedSuperclass mapping instead. Even though this often feels like a wrong design decision, it’s the more flexible and powerful mapping. As you saw in the examples, the mapping as a @MappedSuperclass doesn’t introduce any limitations. You can use all mapping features that you would otherwise use on an entity class.

From an object-oriented design perspective, mapping this as an @Embeddable is the better approach. It utilizes the concept of composition instead of inheritance. But it introduces a few mapping limitations that might require a few workarounds.

In general, I recommend trying the @Embeddable mapping first. It’s the cleaner approach, and it works really well, as long as you model the version or primary key attributes on your entity classes. If you don’t want to do that, you should use a @MappedSuperclass mapping.

Related Articles

Responses

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.