PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Comment puis-je intercepter les événements de transactions JTA et obtenir une référence à l'EntityManager actuel associé à la transaction

Cela a été rapidement répondu ici dans ce post par moi-même, mais en cachant le fait que nous avons passé plus de deux semaines à essayer différentes stratégies pour surmonter cela. Alors, voici notre implémentation finale que nous avons décidé d'utiliser.

Idée de base : Créez votre propre implémentation de javax.persistence.spi.PersistenceProvider en étendant celui donné par Hibernate. Pour tous les effets, c'est le seul point où votre code sera lié à Hibernate ou à toute autre implémentation spécifique à un fournisseur.

public class MyHibernatePersistenceProvider extends org.hibernate.jpa.HibernatePersistenceProvider {

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

}

L'idée est d'encapsuler les versions d'hibernate de EntityManagerFactory et EntityManager avec votre propre implémentation. Vous devez donc créer des classes qui implémentent ces interfaces et conserver l'implémentation spécifique au fournisseur à l'intérieur.

Il s'agit de l'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.

L'EntityManagerWrapper est notre point d'interception. Vous devrez implémenter toutes les méthodes de l'interface. À chaque méthode où une entité peut être modifiée, nous incluons un appel à une requête personnalisée pour définir des variables locales dans la base de données.

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(.....

Inconvénient : Les requêtes d'interception (SET LOCAL) s'exécuteront probablement plusieurs fois dans une même transaction, en particulier s'il y a plusieurs déclarations faites lors d'un seul appel de service. Compte tenu des circonstances, nous avons décidé de le conserver car il s'agit d'un simple appel SET LOCAL en mémoire à PostgreSQL. Puisqu'il n'y a pas de tables impliquées, nous pouvons vivre avec le coup de performance.

Maintenant, remplacez simplement le fournisseur de persistance d'Hibernate dans 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>

En remarque, c'est le CDIBeanUtils que nous devons aider avec le gestionnaire de bean lors de certaines occasions spéciales. Dans ce cas, nous l'utilisons pour rechercher une référence à HttpServletRequest et 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;
    }

}

Pour être juste, cela n'intercepte pas exactement les événements Transactions. Mais nous pouvons inclure les requêtes personnalisées dont nous avons besoin dans la transaction.

J'espère que cela pourra aider les autres à éviter la douleur que nous avons traversée.