Я хочу получить аудит для некоторых из моих сущностей класса JPA с помощью композиции и некоторых методов обратного вызова JPA. Мой текущий подход (укороченный) выглядит следующим образом:JPA пытается вставить одну и ту же сущность дважды в базу данных при использовании EJB с PersistenceContext в EntityListener
Каждый объект, который я хочу аудита реализует следующий простой интерфейс:
public interface Auditable {
MetaContext getAuditContext();
void setAuditContext(MetaContext context);
}
Мета Контекст является еще одним JPA объект класса, который содержит информацию о ревизии:
@Entity
public class MetaContext implements Serializable {
@Id private Long id;
private Date whenCreated;
private Date whenUpdated;
@ManyToOne private User whoCreated;
@ManyToOne private User whoUpdated;
}
это используется с помощью композиции в моем EntityClass
@Entity
@EntityListeners(AuditListener.class)
public class MyEntity implements Auditable {
// ...
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
private MetaContext auditContext;
}
AuditListener выглядит следующим образом:
public class AuditService {
@Inject private SecurityService securityService;
@PrePersist
public void prePersist(Auditable auditable) {
System.out.println("PrePersist method called");
MetaContext context = new MetaContext();
context.setWhenCreated(new Date());
context.setWhoCreated(securityService.getCurrentUser());
auditable.setAuditContext(context);
}
@PostUpdate
public void postUpdate(Auditable auditable) {
System.out.println("PostUpdate method called");
MetaContext context = auditable.getAuditContext();
context.setWhenUpdated(new Date());
context.setWhoUpdated(securityService.getCurrentUser());
}
}
SecurityService
Где еще один EJB, который я использую, чтобы получить User
экземпляр, который принадлежит к фактическому пользователю, который выполняет действие.
@Stateless
public class SecurityService {
@Resource private SessionContext sctx;
@PersistenceContext private EntityManager em;
public String getCurrentUserName() {
Principal principal = sctx == null ? null : sctx.getCallerPrincipal();
return principal == null ? null : principal.getName();
}
public User getCurrentUser() {
String username = getCurrentUserName();
if (username == null) {
return null;
} else {
String jpqlQuery = "SELECT u FROM User u WHERE u.globalId = :name";
TypedQuery<User> query = em.createQuery(jpqlQuery, User.class);
query.setParameter("name", username);
return EJBUtil.getUniqueResult(query.getResultList());
}
}
}
В каждой сущности @TableGenerator
и @GeneratedValue
используется для создания идентификаторов.
Поведение: При вызове EntityManager.persist
на экземпляр свежего MyEntity, сначала PrePersist
метод моего EntityListener называется, как и следовало ожидать. Дата и пользователь установлены правильно. После этого вызывается метод @PostUpdate
. это похоже на EclipseLink в сочетании с генерацией идентификатора. Я заметил это раньше в других проектах, где я использовал разные подходы. Однако теперь в методе @PostUpdate
служба поиска текущего пользователя используется снова, и это, по-видимому, приводит к тому, что JPA хочет вставить MetaContext
в базу данных несколько раз, что приводит к нарушению ограничения Primaray Key в этой таблице. Если я удалю ограничение PK, операция завершится успешно, но какой неправильный результат: whenUpdated
установлен на текущую дату, но whoUpdated все еще null
. Кроме того, регистрируется предупреждение, которое я действительно не понимаю. Код выше получается следующий лог:
Info: PrePersist method called
Info: PostUpdate method called
Info: PostUpdate method called
Warning: The class RepeatableWriteUnitOfWork is already flushing. The query will be
executed without further changes being written to the database. If the query is
conditional upon changed data the changes may not be reflected in the results.
Users should issue a flush() call upon completion of the dependent changes and
prior to this flush() to ensure correct results.
Теперь это становится еще более странно: если я закомментируйте строку // context.setWhoUpdated(securityService.getCurrentUser());
В методе @PostUpdate
журналы показывают, что метод обратного вызова вызывается (только) один раз, но whenUpdated
остается null
.
Если у вас есть вопросы или информация отсутствует, дайте мне знать, и я обновлю вопрос.
Если я вообще не использую никаких обратных вызовов обновления, я не могу обнаружить никаких проблем.
Soemone here, который может объяснить настоящую проблему и/или знает, как исправить мой подход?
Просто добавьте свой ответ: PostUpdate возникает после того, как инструкция обновления отправляется в базу данных. Изменения в сущности НЕ должны быть синхронизированы с базой данных, а также события не вносят изменения отношения к объекту в соответствии со спецификацией JPA 2.1: «В общем, метод жизненного цикла переносимого приложения не должен вызывать EntityManager или выполнять операции запроса, получить доступ к другим сущности, или изменить отношения в пределах того же контекста персистентности [46]. [47] Метод обратного вызова жизненного цикла может изменять состояние отношения объекта, на котором он вызывается ». – Chris
tyvm для вашего ответа 1) Я пытался использовать разные методы обратного вызова. Первоначально это было PreUpdate. Те же результаты. 2) JPA FlushModeType кажется просто подсказкой 'Flushing для совершения транзакции. Поставщик может зафрезить в другое время, но не обязателен для «3» в соответствии с комментарием @Chris, мой подход нарушает спецификацию JPA, поэтому я должен пойти на другое решение, верно? В самом конце я просто хочу избежать проведения аудита в каждом сервисном методе вручную. Конечно, это был бы еще один вызов метода, но я вижу высокий риск, который кто-то забывает назвать. Любые идеи/альтернативы? – stg