Смешивание декларативных и программных транзакций с помощью Spring и слушателей JPA

Я использую JPA EntityListener для выполнения дополнительной аудиторской работы и вводю Spring управляемый AuditService в свой AuditEntryListener с помощью @Configurable. AuditService генерирует коллекцию объектов AuditEntry. AuditService сам по себе является областью bean, и я хотел бы собрать все объекты AuditEntry под общим ключом, к которому можно получить доступ с помощью внешнего уровня обслуживания (тот, который вызвал вызов persist, который, в свою очередь, вызвал EntityListener).

Я смотрю на использование Spring TransactionSynchronizationManager для установки определенного имени транзакции (используя UID() или другую уникальную стратегию) в начале транзакции, а затем используя это имя как ключ в AuditService, который будет позвольте мне сгруппировать все объекты AuditEntry, созданные в этой транзакции.

Является ли смешивание декларативного и программного управления транзакциями потенциальными проблемами? (Хотя я делаю не что иное, как установку имени транзакции). Есть ли лучший способ связать сгенерированные объекты AuditEntry с текущей транзакцией? Это решение работает для меня, но, учитывая, что TransactionSynchronizationManager не предназначен для использования приложения, я хотел бы убедиться, что его использование не вызовет некоторых непредвиденных проблем.

Связанный с нами вопрос

Наконец, связанный, но не сразу улокальный вопрос: я знаю, что документация для JPA EntityListeners предостерегает от использования текущего EntityManager, но если бы я захотел использовать его, чтобы отличить объект от него, то я был бы в безопасности используя аннотацию @Transactional (распространение = REQUIRES_NEW) вокруг моего метода preUpdate()?

Код прототипа:

Класс обслуживания

@Transactional
public void create(MyEntity e) {

    TransactionSynchronizationManager.setCurrentTransactionName(new UID().toString());
    this.em.persist(e);
    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
        @Override
        public void afterCommit() {
            Set<AuditEntry> entries = auditService.getAuditEntries(TransactionSynchronizationManager.getCurrentTransactionName());
            if(entries != null) {
                for(AuditEntry entry : entries) {
                   //do some stuff....
                   LOG.info(entry.toString());
                }
            }
        }
    });
}

JPA EntityListener

@Configurable
public class AuditEntryListener {

@Autowired
private AuditService service;

@PreUpdate
public void preUpdate(Object entity) {
    service.auditUpdate(TransactionSynchronizationManager.getCurrentTransactionName(), entity);
}

public void setService(AuditService service) {
    this.service = service;
}

public AuditService getService() {
    return service;
}

}

AuditService

@Service
public class AuditService {
private Map<String, Set<AuditEntry>> auditEntryMap = new HashMap<String, Set<AuditEntry>>();

public void auditUpdate(String key, Object entity) {
    // do some audit work
    // add audit entries to map
    this.auditEntryMap.get(key).add(ae);
}

}

Ответ 1

@Filip

Насколько я понимаю, ваше требование:

  • Имейте уникальный токен, сгенерированный в каждой транзакции (база данных транзакция, конечно)
  • Храните этот уникальный токен легко доступным для всех слоев

Итак, вы, естественно, думаете о TransactionSynchronizationManager, предоставляемом Spring, как средство для хранения уникального токена (в данном случае UID)

Будьте очень осторожны с этим подходом, TransactionSynchronizationManager является основным помощником хранения для управления всей обработкой @Transactional для Spring. В области @Transactional hood Spring создается соответствующий EntityManager, соответствующий объект синхронизации и присоединяется к потоку локально с помощью TransactionSynchronizationManager.

В коде класса обслуживания внутри метода @Transactional вы нарушаете объект Synchronization, это может привести к нежелательному поведению.

Я сделал неулокальный анализ того, как здесь работает @Transactional, посмотрите: http://doanduyhai.wordpress.com/2011/11/20/spring-transactional-explained/

Теперь вернемся к вашим потребностям. Что вы можете сделать, это:

  • Добавить поток Local в AuditService, содержащий уникальный токен при вводе метода @Transactional и уничтожить его при выходе из метода. Внутри этого вызова метода вы можете получить доступ к уникальному токену на любом уровне. Объяснение для использования ThreadLocal можно найти здесь: http://doanduyhai.wordpress.com/2011/12/04/threadlocal-explained/
  • Создайте новую аннотацию, скажем, @Auditable (uid = "AuditScenario1" ), чтобы аннотировать методы, которые должны быть проверены, и использовать Spring AOP для перехвата этих вызовов метода и управления локальной обработкой Thread для вас

    Пример:

Измененный АудитСервис

@Service
public class AuditService {

public uidThreadLocal = new ThreadLocal<String>();
...
...
}

Аудитируемая аннотация

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface Auditable 
{
    String uid();
}

Использование @Auditable аннотации

@Auditable(uid="AuditScenario1")
@Transactional
public void myMethod()
{
   // Something 
}

Spring AOP часть

@Around("execution(public * *(..)) && @annotation(auditableAnnotation)) 
public Object manageAuditToken(ProceedingJoinPoint jp, Auditable auditableAnnotation)
{
    ...
    ...
    AuditService.uidThreadLocal.set(auditableAnnotation.uid())...
    ...
}

Надеюсь, это поможет.

Ответ 2

Вы можете найти решение с помощью TransactionSynchronizationManager. Мы регистрируем "TransactionInterceptorEntityListener" с JPA как субъект-слушатель. То, что мы хотели достичь, - это возможность слушать события CRUD, чтобы мы могли работать с управляемым "слушателем" spring, который имеет жизненный цикл, привязанный к текущей транзакции (т.е. spring -установленный, но экземпляр для транзакции). Мы подклассифицируем JPATransactionManager и вводим в метод prepareSynchronization(), крючок для установки "TransactionInterceptorSynchronizer". Мы также используем тот же самый крючок, который позволяет коду (в программном tx) связывать и извлекать произвольные объекты с текущей транзакцией, а также регистрировать задания, которые выполняются до/после транзакции.

Общий код сложный, но определенно способный. Если вы используете JPATemplates для программных tx, достичь этого сложно. Поэтому мы перевернули собственный шаблон, который просто вызывает шаблон JPA после ухода за работой перехватчика. Мы планируем скоро открыть нашу библиотеку JPA (написанную поверх классов spring).

Вы можете увидеть шаблон добавления пользовательских транзакций и перехватов с spring управляемыми транзакциями в следующую библиотеку для Postgresql