2017-01-03 25 views
0

Я хочу получить аудит для некоторых из моих сущностей класса 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, который может объяснить настоящую проблему и/или знает, как исправить мой подход?

ответ

3

Раскрытие информации: У меня довольно хорошее понимание JPA, но я никогда не использовал его в настройке EJB.

Идентификатор зарегистрированного предупреждения является nested_entity_manager_flush_not_executed_pre_query_changes_may_be_pending, и если вы посмотрите на код RepeatableWriteUnitOfWork (строка 421, EL 2.5.2) вы можете видеть, что много кода пропускается. По некоторым причинам изменения в UnitOfWork записываются дважды.

Следует помнить, что во время транзакции EclipseLink сбрасывается (по умолчанию используется режим флеша), прежде чем он выполнит выбор запросов, этот предварительный запрос-флеш должен обеспечить согласованность базы данных, иначе вы могли бы закончить которые вы ранее удалили в транзакции. Поскольку вы выполняете выделение внутри PostUpdate, это приведет к флешу, возможно, именно поэтому writeChanges() вызывается дважды.

Другое дело, что мне кажется странным, что вы используете Pre Упорство, но сообщение Update - почему асимметрия, она должна быть не PreUpdate?

Edit: Вы могли бы попытаться изменить режим промывки для запросов, чтобы задержать флеш до совершения query.setFlushMode(), до тех пор, пока вы не планируете удалить пользователя в той же самой транзакции вы все должно быть в порядке.

+1

Просто добавьте свой ответ: PostUpdate возникает после того, как инструкция обновления отправляется в базу данных. Изменения в сущности НЕ должны быть синхронизированы с базой данных, а также события не вносят изменения отношения к объекту в соответствии со спецификацией JPA 2.1: «В общем, метод жизненного цикла переносимого приложения не должен вызывать EntityManager или выполнять операции запроса, получить доступ к другим сущности, или изменить отношения в пределах того же контекста персистентности [46]. [47] Метод обратного вызова жизненного цикла может изменять состояние отношения объекта, на котором он вызывается ». – Chris

+0

tyvm для вашего ответа 1) Я пытался использовать разные методы обратного вызова. Первоначально это было PreUpdate. Те же результаты. 2) JPA FlushModeType кажется просто подсказкой 'Flushing для совершения транзакции. Поставщик может зафрезить в другое время, но не обязателен для «3» в соответствии с комментарием @Chris, мой подход нарушает спецификацию JPA, поэтому я должен пойти на другое решение, верно? В самом конце я просто хочу избежать проведения аудита в каждом сервисном методе вручную. Конечно, это был бы еще один вызов метода, но я вижу высокий риск, который кто-то забывает назвать. Любые идеи/альтернативы? – stg