Search code examples
javaspringspring-bootjpajava-17

How can I make a JPA entity append only where the composite key includes a timestamp?


I have a database where the primary key is a composite key which is composed of a varchar and a timestamp. Each time an entity is modified or inserted, a new entry is added to the database.

How can I model this with a JPA repository? This is what I have right now.

Relevant parts of entity class:

@Entity
@Table(name = "permissions", schema = "auth")
@IdClass(PermissionId.class)
public class Permission implements Serializable {

  @Id
  @Column(name = "permissionid")
  private String permissionId;

  @Id
  @Column(name = "createdat")
  @UpdateTimestamp
  private Instant createdAt;
...
}

ID class:

public class PermissionId implements Serializable {
    private String permissionId;
    private Instant createdAt;
}

When I create a new entity, I get an error saying that createdAt cannot be null. How can I achieve my desired behavior? I cannot change the database schema.

Edit

I found a workaround

@Query(
        value = "INSERT INTO auth.permissions (permissionID, createdAt,...) VALUES (:permissionID, NOW(), ...)",
        nativeQuery = true
)
@Modifying
recordPermission(String permissionID, ...);

This works, but I'm still interested in knowing if there is a "correct " way to do it.


Solution

  • A generic and JPA-standard way to modify the content of an entity in response to its storage operations (create, update, read, delete) is the mechanism of Entity Listeners, described in ch 3.5 of the JPA 2.1 specification. For this case specifically, the @PrePersist listener method, that JPA calls just before persisting an entity (creating/inserting the DB records).

    You can place the listener in the entity class itself, e.g.:

    @Entity
    @Table(name = "permissions", schema = "auth")
    @IdClass(PermissionId.class)
    public class Permission implements Serializable {
      ...
    
      @Id
      @Column(name = "createdat")
      private Instant createdAt;
    
      @PrePersist
      void prePersist() {
        this.createdAt = Instant.now();
      }
    
      ...
    }
    

    Or in a different class like:

    public class PermissionEntityListener {
      @PrePersist
      void prePersist(Permission entity) { // method name can be anything
        entity.setCreatedAt(Instant.now());
      }
    }
    

    If the listener is in a separate class, you have to activate it either with an annotation in the entity class:

    @Entity
    @Table(name = "permissions", schema = "auth")
    @IdClass(PermissionId.class)
    @EntityListeners(PermissionEntityListener.class) // <----- HERE
    public class Permission implements Serializable {
      ...
    }
    

    Or in the XML descriptor, META-INF/orm.xml:

    <entity-mappings>
      ...
      <entity-listeners><!-- can be found at different places, please check specs -->
        <entity-listener class="com.yourpackage.PermissionEntityListener">
          <pre-persist method-name="prePersist" />
        </entity-listener>
      </entity-listeners>
      ...
    </entity-mappings>
    

    There are lots of information online and in the specs for the Entity Listeners.