JPA:如何处理版本控制的实体?

JPA: How to handle versioned entities?

我将实体的版本控制作为其主键的一部分。版本控制是通过最后一次修改的时间戳完成的:

@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;
}

UUID 自动转换为数据库的 String 并返回模型。 LocalDateTime 也是如此。它会自动转换为 Timestamp 并返回。

我的应用程序的一个关键要求是:数据可能永远不会更新或删除,因此任何更新都会产生一个新的条目 lastModified上面的代码满足了这个要求,并且在这一点之前工作正常。

现在是有问题的部分:我想在用户上引用另一个对象。由于版本控制,这将包括 lastModified 字段,因为它是主键的一部分。这会产生一个问题,因为引用可能很快就会过时。

方法可能取决于 Userid。但是如果我尝试这个,JPA 告诉我,我喜欢访问一个字段,它不是 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, ...

}

解决我的困境的正确方法是什么?

编辑

我得到了 JimmyB 的建议,我尝试过但也失败了。我在这里添加了失败的代码:

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

}

这里是逻辑 1:1 关系,由于版本控制,它变成了技术 1:n 关系。

你基本上有三个选择:

  1. 清洁 JPA 方式:声明从用户到 "other object" 的 'inverse' @ManyToOne 关系,并确保在创建新的 User 记录时始终处理它。
  2. 'Hack-ish' 方式:在 "other object" 中声明一个 @OneToMany 关系,并强制它使用一组特定的列进行使用 @JoinColumn 的连接。这个问题是 JPA 总是期望对连接列的唯一引用,以便 reading UserDetail 加上引用的 User 记录应该工作,而 UserDetail应该级联到User以避免unwanted/undocumented影响。
  3. 只需将用户的 UUID 存储在 "other object" 中,并在需要时自行解析引用。

你问题中添加的代码有误:

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

更正确的是

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

但这行不通,因为正如我上面所说,每个 UserDetail 可能有多个用户记录,因此您需要一个 @OneToMany 关系,由Collection<User>.


另一个 'clean' 解决方案是引入一个具有 1:1 基数 w.r.t 的人造实体。到您可以参考的逻辑 User,例如

@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;

}

这样,您可以轻松地从用户导航到详细信息并返回。

我找到了一个解决方案,虽然不是很令人满意,但确实有效。我创建了一个 UUID 字段 userId,它没有绑定到 Entity 并确保它只在构造函数中设置。

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

}

我不喜欢这样一个事实,即我不能依赖数据库来同步 userId,但只要我坚持不 setter 政策,它应该工作得很好。

您似乎需要的内容似乎在历史记录中 table,以跟踪更改。请参阅 https://wiki.eclipse.org/EclipseLink/Examples/JPA/History,了解 EclipseLink 如何在使用 normal/traditional JPA 映射和用法时为您处理此问题。