1. 程式人生 > >Mybatis介紹之引數傳遞

Mybatis介紹之引數傳遞

1     使用Mapper介面時引數傳遞方式

       Mybatis在使用Mapper介面進行程式設計時,其實底層是採用了動態代理機制,表面上是呼叫的Mapper介面,而實際上是通過動態代理呼叫的SqlSession的對應方法,如selectOne(),有興趣的朋友可以檢視DefaultSqlSession的getMapper()方法實現,其最終會獲得一個代理了Mapper介面的MapperProxy物件。MapperProxy物件在呼叫Mapper介面方法時會把傳遞的引數做一個轉換,然後把轉換後的引數作為入參呼叫SqlSession對應的操作方法(如selectOne、insert等)。轉換過程可以參考MapperMethod的execute()方法實現。簡單來說是以下規則:

1、如果傳遞過來是單引數,且沒有以@Param註解進行命名,則直接將單引數作為真實的引數呼叫SqlSession的對應方法。

2、如果傳遞過來的不是單引數或者是包含以@Param註解進行命名的引數,則會將對應的引數轉換為一個Map進行傳遞。具體規則如下:

2.1、      會把對應的引數按照順序以param1、param2、paramN這樣的形式作為Key存入目標Map中,第一個引數是param1,第N個引數是paramN。

2.2、      如果引數是以@Param註解命名的引數,則以@Param指定的名稱作為Key存入目標Map中。

2.3、      如果引數不是以@Param註解命名的,則按照順序以0、1、N這樣的形式作為Key存入目標Map中,第一個引數是0,第N個引數是N。

       我們在Mapper.xml中定義SQL語句中用到的變數最終是以Mybatis按照上述規則把入參轉換後的形式來進行解析的。

2     引數引用#與$的區別

       在Mybatis的Mapper.xml檔案中為了構建動態的SQL,我們通常需要通過引用傳遞的引數來達到對應的效果。在引用傳遞的引數的時候有兩種方式,一種是通過“#{var}”的形式引用,一種是通過“${var}”的形式引用。前者會被以預編譯的方式傳遞,即會生成PreparedStatement來處理,對應的引數在SQL中會以“?”佔位,然後呼叫PreparedStatement的相應API設值;而後者則會先處理好變數再執行相應的SQL,即對應的變數會先被替換,再執行替換了變數的SQL。在一個Mapper對映語句中我們可以同時使用“#”和“$”處理變數。

3     採用#{var}的形式處理變數

       採用#{var}的形式來引用變數時,其中的變數會在解析Mapper.xml檔案中的語句時就被替換為佔位符“?”,同時通過ParameterMapping類記錄對應的變數資訊。在真正執行對應的語句時會用傳遞的真實引數根據ParameterMapping資訊給PreparedStatement設定引數,具體可參考PreparedStatementHandler的parameterize()方法實現。對應的變數在解析的時候都是以傳遞進來的引數為基礎來解析的,根據前面的介紹我們知道,傳遞進來的引數只有兩種情況,一種是原始型別的單引數,一種是被Mybatis封裝過的Map。那對應的引數Mybatis的如何解析的呢?有三種情況。

       第一種,如果真實傳遞進來的是原始型別,那麼對應的屬性解析就是就是基於原始型別的屬性。比如我們真是傳遞進來的一個User型別的物件,User物件有id、name等屬性,那麼我們在Mapper語句中採用#{var}形式訪問變數的時候就可以使用#{id}和#{name}。這跟使用${var}形式訪問變數是不同的,使用${var}形式時我們需要使用${_parameter.id}和${_parameter.name}。

   <insert id="insert"parameterType="com.elim.learn.mybatis.model.User"useGeneratedKeys="true" keyColumn="id" keyProperty="id">

      insert into t_user(name,username,email,mobile) values(#{name},#{username},#{email},#{mobile})

   </insert>

       第二種,如果真實傳遞進來的是一個Map,那麼對應的變數則可以是這個Map裡面的Key。比如下面示例中我們的介面方法對應的是兩個引數,那最終到執行Mapper語句時會被封裝成一個Map,Map裡面的Key包含name、1、param1和param2,那麼我們在使用的時候就可以使用這些變數。

   List<User> findByNameAndMobile(@Param("name") String name, Stringmobile);

   <select id="findByNameAndMobile"resultType="com.elim.learn.mybatis.model.User">

      select id,name,username,email,mobile from t_user where name=#{name} and mobile=#{1}

   </select>

       第三種,真實傳遞進來的引數是可以被已經註冊了的TypeHandler進行處理的,則直接使用真實傳遞進來的引數作為真實的變數值。這也是為什麼我們經常可以在Mapper接口裡面寫delete(Integer id),然後在Mapper語句中直接寫#{id}的原因,因為這個時候傳遞進來的引數是Integer型別,是可以直接被處理的,Mybatis將直接拿它的值作為當前變數id的值,而不會去取傳遞進來的值的id屬性。換句話說,這個時候我們在Mapper語句中定義的變數可以隨便命名,隨便怎麼命名都可以被Mybatis正確的處理。但是通常我們會把它命名為與介面方法引數名稱一致,方便閱讀。

   void delete(Long id);

   <insert id="delete" parameterType="java.lang.Long">

      delete t_user where id=#{id}

   </insert>

       關於預編譯的引數處理是由DefaultParameterHandler類的setParameters()方法處理,核心程式碼如下,欲瞭解更多,可以參考DefaultParameterHandler的完整程式碼。

  public void setParameters(PreparedStatement ps) {

    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());

    List<ParameterMapping> parameterMappings =boundSql.getParameterMappings();

    if (parameterMappings != null) {

      for (inti = 0; i < parameterMappings.size(); i++) {

        ParameterMapping parameterMapping = parameterMappings.get(i);

        if (parameterMapping.getMode() != ParameterMode.OUT) {

          Object value;

          String propertyName = parameterMapping.getProperty();

          if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params

            value = boundSql.getAdditionalParameter(propertyName);

          } else if (parameterObject == null) {

            value = null;

          } else if(typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {

            value = parameterObject;

          } else {

            MetaObject metaObject =configuration.newMetaObject(parameterObject);

            value = metaObject.getValue(propertyName);

          }

          TypeHandler typeHandler = parameterMapping.getTypeHandler();

          JdbcType jdbcType = parameterMapping.getJdbcType();

          if (value == null && jdbcType == null) {

            jdbcType = configuration.getJdbcTypeForNull();

          }

          try {

            typeHandler.setParameter(psi + 1, valuejdbcType);

          } catch (TypeException e) {

            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + ee);

          } catch (SQLException e) {

            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + ee);

          }

        }

      }

    }

  }

