PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Como posso interceptar eventos de transações JTA e obter uma referência ao EntityManager atual associado à transação


Isso foi rapidamente respondido aqui neste post por mim mesmo, mas escondendo o fato de que passamos mais de duas semanas tentando diferentes estratégias para superar isso. Então, aqui vai nossa implementação final que decidimos usar.

Ideia básica: Crie sua própria implementação de javax.persistence.spi.PersistenceProvider estendendo o dado pelo Hibernate. Para todos os efeitos, este é o único ponto em que seu código estará vinculado ao Hibernate ou a qualquer outra implementação específica do fornecedor.
public class MyHibernatePersistenceProvider extends org.hibernate.jpa.HibernatePersistenceProvider {

    @Override
    public EntityManagerFactory createContainerEntityManagerFactory(PersistenceUnitInfo info, Map properties) {
        return new EntityManagerFactoryWrapper(super.createContainerEntityManagerFactory(info, properties));
    }

}

A ideia é envolver as versões de hibernação de EntityManagerFactory e EntityManager com sua própria implementação. Então você precisa criar classes que implementem essas interfaces e manter a implementação específica do fornecedor dentro.

Este é o EntityManagerFactoryWrapper
public class EntityManagerFactoryWrapper implements EntityManagerFactory {

    private EntityManagerFactory emf;

    public EntityManagerFactoryWrapper(EntityManagerFactory originalEMF) {
        emf = originalEMF;
    }

    public EntityManager createEntityManager() {
        return new EntityManagerWrapper(emf.createEntityManager());
    }

    // Implement all other methods for the interface
    // providing a callback to the original emf.

O EntityManagerWrapper é nosso ponto de interceptação. Você precisará implementar todos os métodos da interface. Em cada método em que uma entidade pode ser modificada, incluímos uma chamada para uma consulta personalizada para definir variáveis ​​locais no banco de dados.
public class EntityManagerWrapper implements EntityManager {

    private EntityManager em;
    private Principal principal;

    public EntityManagerWrapper(EntityManager originalEM) {
        em = originalEM;
    }

    public void setAuditVariables() {
        String userid = getUserId();
        String ipaddr = getUserAddr();
        String sql = "SET LOCAL application.userid='"+userid+"'; SET LOCAL application.ipaddr='"+ipaddr+"'";
        em.createNativeQuery(sql).executeUpdate();
    }

    protected String getUserAddr() {
        HttpServletRequest httprequest = CDIBeanUtils.getBean(HttpServletRequest.class);
        String ipaddr = "";
        if ( httprequest != null ) {
            ipaddr = httprequest.getRemoteAddr();
        }
        return ipaddr;
    }

    protected String getUserId() {
        String userid = "";
        // Try to look up a contextual reference
        if ( principal == null ) {
            principal = CDIBeanUtils.getBean(Principal.class);
        }

        // Try to assert it from CAS authentication
        if (principal == null || "anonymous".equalsIgnoreCase(principal.getName())) {
            if (AssertionHolder.getAssertion() != null) {
                principal = AssertionHolder.getAssertion().getPrincipal();
            }
        }
        if ( principal != null ) {
            userid = principal.getName();
        }
        return userid;
    }

    @Override
    public void persist(Object entity) {
        if ( em.isJoinedToTransaction() ) {
            setAuditVariables();
        }
        em.persist(entity);
    }

    @Override
    public <T> T merge(T entity) {
        if ( em.isJoinedToTransaction() ) {
            setAuditVariables();
        }
        return em.merge(entity);
    }

    @Override
    public void remove(Object entity) {
        if ( em.isJoinedToTransaction() ) {
            setAuditVariables();
        }
        em.remove(entity);
    }

    // Keep implementing all methods that can change
    // entities so you can setAuditVariables() before
    // the changes are applied.
    @Override
    public void createNamedQuery(.....

Desvantagem: As consultas de interceptação (SET LOCAL) provavelmente serão executadas várias vezes dentro de uma única transação, especialmente se houver várias instruções feitas em uma única chamada de serviço. Dadas as circunstâncias, decidimos mantê-lo assim devido ao fato de ser uma simples chamada de SET LOCAL na memória para o PostgreSQL. Como não há mesas envolvidas, podemos conviver com o impacto no desempenho.

Agora é só substituir o provedor de persistência do Hibernate dentro do persistence.xml :
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
             version="2.1">
<persistence-unit name="petstore" transaction-type="JTA">
        <provider>my.package.HibernatePersistenceProvider</provider>
        <jta-data-source>java:app/jdbc/exemplo</jta-data-source>
        <properties>
            <property name="hibernate.transaction.jta.platform" value="org.hibernate.service.jta.platform.internal.SunOneJtaPlatform" />
            <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect"/>
        </properties>
</persistence-unit>

Como nota lateral, este é o CDIBeanUtils que temos para ajudar com o gerenciador de bean em algumas ocasiões especiais. Nesse caso, estamos usando-o para procurar uma referência a HttpServletRequest e Principal.
public class CDIBeanUtils {

    public static <T> T getBean(Class<T> beanClass) {

        BeanManager bm = CDI.current().getBeanManager();

        Iterator<Bean<?>> ite = bm.getBeans(beanClass).iterator();
        if (!ite.hasNext()) {
            return null;
        }
        final Bean<T> bean = (Bean<T>) ite.next();
        final CreationalContext<T> ctx = bm.createCreationalContext(bean);
        final T t = (T) bm.getReference(bean, beanClass, ctx);
        return t;
    }

}

Para ser justo, isso não é exatamente interceptar eventos de transações. Mas podemos incluir as consultas personalizadas que precisamos dentro da transação.

Espero que isso possa ajudar outras pessoas a evitar a dor pela qual passamos.