MyBatis3:SQL對映

五月的倉頡發表於2016-03-17

前言

前面學習了config.xml,下面就要進入MyBatis的核心SQL對映了,第一篇文章的時候,student.xml裡面是這麼寫的:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.xrq.StudentMapper">
    <select id="selectStudentById" parameterType="int" resultType="Student">
        <![CDATA[
            select * from student where studentId = #{id}
        ]]>
    </select>
</mapper>

基於這個xml,進行擴充套件和學習。

 

為什麼要使用<![CDATA[ ... ]]>?

上面的配置檔案中,大家一定注意到了一個細節,就是SQL語句用<![CDATA[ ... ]]>這對標籤包含起來了,那麼為什麼要這麼做呢?不妨把上面內容稍微修改一下:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.xrq.StudentMapper">
    <select id="selectStudentById" parameterType="int" resultType="Student">
        select * from student where studentId = #{id} or studentAge < 10 or studentAge > 20;
    </select>
</mapper>

當然這句SQL語句沒有任何含義,只是瞎寫的演示用而已,執行一下看一下結果:

Exception in thread "main" java.lang.ExceptionInInitializerError
    at com.xrq.test.MyBatisTest.main(MyBatisTest.java:9)
Caused by: org.apache.ibatis.exceptions.PersistenceException: 
### Error building SqlSession.
### The error may exist in student.xml
...

後面的異常資訊就不列了。按理說很正常的一句SQL語句,怎麼會報錯呢?仔細想來,錯誤的根本原因就是student.xml本身是一個xml檔案,它並不是專門為MyBatis服務的,它首先具有xml檔案的語法。因此,"< 10 or studentAge >"這段,會先被解析為xml的標籤,xml哪有這種形式的標籤的?所以當然報錯了。

所以,使用<![CDATA[ ... ]]>,它可以保證如論如何<![CDATA[ ... ]]>裡面的內容都會被解析成SQL語句。因此,建議每一條SQL語句都使用<![CDATA[ ... ]]>包含起來,這也是一種規避錯誤的做法。

 

select

SQL對映中有幾個頂級元素,其中最常見的四個就是insert、delete、update、select,分別對應於增、刪、改、查,下面先對於select元素進行學習。

1、多條件查詢查一個結果

前面的select語句只有一個條件,下面看一下多條件查詢如何做,首先是student.xml:

<select id="selectStudentByIdAndName" parameterType="Student" resultType="Student">
    <![CDATA[
        select * from student where studentId = #{studentId} and studentName = #{studentName};
    ]]>
</select>

注意這裡的parameter只能是一個實體類,然後引數要和實體類裡面定義的一樣,比如studentId、studentName,MyBatis將會自動查詢這些屬性,然後將它們的值傳遞到預處理語句的引數中去。

還有一個很重要的地方是,使用引數的時候使用了"#",另外還有一個符號"$"也可以引用引數,使用"#"最重要的作用就是防止SQL隱碼攻擊

接著看一下Java程式碼的寫法:

public Student selectStudentByIdAndName(int studentId, String studentName)
{
    SqlSession ss = ssf.openSession();
    Student student = null;
    try
    {
        student = ss.selectOne("com.xrq.StudentMapper.selectStudentByIdAndName", 
                new Student(studentId, studentName, 0, null));
    }
    finally
    {
        ss.close();
    }
    return student;
}

這裡selectOne方法的第二個引數傳入一個具體的Student進去就可以了,執行就不演示了,結果沒有問題。

2、查詢多個結果

上面的演示查詢的是一個結果,對於select來說,重要的當然是查詢多個結果,查詢多個結果有相應的寫法,看一下:

<select id="selectAll" parameterType="int" resultType="Student" flushCache="false" useCache="true"
    timeout="10000" fetchSize="100" statementType="PREPARED" resultSetType="FORWARD_ONLY">
    <![CDATA[
        select * from student where studentId > #{id};
    ]]>
</select>

這裡稍微玩了一些花樣,select裡面多放了一些屬性,設定了每條語句的作用細節,分別解釋下這些屬性的作用:

  • id----不說了,用來和namespace唯一確定一條引用的SQL語句
  • parameterType----引數型別,如果SQL語句中的動態引數只有一個,這個屬性可有可無
  • resultType----結果型別,注意如果返回結果是集合,應該是集合所包含的型別,而不是集合本身
  • flushCache----將其設定為true,無論語句什麼時候被呼叫,都會導致快取被清空,預設值為false
  • useCache----將其設定為true,將會導致本條語句的結果被快取,預設值為true
  • timeout----這個設定驅動程式等待資料庫返回請求結果,並丟擲異常事件的最大等待值,預設這個引數是不設定的(即由驅動自行處理)
  • fetchSize----這是設定驅動程式每次批量返回結果的行數,預設不設定(即由驅動自行處理)
  • statementType----STATEMENT、PREPARED或CALLABLE的一種,這會讓MyBatis選擇使用Statement、PreparedStatement或CallableStatement,預設值為PREPARED。這個相信大多數朋友自己寫JDBC的時候也只用過PreparedStatement
  • resultSetType----FORWARD_ONLY、SCROLL_SENSITIVE、SCROLL_INSENSITIVE中的一種,預設不設定(即由驅動自行處理)

xml寫完了,看一下如何寫Java程式,比較簡單,使用selectList方法即可:

public List<Student> selectStudentsById(int studentId)
{
    SqlSession ss = ssf.openSession();
    List<Student> list = null;
    try
    {
        list = ss.selectList("com.xrq.StudentMapper.selectAll", studentId);
    }
    finally
    {
        ss.close();
    }
    return list;
}

