我们现在已经有一个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 动态生成实现对象