2

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, ...

}
tgr
  • 3,557
  • 4
  • 33
  • 63

3 Answers3

0

What you have here is a logical 1:1 relationship which, due to versioning, becomes a technical 1:n relationship.

You have basically three options:

  1. Clean JPA way: Declare an 'inverse' @ManyToOne relationship from user to the "other object" and make sure you always handle it whenever a new User record is created.
  2. 'Hack-ish' way: Declare a @OneToMany relationship in the "other object" and force it to use a specific set of columns for the join using @JoinColumn. The problem with this is that JPA always expects unique reference over the join columns so that reading the UserDetail plus referenced User records should work, whereas writing UserDetail should not cascade onto User to avoid unwanted/undocumented effects.
  3. Just store the user's UUID in the "other object" and resolve the reference yourself whenever you need it.

The added code in your question is wrong:

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

More correct, albeit not with the result you want, would be

@JoinColumn(name = "USER_ID", referencedColumnName = "ID")
private User user;

This won't work though, because, as I said above, you may have more than one user record per UserDetail, so you'd need a @OneToMany relationship here, represented by a Collection<User>.


Another 'clean' solution is to introduce an artificial entity with a 1:1 cardinality w.r.t. to the logical User to which you can refer, like

@Entity
public class UserId {
  @Id
  private UUID id;

  @OneToMany(mappedBy="userId")
  private List<User> users;

  @OneToOne(mappedBy="userId")
  private UserDetail detail;
}

@Entity
public class User {

  @Id
  private Long _id;

  @ManyToOne
  private UserId userId;

}

@Entity
public class UserDetail {

  @OneToOne
  private UserId userId;

}

This way, you can somewhat easily navigate from users to details and back.

JimmyB
  • 12,101
  • 2
  • 28
  • 44
  • Thank you for your answer. I tried your second solution and failed (see updated answer). Could you please review it and point out any flaws you can find? – tgr Oct 10 '17 at 08:51
0

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.

tgr
  • 3,557
  • 4
  • 33
  • 63
0

What you seem to require seems to be on the lines of a history table, to keep track of the changes. See https://wiki.eclipse.org/EclipseLink/Examples/JPA/History on how EclipseLink can handle this for you while using normal/traditional JPA mappings and usage.

Chris
  • 20,138
  • 2
  • 29
  • 43
  • Hibernate calls the feature ["Envers"](http://hibernate.org/orm/envers/). – JimmyB Oct 10 '17 at 15:24
  • Thank you for this solution, unfortunately this violates the never update policy (by setting `end_date`, when the entry outdates). Otherwise, this would really the solution to my problem. – tgr Oct 11 '17 at 06:29
  • The end date makes it easier to query and tell when an item has been removed (soft delete), something I'm not sure how you would handle if the row cannot be modified. The HistoryPolicy can be subclassed to get the functionality closer to what you want. – Chris Oct 11 '17 at 14:37