Search code examples
javaoracle-databasejpaeclipselinkor-mapper

JPA: How to handle versioned entities?


I have a versioning on an entity as part of its primary key. The versioning is done via a timestamp of the last modification:

@Entity
@Table(name = "USERS")
@IdClass(CompositeKey.class)
public class User {
  @Column(nullable = false)
  private String name;

  @Id
  @Column(name = "ID", nullable = false)
  private UUID id;

  @Id
  @Column(name = "LAST_MODIFIED", nullable = false)
  private LocalDateTime lastModified;

  // Constructors, Getters, Setters, ...

}

/**
 * This class is needed for using the composite key.
 */
public class CompositeKey {
  private UUID id;
  private LocalDateTime lastModified;
}

The UUID is translated automatically into a String for the database and back for the model. The same goes for the LocalDateTime. It gets automatically translated to a Timestamp and back.

A key requirement of my application is: The data may never update or be deleted, therefore any update will result in a new entry with a younger lastModified. This requirement is satisfied with the above code and works fine until this point.

Now comes the problematic part: I want another object to reference on a User. Due to versioning, that would include the lastModified field, because it is part of the primary key. This yields a problem, because the reference might obsolete pretty fast.

A way to go might be depending on the id of the User. But if I try this, JPA tells me, that I like to access a field, which is not an Entity:

@Entity
@Table(name = "USER_DETAILS")
public class UserDetail {

  @Id
  @Column(nullable = false)
  private UUID id;

  @OneToOne(optional = false)
  @JoinColumn(name = "USER_ID", referencedColumnName = "ID")
  private UUID userId;

  @Column(nullable = false)
  private boolean married;

  // Constructors, Getter, Setter, ...

}

What would be the proper way of solving my dilemma?

Edit

I got a suggestion by JimmyB which I tried and failed too. I added the failing code here:

@Entity
@Table(name = "USER_DETAILS")
public class UserDetail {

  @Id
  @Column(nullable = false)
  private UUID id;

  @OneToMany
  @JoinColumn(name = "USER_ID", referencedColumnName = "ID")
  private List<User> users;

  @Column(nullable = false)
  private boolean married;

  public User getUser() {
    return users.stream().reduce((a, b) -> {
      if (a.getLastModified().isAfter(b.getLastModified())) {
        return a;
      }
      return b;
    }).orElseThrow(() -> new IllegalStateException("User detail is detached from a User."));
  }

  // Constructors, Getter, Setter, ...

}

Solution

  • I came to a solution, that is not really satisfying, but works. I created a UUID field userId, which is not bound to an Entity and made sure, it is set only in the constructor.

    @Entity
    @Table(name = "USER_DETAILS")
    public class UserDetail {
    
      @Id
      @Column(nullable = false)
      private UUID id;
    
      @Column(nullable = false)
      // no setter for this field
      private UUID userId;
    
      @Column(nullable = false)
      private boolean married;
    
      public UserDetail(User user, boolean isMarried) {
        this.id = UUID.randomUUID();
        this.userId = user.getId();
        this.married = isMarried;
      }
    
      // Constructors, Getters, Setters, ...
    
    }
    

    I dislike the fact, that I cannot rely on the database, to synchronize the userId, but as long as I stick to the no setter policy, it should work pretty well.