4     採用${var}的形式處理變數

       採用“${var}”的形式來引用變數時,其中的變數會在MappedStatement呼叫getBoundSql()方法獲取對應的BoundSql時被替換。${var}中的var可以是如下取值:

1、內建的_parameter變數,對應轉換後的傳遞引數,在只傳遞單引數且是沒有使用@Param註解對引數命名的時候如果我們需要通過${var}的形式來訪問傳遞的單引數,則可以使用${_parameter};

2、如果對應的Mapper介面方法是多引數或者擁有@Param命名的引數時可以使用param1、paramN的形式;

3、如果對應的Mapper介面方法引數是@Param命名的方法引數,則可以使用@Param指定的名稱;

4、如果對應的Mapper介面方法擁有多個引數,且擁有沒有使用@Param命名的引數,那沒有使用@Param命名的引數可以通過0、1、N形式訪問。

       根據上述規則如我們有一個findById的方法其接收一個Long型別的引數作為ID,當使用${var}的形式引用變數時則可以寫成如下這樣:

   <select id="findById"resultType="com.elim.learn.mybatis.model.User"parameterType="java.lang.Long" >

      select id,name,username,email,mobile from t_user where id=${_parameter}

   </select>

       當我們的Mapper介面方法引數使用了@Param命名的時候,我們還可以使用@Param指定的引數名。

public interface UserMapper {

   User findById(@Param("id") Long id);

}

   <select id="findById"resultType="com.elim.learn.mybatis.model.User"parameterType="java.lang.Long" >

      select id,name,username,email,mobile from t_user where id=${id}

   </select>

       注意,但是使用了@Param對單引數命名後我們就不能再在Mapper語句中通過${_parameter}來引用介面方法引數傳遞過來的單引數了,因為此時其已經被包裝為一個Map了,如果要通過_parameter來引用的話,我們應該使用${_parameter.param1}或${_parameter.varName},對於上面的示例來說就是${_parameter.param1}或${_parameter.id}。

       下面我們來看一個傳遞多引數的,假設我們有如下這樣的一個Mapper語句及對應的Mapper介面方法,這個Mapper介面方法接收兩個引數,第一個引數是用@Param註解指定了引數名為name,第二個引數是沒有使用註解的,具體如下。

   <!-- 當對應的介面方法傳遞多個引數時,可以不指定parameterType引數,就算指定了也沒用,因為這個時候預設是Map -->

   <select id="findByNameAndMobile"resultType="com.elim.learn.mybatis.model.User">

      select id,name,username,email,mobile from t_user where name='${name}' and mobile='${1}'

   </select>

   List<User> findByNameAndMobile(@Param("name") String name, Stringmobile);

     那我們的Mapper.xml檔案中的對應語句需要Mapper介面方法引數時有哪幾種方式呢?按照之前的規則,對於第一個方法引數name而言,因為使用了@Param指定了引數名name,所以我們可以在Mapper.xml檔案中通過變數name和param1來引用它,而第二個引數mobile是沒有使用@Param指定引數名的,則我們可以使用param2和引數相對位置“1”來引用它。如上面的示例中,我們就是通過第二個引數的相對位置“1”來引用的。如果我們把第二個引數改為${mobile}是引用不到,且系統會報如下錯誤,有興趣的朋友可以試一下。

org.apache.ibatis.binding.BindingException: Parameter 'mobile' not found. Available parameters are [1, name, param1, param2]

       一般情況下為了防止SQL注入問題,是不建議直接在where條件中使用${var}的形式訪問變數的,通常會用預編譯形式的#{var}。而${var}往往是用來傳遞一些非where條件的內容,比如排序資訊,具體用法請根據實際情況決定。