mybatis / spring

Spring integration for MyBatis 3
Apache License 2.0
2.82k stars 2.6k forks source link

The query cache is not cleared after transaction rolls back to a savepoint #785

Open luozhenyu opened 1 year ago

luozhenyu commented 1 year ago
// assume I inserted a row with 2 columns(id = 1, value = 10);
mapper.insert(1, 10);

// Obviously, the result is 10;
int value = mapper.selectValueById(1);

// Then create a savepoint by Spring
Object savepoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();

// Update its value to 20
mapper.updateValueById(1, 20);

// Select its value, the value is 20 
int value = mapper.selectValueById(1);

// Rollback to savepoint, the value is 10 in database now
TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savepoint);

// Select its value again, but the value is still 20 which is retrieved from local cache
int value = mapper.selectValueById(1);
kazuki43zoo commented 1 year ago

The cache feature belong the MyBatis core module. Probably, the MyBatis core module does not support to clear cache when rollback savepoint. You can prevent this behavior with localCacheScope set to STATEMENT instead of SESSION (default) or call the SqlSession#clearCache().

@harawata Do you have any comments?

harawata commented 1 year ago

MyBatis' cache is not aware of Spring transaction's savepoint. You may have to use the workarounds proposed by @kazuki43zoo .

luozhenyu commented 1 year ago

@harawata How about merge the following code into mybatis-spring-boot-starter? I solved this problem by add a clear cache post processor just like spring-tx did.

import org.aopalliance.intercept.MethodInterceptor;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.aop.framework.AbstractAdvisingBeanPostProcessor;
import org.springframework.aop.support.AopUtils;
import org.springframework.aop.support.StaticMethodMatcherPointcutAdvisor;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource;
import org.springframework.transaction.interceptor.TransactionAttribute;
import org.springframework.transaction.interceptor.TransactionalProxy;

import java.lang.reflect.Method;

/**
 * Clear mybatis cache after transaction rollback
 * <p>Spring will not notify mybatis to clear cache after a savepoint rollback. This class
 * catches a exception, clears the mybatis cache and rethrows it back</p>
 *
 * @author luozhenyu
 */
@Component
public class MybatisClearCachePostProcessor extends AbstractAdvisingBeanPostProcessor implements InitializingBean {

    private final AnnotationTransactionAttributeSource annotationTransactionAttributeSource
        = new AnnotationTransactionAttributeSource();

    private final SqlSessionTemplate sqlSessionTemplate;

    @Autowired
    public MybatisClearCachePostProcessor(SqlSessionTemplate sqlSessionTemplate) {
        this.sqlSessionTemplate = sqlSessionTemplate;
    }

    @Override
    public void afterPropertiesSet() {
        StaticMethodMatcherPointcutAdvisor clearCacheAdvisor = new StaticMethodMatcherPointcutAdvisor() {
            @Override
            public boolean matches(Method method, Class<?> targetClass) {
                if (targetClass != null && TransactionalProxy.class.isAssignableFrom(targetClass)) {
                    return false;
                }
                TransactionAttribute attribute = annotationTransactionAttributeSource.getTransactionAttribute(method, targetClass);
                return attribute != null && attribute.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED;
            }
        };

        clearCacheAdvisor.setAdvice((MethodInterceptor) invocation -> {
            try {
                return invocation.proceed();
            } catch (Throwable t) {
                Method method = invocation.getMethod();
                Class<?> targetClass = invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null;

                TransactionAttribute attribute = annotationTransactionAttributeSource.getTransactionAttribute(method, targetClass);
                if (attribute != null && attribute.rollbackOn(t)) {
                    sqlSessionTemplate.clearCache();
                }
                throw t;
            }
        });

        this.advisor = clearCacheAdvisor;
        this.beforeExistingAdvisors = true;
    }
}