同樣,結果也就不演示了,查出來和資料庫內的資料相符。

3、使用resultMap來接收查詢結果

上面使用的是resultType來接收查詢結果,下面來看另外一種方式----使用resultMap,被MyBatis稱為MyBatis中最重要最強大的元素。

上面使用resultType的方式是有前提的,那就是假定列名和Java Bean中的屬性名存在對應關係,如果名稱不對應,也沒關係,可以採用類似下面的方式:

<select id="selectAll" parameterType="int" resultType="Student" flushCache="false" useCache="true"
    timeout="10000" fetchSize="100" statementType="PREPARED" resultSetType="FORWARD_ONLY">
    <![CDATA[
        select student_id as "studentId",student_name as "studentName",student_age as "studentAge",
      student_phone as "studentPhone" from student whehre restudentId > #{id};
]]> </select>

毫無疑問,這樣很繁瑣,我們可以採用resultMap來解決列名不匹配的問題,把2由resultType的形式改成resultMap的形式,Java程式碼不需要動:

<resultMap type="Student" id="studentResultMap">
    <id property="studentId" column="studentId" />
    <result property="studentName" column="studentName" />
    <result property="studentAge" column="studentAge" />
    <result property="studentPhone" column="studentPhone" />
</resultMap>

<select id="selectAll" parameterType="int" resultMap="studentResultMap" flushCache="false" useCache="true"
        timeout="10000" fetchSize="100" statementType="PREPARED" resultSetType="FORWARD_ONLY">
    <![CDATA[
        select * from student where studentId > #{id};
    ]]>
</select>

這樣就可以了,注意兩點:

1、resultMap定義中主鍵要使用id

2、resultMap和resultType不可以同時使用

對resultMap有很好的理解的話,許多複雜的對映問題就很好解決了。

 

insert

select看完了,接著看一下插入的方法。首先是student.xml的配置方法,由於插入資料涉及一個主鍵問題,我用的是MySQL,我試了一下使用以下兩種方式都可以:

<insert id="insertOneStudent" parameterType="Student">
    <![CDATA[
        insert into student    values(null, #{studentName}, #{studentAge}, #{studentPhone});
    ]]>    
</insert>
<insert id="insertOneStudent" parameterType="Student" useGeneratedKeys="true" keyProperty="studentId">
    <![CDATA[
        insert into student(studentName, studentAge, studentPhone) 
            values(#{studentName}, #{studentAge}, #{studentPhone});
    ]]>    
</insert>

前一種是MySQL本身的語法,主鍵欄位在insert的時候傳入null,後者是MyBatis支援的生成主鍵方式,useGeneratedKeys表示讓資料庫自動生成主鍵,keyProperty表示生成主鍵的列。

Java程式碼比較容易:

public void insertOneStudent(String studentName, int studentAge, String studentPhone)
{
    SqlSession ss = ssf.openSession();
    try
    {
        ss.insert("com.xrq.StudentMapper.insertOneStudent", 
            new Student(0, studentName, studentAge, studentPhone));
        ss.commit();
    }
    catch (Exception e)
    {
        ss.rollback();
    }
    finally
    {
        ss.close();
    }
}

還是一樣,insert方法比如傳入Student的實體類,如果insertOneStudent方法要傳入的引數比較多的話,建議不要把每個屬性單獨作為形參,而是直接傳入一個Student物件,這樣也比較符合物件導向的程式設計思想。

然後還有一個問題,這個我回頭還得再看一下。照理說設定了transactionManager的type為JDBC,對事物的處理應該和底層JDBC是一致的,JDBC預設事物是自動提交的,這裡事物卻得手動提交,拋異常了得手動回滾才行。

 

修改、刪除元素

修改和刪除元素比較類似,就看一下student.xml檔案怎麼寫,Java程式碼就不列了,首先是修改元素:

<update id="updateStudentAgeById" parameterType="Student">
    <![CDATA[
        update student set studentAge = #{studentAge} where 
            studentId = #{studentId};
    ]]>    
</update>

接著是刪除元素:

<delete id="deleteStudentById" parameterType="int">
    <![CDATA[
        delete from student where studentId = #{studentId};
    ]]>    
</delete>

這裡我又發現一個問題,記錄一下,update的時候Java程式碼是這麼寫的:

public void updateStudentAgeById(int studentId, int studentAge)
{
    SqlSession ss = ssf.openSession();
    try
    {
        ss.update("com.xrq.StudentMapper.updateStudentAgeById",
                new Student(studentId, null, studentAge, null));
        ss.commit();
    }
    catch (Exception e)
    {
        ss.rollback();
    }
    finally
    {
        ss.close();
    }
}

studentId和studentAge必須是這個順序,互換位置就更新不了學生的年齡了,這個是為什麼我還要後面去研究一下,也可能是寫程式碼的問題。

 

SQL

SQL可以用來定義可重用的SQL程式碼段,可以包含在其他語句中,比如我把上面的插入換一下,先定義一個SQL:

<sql id="insertColumns">
    studentName, studentAge, studentPhone
</sql>

然後在修改一下insert:

<insert id="insertOneStudent" parameterType="Student" useGeneratedKeys="true" keyProperty="studentId">
    insert into student(<include refid="insertColumns" />) 
        values(#{studentName}, #{studentAge}, #{studentPhone});
</insert>

注意這裡要把"<![CDATA[ ... ]]>"給去掉,否則"<"和">"就被當成SQL裡面的小於和大於了,因此使用SQL的寫法有一定限制,使用前要注意一下避免出錯。

 

相關文章