/*
* Copyright 2010-2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mybatis.spring;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.fail;
import org.apache.ibatis.exceptions.PersistenceException;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.apache.ibatis.transaction.managed.ManagedTransactionFactory;
import org.junit.After;
import org.junit.Test;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.TransientDataAccessResourceException;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.jta.JtaTransactionManager;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import com.mockrunner.mock.ejb.MockUserTransaction;
import com.mockrunner.mock.jdbc.MockConnection;
import com.mockrunner.mock.jdbc.MockDataSource;
import com.mockrunner.mock.jdbc.MockPreparedStatement;
/**
* @version $Id$
*/
public final class MyBatisSpringTest extends AbstractMyBatisSpringTest {
private SqlSession session;
@After
public void validateSessionClose() {
// assume if the Executor is closed, the Session is too
if ((session != null) && !executorInterceptor.isExecutorClosed()) {
session = null;
fail("SqlSession is not closed");
} else {
session = null;
}
}
// ensure MyBatis API still works with SpringManagedTransaction
@Test
public void testMyBatisAPI() {
session = sqlSessionFactory.openSession();
session.getMapper(TestMapper.class).findTest();
session.close();
assertNoCommit();
assertSingleConnection();
assertExecuteCount(1);
}
@Test
public void testMyBatisAPIWithCommit() {
session = sqlSessionFactory.openSession();
session.getMapper(TestMapper.class).findTest();
session.commit(true);
session.close();
assertCommit();
assertSingleConnection();
assertExecuteCount(1);
}
@Test
public void testMyBatisAPIWithRollback() {
session = sqlSessionFactory.openSession();
session.getMapper(TestMapper.class).findTest();
session.rollback(true);
session.close();
assertRollback();
assertSingleConnection();
assertExecuteCount(1);
}
// basic tests using SqlSessionUtils instead of using the MyBatis API directly
@Test
public void testSpringAPI() {
session = SqlSessionUtils.getSqlSession(sqlSessionFactory);
session.getMapper(TestMapper.class).findTest();
SqlSessionUtils.closeSqlSession(session, sqlSessionFactory);
assertNoCommit();
assertSingleConnection();
assertExecuteCount(1);
}
@Test
public void testSpringAPIWithCommit() {
session = SqlSessionUtils.getSqlSession(sqlSessionFactory);
session.getMapper(TestMapper.class).findTest();
session.commit(true);
SqlSessionUtils.closeSqlSession(session, sqlSessionFactory);
assertCommit();
assertSingleConnection();
}
@Test
public void testSpringAPIWithRollback() {
session = SqlSessionUtils.getSqlSession(sqlSessionFactory);
session.getMapper(TestMapper.class).findTest();
session.rollback(true);
SqlSessionUtils.closeSqlSession(session, sqlSessionFactory);
assertRollback();
assertSingleConnection();
}
@Test
public void testSpringAPIWithMyBatisClose() {
// This is a programming error and could lead to connection leak if there is a transaction
// in progress. But, the API allows it, so make sure it at least works without a tx.
session = SqlSessionUtils.getSqlSession(sqlSessionFactory);
session.getMapper(TestMapper.class).findTest();
session.close();
assertNoCommit();
assertSingleConnection();
}
// Spring API should work with a MyBatis TransactionFactories
@Test
public void testWithNonSpringTransactionFactory() {
Environment original = sqlSessionFactory.getConfiguration().getEnvironment();
Environment nonSpring = new Environment("non-spring", new JdbcTransactionFactory(), dataSource);
sqlSessionFactory.getConfiguration().setEnvironment(nonSpring);
try {
session = SqlSessionUtils.getSqlSession(sqlSessionFactory);
session.getMapper(TestMapper.class).findTest();
SqlSessionUtils.closeSqlSession(session, sqlSessionFactory);
// users need to manually call commit, rollback and close, just like with normal MyBatis
// API usage
assertNoCommit();
assertSingleConnection();
} finally {
sqlSessionFactory.getConfiguration().setEnvironment(original);
}
}
// Spring TX, non-Spring TransactionFactory, Spring managed DataSource
// this should not work since the DS will be out of sync with MyBatis
@Test(expected = TransientDataAccessResourceException.class)
public void testNonSpringTxFactoryWithTx() throws Exception {
Environment original = sqlSessionFactory.getConfiguration().getEnvironment();
Environment nonSpring = new Environment("non-spring", new JdbcTransactionFactory(), dataSource);
sqlSessionFactory.getConfiguration().setEnvironment(nonSpring);
TransactionStatus status = null;
try {
status = txManager.getTransaction(new DefaultTransactionDefinition());
session = SqlSessionUtils.getSqlSession(sqlSessionFactory);
fail("should not be able to get an SqlSession using non-Spring tx manager when there is an active Spring tx");
} finally {
// rollback required to close connection
txManager.rollback(status);
sqlSessionFactory.getConfiguration().setEnvironment(original);
}
}
// Spring TX, non-Spring TransactionFactory, MyBatis managed DataSource
// this should work since the DS is managed MyBatis
@Test
public void testNonSpringTxFactoryNonSpringDSWithTx() throws java.sql.SQLException {
Environment original = sqlSessionFactory.getConfiguration().getEnvironment();
MockDataSource mockDataSource = new MockDataSource();
mockDataSource.setupConnection(createMockConnection());
Environment nonSpring = new Environment("non-spring", new JdbcTransactionFactory(), mockDataSource);
sqlSessionFactory.getConfiguration().setEnvironment(nonSpring);
TransactionStatus status = null;
try {
status = txManager.getTransaction(new DefaultTransactionDefinition());
session = SqlSessionUtils.getSqlSession(sqlSessionFactory);
session.commit();
session.close();
txManager.commit(status);
// txManager still uses original connection
assertCommit();
assertSingleConnection();
// SqlSession uses its own connection
// that connection will not have commited since no SQL was executed by the session
MockConnection mockConnection = (MockConnection) mockDataSource.getConnection();
assertEquals("should call commit on Connection", 0, mockConnection.getNumberCommits());
assertEquals("should not call rollback on Connection", 0, mockConnection.getNumberRollbacks());
assertCommitSession();
} finally {
SqlSessionUtils.closeSqlSession(session, sqlSessionFactory);
sqlSessionFactory.getConfiguration().setEnvironment(original);
}
}
@Test(expected = TransientDataAccessResourceException.class)
public void testChangeExecutorTypeInTx() throws Exception {
TransactionStatus status = null;
try {
status = txManager.getTransaction(new DefaultTransactionDefinition());
session = SqlSessionUtils.getSqlSession(sqlSessionFactory);
session = SqlSessionUtils.getSqlSession(sqlSessionFactory, ExecutorType.BATCH, exceptionTranslator);
fail("should not be able to change the Executor type during an existing transaction");
} finally {
SqlSessionUtils.closeSqlSession(session, sqlSessionFactory);
// rollback required to close connection
txManager.rollback(status);
}
}
@Test
public void testChangeExecutorTypeInTxRequiresNew() throws Exception {
try {
txManager.setDataSource(dataSource);
TransactionStatus status = txManager.getTransaction(new DefaultTransactionDefinition());
session = SqlSessionUtils.getSqlSession(sqlSessionFactory);
// start a new tx while the other is in progress
DefaultTransactionDefinition txRequiresNew = new DefaultTransactionDefinition();
txRequiresNew.setPropagationBehaviorName("PROPAGATION_REQUIRES_NEW");
TransactionStatus status2 = txManager.getTransaction(txRequiresNew);
SqlSession session2 = SqlSessionUtils.getSqlSession(sqlSessionFactory, ExecutorType.BATCH, exceptionTranslator);
SqlSessionUtils.closeSqlSession(session2, sqlSessionFactory);
txManager.rollback(status2);
SqlSessionUtils.closeSqlSession(session, sqlSessionFactory);
txManager.rollback(status);
} finally {
// reset the txManager; keep other tests from potentially failing
txManager.setDataSource(dataSource);
// null the connection since it was not used
// this avoids failing in validateConnectionClosed()
connection = null;
}
}
@Test
public void testWithJtaTxManager() {
JtaTransactionManager jtaManager = new JtaTransactionManager(new MockUserTransaction());
DefaultTransactionDefinition txDef = new DefaultTransactionDefinition();
txDef.setPropagationBehaviorName("PROPAGATION_REQUIRED");
TransactionStatus status = jtaManager.getTransaction(txDef);
session = SqlSessionUtils.getSqlSession(sqlSessionFactory);
session.getMapper(TestMapper.class).findTest();
SqlSessionUtils.closeSqlSession(session, sqlSessionFactory);
jtaManager.commit(status);
// assume a real JTA tx would enlist and commit the JDBC connection
assertNoCommitJdbc();
assertCommitSession();
assertSingleConnection();
}
@Test
public void testWithJtaTxManagerAndNonSpringTxManager() throws java.sql.SQLException {
Environment original = sqlSessionFactory.getConfiguration().getEnvironment();
MockDataSource mockDataSource = new MockDataSource();
mockDataSource.setupConnection(createMockConnection());
Environment nonSpring = new Environment("non-spring", new ManagedTransactionFactory(), mockDataSource);
sqlSessionFactory.getConfiguration().setEnvironment(nonSpring);
JtaTransactionManager jtaManager = new JtaTransactionManager(new MockUserTransaction());
DefaultTransactionDefinition txDef = new DefaultTransactionDefinition();
txDef.setPropagationBehaviorName("PROPAGATION_REQUIRED");
TransactionStatus status = jtaManager.getTransaction(txDef);
try {
session = SqlSessionUtils.getSqlSession(sqlSessionFactory);
session.getMapper(TestMapper.class).findTest();
// Spring is not managing SqlSession, so commit is needed
session.commit(true);
SqlSessionUtils.closeSqlSession(session, sqlSessionFactory);
jtaManager.commit(status);
// assume a real JTA tx would enlist and commit the JDBC connection
assertNoCommitJdbc();
assertCommitSession();
MockConnection mockConnection = (MockConnection) mockDataSource.getConnection();
assertEquals("should call commit on Connection", 0, mockConnection.getNumberCommits());
assertEquals("should not call rollback on Connection", 0, mockConnection.getNumberRollbacks());
assertEquals("should not call DataSource.getConnection()", 0, dataSource.getConnectionCount());
} finally {
SqlSessionUtils.closeSqlSession(session, sqlSessionFactory);
sqlSessionFactory.getConfiguration().setEnvironment(original);
// null the connection since it was not used
// this avoids failing in validateConnectionClosed()
connection = null;
}
}
@Test
public void testWithTxSupports() {
DefaultTransactionDefinition txDef = new DefaultTransactionDefinition();
txDef.setPropagationBehaviorName("PROPAGATION_SUPPORTS");
TransactionStatus status = txManager.getTransaction(txDef);
session = SqlSessionUtils.getSqlSession(sqlSessionFactory);
session.getMapper(TestMapper.class).findTest();
SqlSessionUtils.closeSqlSession(session, sqlSessionFactory);
txManager.commit(status);
// SUPPORTS should just activate tx synchronization but not commits
assertNoCommit();
assertSingleConnection();
}
@Test
public void testRollbackWithTxSupports() {
DefaultTransactionDefinition txDef = new DefaultTransactionDefinition();
txDef.setPropagationBehaviorName("PROPAGATION_SUPPORTS");
TransactionStatus status = txManager.getTransaction(txDef);
session = SqlSessionUtils.getSqlSession(sqlSessionFactory);
session.getMapper(TestMapper.class).findTest();
SqlSessionUtils.closeSqlSession(session, sqlSessionFactory);
txManager.rollback(status);
// SUPPORTS should just activate tx synchronization but not commits
assertNoCommit();
assertSingleConnection();
}
@Test
public void testWithTxRequired() {
DefaultTransactionDefinition txDef = new DefaultTransactionDefinition();
txDef.setPropagationBehaviorName("PROPAGATION_REQUIRED");
TransactionStatus status = txManager.getTransaction(txDef);
session = SqlSessionUtils.getSqlSession(sqlSessionFactory);
session.getMapper(TestMapper.class).findTest();
SqlSessionUtils.closeSqlSession(session, sqlSessionFactory);
txManager.commit(status);
assertCommit();
assertCommitSession();
assertSingleConnection();
}
@Test
public void testSqlSessionCommitWithTx() {
DefaultTransactionDefinition txDef = new DefaultTransactionDefinition();
txDef.setPropagationBehaviorName("PROPAGATION_REQUIRED");
TransactionStatus status = txManager.getTransaction(txDef);
session = SqlSessionUtils.getSqlSession(sqlSessionFactory);
session.getMapper(TestMapper.class).findTest();
// commit should no-op since there is an active transaction
session.commit(true);
SqlSessionUtils.closeSqlSession(session, sqlSessionFactory);
txManager.commit(status);
// Connection should be committed once, but we explicitly called commit on the SqlSession,
// so it should be committed twice
assertEquals("should call commit on Connection", 1, connection.getNumberCommits());
assertEquals("should not call rollback on Connection", 0, connection.getNumberRollbacks());
assertEquals("should call commit on SqlSession", 2, executorInterceptor.getCommitCount());
assertEquals("should not call rollback on SqlSession", 0, executorInterceptor.getRollbackCount());
assertSingleConnection();
}
@Test
public void testWithInterleavedTx() throws Exception {
// this session will use one Connection
session = SqlSessionUtils.getSqlSession(sqlSessionFactory);
session.getMapper(TestMapper.class).findTest();
// this transaction should use another Connection
TransactionStatus status = txManager.getTransaction(new DefaultTransactionDefinition());
// session continues using original connection
session.getMapper(TestMapper.class).insertTest("test2");
session.commit(true);
SqlSessionUtils.closeSqlSession(session, sqlSessionFactory);
// this should succeed
// SpringManagedTransaction (from SqlSession.commit()) should not interfere with tx
txManager.commit(status);
// two DB connections should have completed, each using their own Connection
assertEquals("should call DataSource.getConnection() twice", 2, dataSource.getConnectionCount());
// both connections should be committed
assertEquals("should call commit on Connection 1", 1, connection.getNumberCommits());
assertEquals("should not call rollback on Connection 1", 0, connection.getNumberRollbacks());
assertEquals("should call commit on Connection 2", 1, connectionTwo.getNumberCommits());
assertEquals("should not call rollback on Connection 2", 0, connectionTwo.getNumberRollbacks());
// the SqlSession should have also committed and executed twice
assertCommitSession();
assertExecuteCount(2);
assertConnectionClosed(connection);
assertConnectionClosed(connectionTwo);
}
@Test
public void testSuspendAndResume() throws Exception {
try {
txManager.setDataSource(dataSource);
TransactionStatus status = txManager.getTransaction(new DefaultTransactionDefinition());
session = SqlSessionUtils.getSqlSession(sqlSessionFactory);
// start a new tx while the other is in progress
DefaultTransactionDefinition txRequiresNew = new DefaultTransactionDefinition();
txRequiresNew.setPropagationBehaviorName("PROPAGATION_REQUIRES_NEW");
TransactionStatus status2 = txManager.getTransaction(txRequiresNew);
SqlSession session2 = SqlSessionUtils.getSqlSession(sqlSessionFactory);
assertNotSame("getSqlSession() should not return suspended SqlSession", session, session2);
SqlSessionUtils.closeSqlSession(session2, sqlSessionFactory);
txManager.commit(status2);
// first tx should be resumed now and this should succeed
session.getMapper(TestMapper.class).findTest();
SqlSessionUtils.closeSqlSession(session, sqlSessionFactory);
txManager.commit(status);
// two transactions should have completed, each using their own Connection
assertEquals("should call DataSource.getConnection() twice", 2, dataSource.getConnectionCount());
// both connections and should be committed
assertEquals("should call commit on Connection 1", 1, connection.getNumberCommits());
assertEquals("should not call rollback on Connection 1", 0, connection.getNumberRollbacks());
assertEquals("should call commit on Connection 2", 1, connectionTwo.getNumberCommits());
assertEquals("should not call rollback on Connection 2", 0, connectionTwo.getNumberRollbacks());
// the SqlSession should have also committed twice
assertEquals("should call commit on SqlSession", 2, executorInterceptor.getCommitCount());
assertEquals("should call rollback on SqlSession", 0, executorInterceptor.getRollbackCount());
assertConnectionClosed(connection);
assertConnectionClosed(connectionTwo);
} finally {
// reset the txManager; keep other tests from potentially failing
txManager.setDataSource(dataSource);
// null the connection since it was not used
// this avoids failing in validateConnectionClosed()
connection = null;
}
}
@Test
public void testBatch() {
setupBatchStatements();
session = SqlSessionUtils.getSqlSession(sqlSessionFactory, ExecutorType.BATCH, exceptionTranslator);
session.getMapper(TestMapper.class).insertTest("test1");
session.getMapper(TestMapper.class).insertTest("test2");
session.getMapper(TestMapper.class).insertTest("test3");
// nothing should execute until commit
assertExecuteCount(0);
session.commit(true);
SqlSessionUtils.closeSqlSession(session, sqlSessionFactory);
assertCommit();
assertSingleConnection();
assertExecuteCount(3);
}
@Test
public void testBatchInTx() {
setupBatchStatements();
DefaultTransactionDefinition txDef = new DefaultTransactionDefinition();
txDef.setPropagationBehaviorName("PROPAGATION_REQUIRED");
TransactionStatus status = txManager.getTransaction(txDef);
session = SqlSessionUtils.getSqlSession(sqlSessionFactory, ExecutorType.BATCH, exceptionTranslator);
session.getMapper(TestMapper.class).insertTest("test1");
session.getMapper(TestMapper.class).insertTest("test2");
session.getMapper(TestMapper.class).insertTest("test3");
SqlSessionUtils.closeSqlSession(session, sqlSessionFactory);
txManager.commit(status);
assertCommit();
assertSingleConnection();
assertExecuteCount(3);
}
@Test(expected = PersistenceException.class)
public void testBatchWithError() {
try {
setupBatchStatements();
session = SqlSessionUtils.getSqlSession(sqlSessionFactory, ExecutorType.BATCH, exceptionTranslator);
session.getMapper(TestMapper.class).insertTest("test1");
session.getMapper(TestMapper.class).insertTest("test2");
session.update("org.mybatis.spring.TestMapper.insertFail");
session.getMapper(TestMapper.class).insertTest("test3");
session.commit(true);
SqlSessionUtils.closeSqlSession(session, sqlSessionFactory);
assertCommit();
assertSingleConnection();
assertExecuteCount(4);
} finally {
SqlSessionUtils.closeSqlSession(session, sqlSessionFactory);
}
}
@Test(expected = DataAccessException.class)
public void testBatchInTxWithError() {
setupBatchStatements();
DefaultTransactionDefinition txDef = new DefaultTransactionDefinition();
txDef.setPropagationBehaviorName("PROPAGATION_REQUIRED");
TransactionStatus status = txManager.getTransaction(txDef);
session = SqlSessionUtils.getSqlSession(sqlSessionFactory, ExecutorType.BATCH, exceptionTranslator);
session.getMapper(TestMapper.class).insertTest("test1");
session.getMapper(TestMapper.class).insertTest("test2");
session.update("org.mybatis.spring.TestMapper.insertFail");
session.getMapper(TestMapper.class).insertTest("test3");
SqlSessionUtils.closeSqlSession(session, sqlSessionFactory);
txManager.commit(status);
}
private void setupBatchStatements() {
// these queries must be the same as the query in TestMapper.xml
connection.getPreparedStatementResultSetHandler().addPreparedStatement(
new MockPreparedStatement(connection, "INSERT ? INTO test"));
connection.getPreparedStatementResultSetHandler().prepareThrowsSQLException("INSERT fail");
}
}