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.
У вас есть в основном три варианта:
- Чистый способ JPA: объявить "обратный"
@ManyToOne
отношения от пользователя к "другому объекту" и убедитесь, что вы всегда обрабатываете его всякий раз, когда новыйUser
запись создана. - Способ взлома: объявить
@OneToMany
отношения в "другом объекте" и заставить его использовать определенный набор столбцов для объединения с помощью@JoinColumn
, Проблема в том, что JPA всегда ожидает уникальную ссылку на столбцы соединения, так что при чтенииUserDetail
плюс ссылкаUser
записи должны работать, тогда как записьUserDetail
не должен каскадировать наUser
чтобы избежать нежелательных / недокументированных эффектов. - Просто храните пользователя
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;
}
Таким образом, вы можете легко переходить от пользователей к деталям и обратно.