1. 程式人生 > 資料庫 >【MyBatis】Spring整合原理(二):建立 SqlSession

【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)); 
} 

雖然這樣也能完資料庫操作,但是仍然存在問題:

  1. 程式碼多:我們的每一個DAO層的介面(Mapper介面也屬於),如果要拿到一個 SqlSessionTemplate 去操作資料庫,都要建立實現一個實現類,加上@Repository的註解,繼承BaseDao,這個工作量也不小。
  2. 硬編碼:我們去直接呼叫 selectOne() 方法,還是出現了 StatementID 的硬編碼,並且 MyBatis 內部基於介面的動態代理 MapperProxy 在這裡根本沒用上。

所以,我們能不能通過什麼方式實現以下兩個目標?

  1. 不建立任何的實現類,而是通過 @Autowired 直接將 Mapper 注入到要使用的地方,並且可以拿到 SqlSessionTemplate 去操作資料庫
  2. 當執行資料庫操作的時候不是硬編碼,而是是基於 MapperProxy 動態生成實現物件

請看下一篇