【MyBatis】Spring整合原理(二):建立 SqlSession
我們現在已經有一個DefaultSqlSessionFactory,按照程式設計式的開發過程,我們接下來就會建立一個 SqlSession 的實現類,但是在 Spring 裡面,我們不是直接使用 DefaultSqlSession 的,而是對它進行了一個封裝,這個 SqlSession 的實現類就是SqlSessionTemplate。這個跟 Spring 封裝其他的元件是一樣的,比如 JdbcTemplate,RedisTemplate 等等,也是 Spring 跟 MyBatis 整合的最關鍵的一個類。
為什麼不用直接使用 DefaultSqlSession?
DefaultSqlSession 是執行緒不安全的。因為 SqlSession 的生命週期是請求和操作(Request/Method),所以我們會在每次請求到來(一個請求一般會執行多條sql)的時候都建立一個 DefaultSqlSession(多例,執行緒安全)
而從 SqlSessionTemplate 的類註釋中,我們可以看到它是執行緒安全的,,Spring IOC 容器中只有一個 SqlSessionTemplate(預設單例)。
SqlSessionTemplate
SqlSessionTemplate 實現了 SqlSession 介面,所以跟 DefaultSqlSession 有一樣的方法:selectOne()、selectList()、insert()、update()、delete()… 不過所有方法的實現都是通過一個代理物件:
這個代理物件在 SqlSessionTemplate 構造方法裡面通過一個代理類建立:
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) { notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required"); notNull(executorType, "Property 'executorType' is required"); this.sqlSessionFactory = sqlSessionFactory; this.executorType = executorType; this.exceptionTranslator = exceptionTranslator; // 基於JDK動態代理建立代理物件 this.sqlSessionProxy = (SqlSession) newProxyInstance( SqlSessionFactory.class.getClassLoader(), new Class[] { SqlSession.class }, new SqlSessionInterceptor()); }
所以,當呼叫 SqlSessionTemplate 中的方法時,它們都會走到內部代理類 SqlSessionInterceptor 的 invoke() 方法
// SqlSessionTemplate的內部類 private class SqlSessionInterceptor implements InvocationHandler { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 首先會使用工廠類、執行器型別、異常解析器建立一個 sqlSession, SqlSession sqlSession = getSqlSession( SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator); try { // 然後再呼叫sqlSession的實現類,實際上就是在這裡呼叫了DefaultSqlSession的方法。 Object result = method.invoke(sqlSession, args); if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) { sqlSession.commit(true); } return result; } catch (Throwable t) { Throwable unwrapped = unwrapThrowable(t); if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) { closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory); sqlSession = null; Throwable translated = SqlSessionTemplate.this.exceptionTranslator. translateExceptionIfPossible((PersistenceException) unwrapped); if (translated != null) { unwrapped = translated; } } throw unwrapped; } finally { if (sqlSession != null) { closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory); } } } }
按照程式設計式使用的套路,拿到 SqlSession 後就可以進行 SqlSession#selectOne()
進行使用了。那在 Spring 中,我們如何在Dao層拿到一個 SqlSessionTemplate 例項?
SqlSessionDaoSupport
MyBatis裡面提供了一個SqlSessionDaoSupport,裡面持有一個SqlSessionTemplate 物件,並且提供了一個 getSqlSession()方法,讓我們獲得一個SqlSessionTemplate。
public abstract class SqlSessionDaoSupport extends DaoSupport {
private SqlSession sqlSession;
private boolean externalSqlSession;
// 使用者在使用時應該先通過該方法傳入 SqlSessionFactory,然後得到一個 SqlSessionTemplate
// 注:傳入的 SqlSessionFactory 是通過xml中配置的 SqlSessionFactoryBean 建立的 SqlSessionFactory 例項
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
if (!this.externalSqlSession) {
this.sqlSession = new SqlSessionTemplate(sqlSessionFactory);
}
}
public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
this.sqlSession = sqlSessionTemplate;
this.externalSqlSession = true;
}
// 使用者通過此方法去獲取 SqlSession
// 注:實際上返回的是上面 setSqlSessionFactory 建立的 SqlSessionTemplate
public SqlSession getSqlSession() {
return this.sqlSession;
}
@Override
protected void checkDaoConfig() {
notNull(this.sqlSession, "Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required");
}
}
也就是說我們讓 DAO 層的實現類繼承 SqlSessionDaoSupport,就可以獲得SqlSessionTemplate,然後在裡面封裝SqlSessionTemplate的方法。但為了減少重複的程式碼,我們通常不會讓我們的實現類直接去繼承SqlSessionDaoSupport,而是先建立一個BaseDao繼承 SqlSessionDaoSupport。
1)在BaseDao裡面封裝對資料庫的操作,包括selectOne()、selectList()、 insert()、delete()這些方法,子類就可以直接呼叫。
public class BaseDao extends SqlSessionDaoSupport {
// 在IOC容器獲取 SqlSessionFactory
// 注:這裡實際是通過xml中配置的 SqlSessionFactoryBean 建立的 SqlSessionFactory 例項
@Autowired
private SqlSessionFactory sqlSessionFactory;
@Autowired
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
super.setSqlSessionFactory(sqlSessionFactory);
}
public Object selectOne(String statement, Object parameter) {
// 呼叫SqlSessionDaoSupport的getSqlSession方法獲取到SqlSessionTemplate
return getSqlSession().selectOne(statement, parameter);
}
// .......
}
2)讓我們的實現類繼承BaseDao並且實現我們的DAO層介面,這裡就是我們的Mapper介面。實現類需要加上@Repository的註解。在實現類的方法裡面,我們可以直接呼叫父類(BaseDao)封裝的selectOne()方法,那麼它最終會呼叫sqlSessionTemplate的selectOne()方法。
@Repository
public class EmployeeDaoImpl extends BaseDao implements EmployeeMapper {
@Override
public Employee selectByPrimaryKey(Integer empId) {
// 最後會執行 sqlSession.selectOne("com.my.dao.EmployeeMapper.selectById",empId);
Employee emp = (Employee) this.selectOne("com.my.dao.EmployeeMapper.selectById",empId);
return emp;
}
// ......
}
3)在需要使用的地方,比如Service層,注入我們的實現類,呼叫實現類的方法就行了。我們這裡直接在單元測試類裡面注入:
@Autowired
EmployeeDaoImpl employeeDao;
@Test
public void EmployeeDaoSupportTest() {
// 最終會呼叫到DefaultSqlSession的方法。
System.out.println(employeeDao.selectById(1));
}
雖然這樣也能完資料庫操作,但是仍然存在問題:
- 程式碼多:我們的每一個DAO層的介面(Mapper介面也屬於),如果要拿到一個 SqlSessionTemplate 去操作資料庫,都要建立實現一個實現類,加上@Repository的註解,繼承BaseDao,這個工作量也不小。
- 硬編碼:我們去直接呼叫 selectOne() 方法,還是出現了 StatementID 的硬編碼,並且 MyBatis 內部基於介面的動態代理 MapperProxy 在這裡根本沒用上。
所以,我們能不能通過什麼方式實現以下兩個目標?
- 不建立任何的實現類,而是通過 @Autowired 直接將 Mapper 注入到要使用的地方,並且可以拿到 SqlSessionTemplate 去操作資料庫
- 當執行資料庫操作的時候不是硬編碼,而是是基於 MapperProxy 動態生成實現物件
請看下一篇