JPA: Как обращаться с версионными сущностями?

У меня есть версия для сущности как часть ее первичного ключа. Управление версиями осуществляется с помощью отметки времени последней модификации:

@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 поле, потому что это часть первичного ключа. Это приводит к проблеме, потому что ссылка может устареть довольно быстро.

Способ может быть в зависимости от id из User, Но если я попробую это, 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, ...

}

3 ответа

Решение

Я пришел к решению, которое не очень удовлетворяет, но работает. Я создал 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, но до тех пор, пока я придерживаюсь политики запрета, она должна работать довольно хорошо.

То, что вам требуется, похоже, находится в строках таблицы истории, чтобы отслеживать изменения. См. https://wiki.eclipse.org/EclipseLink/Examples/JPA/History о том, как EclipseLink может обработать это для вас при использовании обычных / традиционных отображений JPA и использования.

Здесь у вас есть логические отношения 1:1, которые из-за управления версиями становятся техническими отношениями 1:n.

У вас есть в основном три варианта:

  1. Чистый способ JPA: объявить "обратный" @ManyToOne отношения от пользователя к "другому объекту" и убедитесь, что вы всегда обрабатываете его всякий раз, когда новый User запись создана.
  2. Способ взлома: объявить @OneToMany отношения в "другом объекте" и заставить его использовать определенный набор столбцов для объединения с помощью @JoinColumn, Проблема в том, что JPA всегда ожидает уникальную ссылку на столбцы соединения, так что при чтении UserDetail плюс ссылка User записи должны работать, тогда как запись UserDetail не должен каскадировать на User чтобы избежать нежелательных / недокументированных эффектов.
  3. Просто храните пользователя UUID в "другом объекте" и разрешите ссылку самостоятельно, когда вам это нужно.

Добавленный код в вашем вопросе неверен:

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

Более правильным, хотя и не с желаемым результатом, было бы

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

Это не будет работать, потому что, как я сказал выше, у вас может быть более одной пользовательской записи на UserDetailтак что вам нужно @OneToMany отношения здесь, представленные Collection<User>,


Другое "чистое" решение состоит в том, чтобы ввести искусственный объект с соотношением 1:1 по отношению к логическому 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;

}

Таким образом, вы можете легко переходить от пользователей к деталям и обратно.

Другие вопросы по тегам