简体   繁体   中英

How to comprehensively intercept Insert/Update/Delete DB Transactions in a Hibernate 4 application

I created several similar threads about particular issues, but let me summarize the problem I'm facing more globally:

GOAL

Need to intercept all Insert/Update/Delete DB Transactions in an application written in Spring 4.1.5, Hibernate 4.3.8. The app uses all styles of Hibernate usage:

  • Object-based through Session, eg sessionFactory.getCurrentSession().saveOrUpdate(obj);
  • HQL executeUpdate, eg Query q = "update Obj..."; q.executeUpdate(); Query q = "update Obj..."; q.executeUpdate();
  • Criteria API, eg Criteria q = sessionFactory.getCurentSession().createCriteria(..); q.list(); Criteria q = sessionFactory.getCurentSession().createCriteria(..); q.list();

Possible Approaches

  • Interceptor : Implement an Interceptor.

public class MyInterceptor extends EmptyInterceptor {..}

Override either (1) Transaction-level methods or (2) specific action methods.

Transaction-level:

@Override
    public void afterTransactionBegin(Transaction tx) {                 
        super.afterTransactionBegin(tx);            
    }

Specific action level :

@Override
public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
    return super.onSave(entity, id, state, propertyNames, types);
}

@Override
public void onDelete(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
    super.onDelete(entity, id, state, propertyNames, types);
}
  • EventListener : Implement a PreUpdateEventListener , PreInsertEventListener and register it.

     @Component public class HibernateSaveUpdateEventListener implements PreUpdateEventListener, PreInsertEventListener { @Override public boolean onPreInsert(PreInsertEvent arg0) { //... } @Override public boolean onPreUpdate(PreUpdateEvent arg0) { //... } }

Problems

  • Neither the EventListener nor Interceptor approach intercepts HQL executeUpdate() . But we have many instances of that in the app. Some other users have confirmed that:

https://forum.hibernate.org/viewtopic.php?f=1&t=1012054

https://coderanch.com/t/669945/java/Hibernate-interceptor-working-hibernate-query

Seems like the issue is that EventListener/Interceptor only work with a Session . But HQL executeUpdate is on the Query object, not the Session .

  • Can't use the Transaction-level Interceptor approach of afterTransactionBegin(Transaction tx) either. The reason is, I can't distinguish Read-Only Select Transactions from Write Transactions. tx doesn't tell me whether I'm in a readOnly=false or readOnly=true transaction. So the afterTransactionBegin override will fire for all transactions, and I need to limit it non-Read-Only ones only (Insert, Update, Delete).

@Transactional(readOnly = false) @Transactional(readOnly = true)

So is there a solution to comprehensively intercept Insert/Update/Delete for a Hibernate app that uses both object operations and HQL operations?

There's an easy way to check for Read-Only/non-Read-Only Transactions with a Spring-level transaction listener, and this will include outside SQL/HQL (non-entity) . On the Spring level, rather than Hibernate, there's a method called TransactionSynchronizationManager.isCurrentTransactionReadOnly() that tells us if we're inside a read-only transaction we're intercepting. It can be called from the following implementation of TransactionSynchronization which uses a pointcut to intercept all @Before @Transactional Spring processing.

@Component
@Aspect
public class TransactionSynchronizationAspect implements TransactionSynchronization {   
    
    @Before("@annotation(org.springframework.transaction.annotation.Transactional)")
    public void registerTransactionSynchronization() {
        boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
        
        if (!isReadOnly) {
            // ... logic goes here...
        }

        // Continue
        TransactionSynchronizationManager.registerSynchronization(this);

    }

    @Override
    public void afterCompletion(int status) {
        // No custom code here
    }
}

As for the Hibernate approach -- the original Hibernate Transaction Listener in the Original Post -- it doesn't distinguish between Read-Only/non-Read-Only, but we can add a tx:advice to restrict the Listener to fire only on specified (mapped) Service method names, like so:

<aop:config>
    <aop:advisor id="managerTx" advice-ref="txAdvice"
        pointcut="execution(* com.app.*.*.service.*.*(..))"/>
</aop:config>

<tx:advice id="txAdvice" transaction-manager="customTransactionManager">
    <tx:attributes>
        <tx:method name="delete*" propagation="REQUIRED" isolation="READ_COMMITTED"/>
        <tx:method name="update*" propagation="REQUIRED" isolation="READ_COMMITTED"/>
        <tx:method name="save*" propagation="REQUIRED" isolation="READ_COMMITTED"/>
    </tx:attributes>
</tx:advice>

This XML file can be imported with @ImportResource. With this strategy, all Write operations would be intercepted with the Listener, while Reads would always go through automatically (ie we don't need to do anything custom with them). This Hibernate Transaction Listener can be implemented as

public class HibernateTransactionInterceptor extends EmptyInterceptor {
    @Override
    public void afterTransactionBegin(Transaction tx) {
      //...
    }    
}

I managed to intercept executeUpdate() by overriding ASTQueryTranslatorFactory with my own implementation:

You just need to set the hibernate property hibernate.query.factory_class with a custom class.

<bean lazy-init="false" id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean" depends-on="dataSource">
    ...
    <property name="jpaProperties">
        <props>
            <prop key="hibernate.query.factory_class">package.to.your.OwnASTQueryTranslatorFactory</prop>
            ...other properties
        </props>
    </property>
</bean>
public class OwnASTQueryTranslatorFactoryextends ASTQueryTranslatorFactory {
    private static final Pattern INVALID_PATTERN = Pattern.compile("some regexp to prevent certain queries to be executed", Pattern.CASE_INSENSITIVE);

    public static boolean isInvalidHql(String hql) {
        return INVALID_PATTERN.matcher(hql).find()
    }
    
    @Override
    public QueryTranslator createQueryTranslator(
            String queryIdentifier,
            String hql,
            Map filters,
            SessionFactoryImplementor factory,
            EntityGraphQueryHint entityGraphQueryHint) {
        if (isInvalidHql(hql)) {
            /**
             * My use-case was to prevent certain syntax for HQL to be executed
             * here you can add w/e you want to do when the `createQuery` is executed.
             *
             * There's a lot more you can do if you also add a custom implementation
             * of `QueryTranslatorImpl`
             * 
             **/
            throw new QueryException("Invalid HQL!", hql);
        }
        return super.createQueryTranslator(queryIdentifier, hql, filters, factory, entityGraphQueryHint);
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM