The problem
Sometimes we want to register a default Entity Listeners which are called as soon an entity is persisted, without adding the @EntityListeners
annotation to our classes, as in most cases we anyway provide interfaces so we can access data on the entity itself. The problem is, that we cannot annotate our entity listener with any annotation, only the entity itself.
Solution
In JPA we can provide entity mappings in the orm.xml
which allows us to define default listeners which are executed on any entity.
<?xml version="1.0" encoding="UTF-8"?> <entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm http://xmlns.jcp.org/xml/ns/persistence/orm_2_1.xsd" version="2.1"> <persistence-unit-metadata> <persistence-unit-defaults> <entity-listeners> <entity-listener class="org.sterl.example.EntityListener"> <!-- optional, annotations will work too --> <pre-persist method-name="fooMethod"/> </entity-listener> </entity-listeners> </persistence-unit-defaults> </persistence-unit-metadata> </entity-mappings>
Furthermore, the body of the entity-listener may contain elements pre-persist to define method.
Example
Let’s assume we want to add to each entity an update time and a create time. We could, of course, implement a base entity, but as we should favor composition over inheritance — which would allow us to add even more common fields to the entity — we want to go with an embedded class. Let’s say TimestampBE
.
Embeddable time stamps
This class adds to any entity the create date and update date. We just store UTC longs measured from 1970-01-01T00:00Z.
@Embeddable @Getter @Setter public class TimestampBE implements Serializable { @Column(name = "create_date", nullable = false, updatable = false) private Long createDate; @Column(name = "update_date", nullable = false, updatable = true) private Long updateDate; }
Tag interface
Next, we add an interface that allows us to tag any entity that has a timestamp.
/** * Interface which marks an entity that it has the timestamp entity. */ public interface HasTimestamp { TimestampBE getTimestamp(); void setTimestamp(TimestampBE timestamp); }
Timestamp Entity Listener
Now we have to define the behavior which sets us the create and update date in the live cycle of the entity.
public class TimestampEntityListener { @PrePersist void prePersist(Object entity) { if (entity instanceof HasTimestamp) { TimestampBE timestamp = getTimestamp((HasTimestamp)entity); final long now = Clock.systemUTC().millis(); timestamp.setCreateDate(now); timestamp.setUpdateDate(now); } } @PreUpdate void preUpdate(Object entity) { if (entity instanceof HasTimestamp) { TimestampBE timestamp = getTimestamp((HasTimestamp)entity); final long now = Clock.systemUTC().millis(); if (timestamp.getCreateDate() == null) timestamp.setCreateDate(now); timestamp.setUpdateDate(now); } } private TimestampBE getTimestamp(HasTimestamp ht) { TimestampBE timestamp = ht.getTimestamp(); if (timestamp == null) { timestamp = new TimestampBE(); ht.setTimestamp(timestamp); } return timestamp; } }
Reister the default listener
Add the src/main/resources/META-INF/orm.xml
and register the Listener above. Adjust the package as needed:
<?xml version="1.0" encoding="UTF-8"?> <entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm http://xmlns.jcp.org/xml/ns/persistence/orm_2_1.xsd" version="2.1"> <persistence-unit-metadata> <persistence-unit-defaults> <entity-listeners> <entity-listener class="org.sterl.jpa.timestamp.TimestampEntityListener" /> </entity-listeners> </persistence-unit-defaults> </persistence-unit-metadata> </entity-mappings>
Use the new classes
In the end, you may add the new class at any entity:
@Entity @Data @NoArgsConstructor @AllArgsConstructor @Builder public class PersonBE implements HasTimestamp, Serializable { @Id @GeneratedValue @Column(name = "person_id") private Long id; private String name; @Embedded private TimestampBE timestamp; }