2021-1-02----ssm之第2章 SQL對映檔案

我卡在門縫裡了>_<發表於2021-01-02


❖ 掌握通過SQL對映檔案進行增、刪、改、查的方法

❖ 掌握引數的使用方法

❖ 掌握resultMap

❖ 瞭解Cache的使用方法

任務1:實現條件查詢任務

2:實現增刪改操作任務

3:實現高階結果對映任務

4:配置resultMap自動對映級別和MyBatis快取

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-Vt2c7K3H-1609551381895)(C:\Users\pcy\AppData\Roaming\Typora\typora-user-images\image-20201216070352412.png)]

任務1 實現條件查詢

關鍵步驟如下。

➢ 使用select元素實現根據使用者名稱模糊查詢使用者列表資訊。

➢ 使用select元素實現多條件查詢使用者列表資訊。

➢ 使用resultMap元素對映自定義查詢結果。

2.1.1 SQL對映檔案

➢ mapper:對映檔案的根元素節點,只有一個屬性namespace(名稱空間),其作用如下。♦ 用於區分不同的mapper,全域性唯一。♦ 繫結DAO介面,即面向介面程式設計。當namespace繫結某一介面之後,可以不用寫該介面的實現類,MyBatis會通過介面的完整限定名查詢到對應的mapper配置來執行SQL語句。因此namespace的命名必須跟介面同名。

➢ cache:配置給定名稱空間的快取。

➢ cache-ref:從其他名稱空間引用快取配置。

➢ resultMap:用來描述資料庫結果集和物件的對應關係。

➢ sql:可以重用的SQL塊,也可以被其他語句引用。

➢ insert:對映插入語句。

➢ update:對映更新語句。

➢ delete:對映刪除語句。

➢ select:對映查詢語句。

注意

MyBatis的SQL對映檔案中mapper元素的namespace屬性有如下要求:(1)namespace的命名必須跟某個DAO介面同名,同屬於DAO層,故在程式碼結構上,對映檔案與該DAO介面應放置在同一package下(如cn.smbms.dao. user),並且習慣上都是以Mapper結尾(如UserMapper.java、UserMapper.xml)。

(2)在不同的mapper檔案中,子元素的id可以相同,MyBatis通過namespace和子元素的id聯合區分。介面中的方法與對映檔案中SQL語句id應一一對應。

2.1.2 單條件查詢

與查詢對應的select元素是使用MyBatis時最常用的。在上一章,我們實現了對使用者表的簡單查詢,現在升級需求,增加查詢條件,那麼如何實現帶引數和返回複雜型別的查詢?這就需要先詳細瞭解select元素的屬性。以根據使用者名稱模糊查詢來獲取使用者列表資訊為例,

➢ id:名稱空間中唯一的識別符號,可以被用來引用這條語句。
➢ parameterType:表示查詢語句傳入引數的型別的完全限定名或別名。它支援基礎資料型別和複雜資料型別。在示例1中使用的是基礎資料型別“string”,這是一個別名,代表String,屬於一個內建的型別別名。對於普通的Java型別,有許多內建的型別別名,並且它們對大小寫不敏感。
除了內建的型別別名外,還可以為自定義的類設定別名。
別名(typeAliases)在mybatis-config.xml中的設定,在對映檔案中可直接使用別名,以減少配置檔案的程式碼。
➢ resultType:查詢語句返回結果型別的完全限定名或別名。別名的使用方式與parameterType相同。
<select id="getUserListByUserName" resultType="User" parameterType="String">
   select * from smbms_user where userName like CONCAT ('%',#{userName},'%')
</select>

這是一個id為getUserListByUserName的對映語句,引數型別為string,返回結果的型別是User。為了使資料庫查詢的結果和返回型別中的屬效能夠自動匹配、以便於開發,對於MySQL資料庫和JavaBean都會採用同一套命名規則,即Java命名駝峰規則,這樣就不需要再做對映(注:如果資料庫表的欄位名和屬性名不一致則需要手動對映)。引數的傳遞使用#{引數名},它告訴MyBatis生成PreparedStatement引數。

@Test
public void testGetUserListByUserName(){
   SqlSession sqlSession = null;
   List<User> userList = new ArrayList<User>();
   try {
      sqlSession = MyBatisUtil.createSqlSession();
      
      //第一種方式:呼叫selectList方法執行查詢操作
      //userList = sqlSession.selectList("cn.smbms.dao.user.UserMapper.getUserListByUserName","趙");
      
      //第二種方式:呼叫getMapper(Mapper.class)執行dao介面方法來實現對資料庫的查詢操作
      userList = sqlSession.getMapper(UserMapper.class).getUserListByUserName("趙");
      
   } catch (Exception e) {
      // TODO: handle exception
      e.printStackTrace();
   }finally{
      MyBatisUtil.closeSqlSession(sqlSession);
   }
   for(User user: userList){
      logger.debug("testGetUserListByUserName userCode: " + user.getUserCode() + " and userName: " + user.getUserName());
   }
}

2.1.3 多條件查詢

通過一個條件對使用者表進行查詢操作,但在實際應用中,資料查詢會有多種條件,結果也會有各種型別

查詢條件包括:使用者名稱(模糊查詢)、使用者角色。那對於多條件查詢,該如何實現?我們可以考慮將查詢條件封裝成物件進行入參,改造UserMapper.java

public List<User> getUserList(User user);
<!-- 查詢使用者列表(引數:物件入參) -->
<select id="getUserList" resultType="User" parameterType="User">
   select * from smbms_user 
      where userName like CONCAT ('%',#{userName},'%') and userRole = #{userRole}
</select>
@Test
public void testGetUserList(){
   SqlSession sqlSession = null;
   List<User> userList = new ArrayList<User>();
   try {
      sqlSession = MyBatisUtil.createSqlSession();
      User user = new User();
      user.setUserName("趙");
      user.setUserRole(3);
      //第一種方式:呼叫selectList方法執行查詢操作
      //userList = sqlSession.selectList("cn.smbms.dao.user.UserMapper.getUserList",user);
      //第二種方式:呼叫getMapper(Mapper.class)執行dao介面方法來實現對資料庫的查詢操作
      userList = sqlSession.getMapper(UserMapper.class).getUserList(user);
   } catch (Exception e) {
      // TODO: handle exception
      e.printStackTrace();
   }finally{
      MyBatisUtil.closeSqlSession(sqlSession);
   }
   for(User user: userList){
      logger.debug("testGetUserList userCode: " + user.getUserCode() + " and userName: " + user.getUserName());
   }
}

parameterType使用了複雜資料型別,把條件引數封裝成User物件進行入參。對User物件中userName和userRole兩個屬性分別進行賦值,在對映的查詢語句中設定parameterType為User型別,傳入引數分別使用#{userName}和#{userRole}來表示,即#{屬性名}(引數物件中的屬性名)。

parameterType支援的複雜資料型別除了JavaBean之外,還有Map型別,改造上一示例,把使用者名稱和使用者角色封裝成Map物件進行入參,測試類UserMapperTest.java的關鍵程式碼如示例5所示。

public List<User> getUserListByMap(Map<String, String> userMap);
改造UserMapper.xml,將parameterType設定為Map, SQL語句中的引數值使用#{uName}和#{uRole}來表示,即#{Map的key}
<select id="getUserListByMap" resultType="User" parameterType="Map">
   select * from smbms_user 
      where userName like CONCAT ('%',#{uName},'%') and userRole = #{uRole}
</select>
這種做法更加靈活,不管是什麼型別的引數、有多少個引數,我們都可以把它們封裝成Map資料結構進行入參,通過Map的key即可獲取傳入的值。
@Test
public void testGetUserListByMap(){
   SqlSession sqlSession = null;
   List<User> userList = new ArrayList<User>();
   try {
      sqlSession = MyBatisUtil.createSqlSession();
      Map<String, String> userMap = new HashMap<String, String>();
      userMap.put("uName", "趙");
      userMap.put("uRole", "3");
      //第一種方式:呼叫selectList方法執行查詢操作
      //userList = sqlSession.selectList("cn.smbms.dao.user.UserMapper.getUserListByMap",userMap);
      //第二種方式:呼叫getMapper(Mapper.class)執行dao介面方法來實現對資料庫的查詢操作
      userList = sqlSession.getMapper(UserMapper.class).getUserListByMap(userMap);
      
   } catch (Exception e) {
      // TODO: handle exception
      e.printStackTrace();
   }finally{
      MyBatisUtil.closeSqlSession(sqlSession);
   }
   for(User user: userList){
      logger.debug("testGetUserListByMap userCode: " + user.getUserCode() + " and userName: " + user.getUserName());
   }
}

注意

MyBatis傳入引數型別可以是Java基礎資料型別,但是隻適用於一個引數的情況,通過#{引數名}即可獲取傳入的值。若是多引數入參,則需要複雜資料型別來支援,包括Java實體類、Map,通過#{屬性名}或#{Map的key}來獲取傳入的引數值。

2.1.4 自定義查詢結果對映

(1)修改POJO:User.java增加userRoleName屬性,並修改查詢使用者列表的SQL語句,對使用者表(smbms_user)和角色表(smbms_role)進行聯表查詢,使用resultType自動對映。

(2)通過resultMap對映自定義結果。

注意

MyBatis中使用resultType做自動對映時,要注意欄位名和POJO的屬性名必須一致。若不一致,則需要給欄位起別名,保證別名與屬性名一致。

查詢出使用者資訊列表所必需的顯示欄位(包括使用者編碼、使用者名稱稱、性別、年齡、電話、使用者角色等欄位資訊),注意使用者角色要顯示角色名稱而不是角色id。首先需要在User類中加入userRoleName屬性:private StringuserRoleName,及其相應的getter和setter方法。然後修改UserMapper介面中的查詢使用者列表的getUserList()方法,在UserMapper. xml中,修改getUserList的SQL對映語句,並修改select的resultType屬性為resultMap,其屬性值為userList,

public List<User> getUserListByMap(Map<String, String> userMap);
通過getUserList的SQL語句,進行聯表查詢,可得到使用者對應角色的中文名稱。接下來在UserMapper.xml中增加id為userList的resultMap元素節點
<resultMap type="User" id="userList">
   <result property="id" column="id"/>
   <result property="userCode" column="userCode"/>
   <result property="userName" column="userName"/>
   <result property="phone" column="phone"/>
   <result property="birthday" column="birthday"/>
   <result property="gender" column="gender"/>
   <result property="userRole" column="userRole"/>
   <result property="userRoleName" column="roleName"/>
</resultMap>


<!-- 查詢使用者列表(引數:物件入參) -->
<select id="getUserList" resultMap="userList" parameterType="User">
   select u.*,r.roleName from smbms_user u,smbms_role r
      where u.userName like CONCAT ('%',#{userName},'%') 
            and u.userRole = #{userRole} and u.userRole = r.id
</select>

resultMap元素用來描述如何將結果集對映到Java物件,此處使用resultMap對列表展示所需的必要欄位進行自由對映,特別是當資料庫的欄位名和POJO中的屬性名不一致的情況下,比如角色名稱,欄位名column是roleName,而User物件的屬性名為userRoleName,此時就需要做對映。

resultMap元素的屬性值和子節點:

➢ id屬性:唯一標識,此id值用於對select元素resultMap屬性的引用。

➢ type屬性:表示該resultMap的對映結果型別。

➢ result子節點:用於標識一些簡單屬性,其中column屬性表示從資料庫中查詢的欄位名,property則表示查詢出來的欄位對應的值賦給實體物件的哪個屬性。

最後在測試類中進行相關欄位的輸出,展示列表(使用者編碼、使用者名稱稱、性別、年齡、電話、使用者角色)。注意:使用者角色不再是角色id,輸出的是角色名稱。MyBatis中在對查詢進行select對映的時候,返回型別可以用resultType,也可以用resultMap。

1.resultType

resultType直接表示返回型別,包括基礎資料型別和複雜資料型別。

2.resultMap

resultMap則是對外部resultMap定義的引用,對應外部resultMap的id,表示返回結果對映到哪一個resultMap上。它的應用場景一般是:資料庫欄位資訊與物件屬性不一致或者需要做複雜的聯合查詢,以便自由控制對映結果。

3.resultType和resultMap的關聯

在MyBatis進行查詢對映的時候,查詢出來的每個欄位值都放在一個對應的Map裡面,其中鍵是欄位名,值則是其對應的值。當select元素提供的返回型別屬性是resultType的時候,MyBatis會將Map裡面的鍵值對取出賦給resultType所指定的物件對應的屬性(即呼叫對應的物件裡的屬性的setter方法進行填充)。正因為如此,當使用resultType的時候,直接在後臺就能接收到其相應的物件屬性值。由此可看出,其實MyBatis的每個查詢對映的返回型別都是resultMap,只是當我們提供的返回型別屬性是resultType的時候,MyBatis會自動把對應的值賦給resultType所指定物件的屬性;而當我們提供的返回型別屬性是resultMap的時候,因為Map不能很好地表示領域模型,就需要通過進一步的定義把它轉化為對應的實體物件。

注意

在MyBatis的select元素中,resultType和resultMap本質上是一樣的,都是Map資料結構。但需要明確一點:resultType屬性和resultMap屬性絕對不能同時存在,只能二者選其一使用。

4.resultMap的自動對映級別

在上面的示例中,選擇部分欄位進行resultMap對映,我們希望沒有對映的欄位不能在後臺查詢並輸出,即使SQL語句中是查詢所有欄位(select * from……)。因為我們使用resultMap也是為了自由靈活地控制對映結果,達到只對關心的屬性進行賦值填充的目的。修改測試類(UserMapperTest.java)的輸出項

@Test
public void testGetUserList(){
   SqlSession sqlSession = null;
   List<User> userList = new ArrayList<User>();
   try {
      sqlSession = MyBatisUtil.createSqlSession();
      User user = new User();
      user.setUserName("趙");
      user.setUserRole(3);
      userList = sqlSession.getMapper(UserMapper.class).getUserList(user);
   } catch (Exception e) {
      // TODO: handle exception
      e.printStackTrace();
   }finally{
      MyBatisUtil.closeSqlSession(sqlSession);
   }
   
   /**
    * 若設定resultMap的自動對映級別為NONE,
    * 那麼沒有進行對映匹配的屬性(比如:address等)則輸出為null
    * 若不設定resultMap的自動對映級別,則不管是否進行了對映,所有的屬性值均可輸出
    */
   for(User user: userList){
      logger.debug("testGetUserList userCode: " + user.getUserCode() + 
               " and userName: " + user.getUserName() + 
               " and userRole: " + user.getUserRole() + 
               " and userRoleName: " + user.getUserRoleName() +
               " and age: " + user.getAge() +
               " and address: " + user.getAddress());
   }
}

在該示例程式碼中,對比之前設定的resultMap對映的屬性,增加了address和age兩個屬性值的輸出。觀察輸出結果,發現address和age的值均可正常輸出

為何沒有在resultMap中做對映關聯的age和address卻能正常輸出結果?若需求為沒有在resultMap內對映的欄位不能獲取,那麼又該如何實現?

這跟resultMap的自動對映級別有關,預設的對映級別為PARTIAL。要滿足新需求,則需要設定MyBatis對於resultMap的自動對映級別(autoMappingBehavior)為NONE,即禁止自動匹配。修改mybatis-config.xml

<settings>
   <!-- 配置mybatis的log實現為LOG4J -->
   <setting name="logImpl" value="LOG4J" />
   <!-- 設定resultMap的自動對映級別為NONE(禁止自動匹配) -->
   <setting name="autoMappingBehavior" value="NONE" />
</settings>

增加以上的設定之後,再進行結果的輸出,發現address屬性值為null,即該屬性沒有進行自動setter賦值,但是age的屬性值仍為30,並非為空。這是因為age屬性值並非直接取自資料表,而是在getAge()方法中通過birthday屬性計算得出,只要載入了birthday就可以計算出age

注意

在MyBatis中,使用resultMap能夠進行自動對映匹配的前提是欄位名和屬性名必須一致,在預設對映級別(PARTIAL)情況下,若欄位名和屬性名一致,即使沒有做屬性名和欄位名的匹配,也可以在後臺獲取到未匹配過的屬性值;若欄位名和屬性名不一致,且在resultMap裡沒有做對映,那麼就無法在後臺獲取並輸出。

任務2 實現增刪改操作

關鍵步驟如下。

➢ 使用insert元素實現使用者表的增加。

➢ 使用update元素實現根據使用者id修改使用者資訊。

➢ 使用@Param註解實現多引數入參。

➢ 使用delete元素實現根據使用者id刪除使用者。

2.2.1 增加操作

MyBatis實現增加操作,使用的是insert元素來對映插入語句。具體的用法很簡單,下面通過使用者表的增加操作示例來演示具體用法,首先在UserMapper介面裡增加add()方法。

public int add(User user);

要插入的User物件作為入參,返回值為int型別,即返回執行SQL語句影響的行數。修改UserMapper.xml,增加插入語句

<insert id="add" parameterType="User">
   insert into smbms_user (userCode,userName,userPassword,gender,birthday,phone,
                     address,userRole,createdBy,creationDate) 
         values (#{userCode},#{userName},#{userPassword},#{gender},#{birthday},#{phone},
         #{address},#{userRole},#{createdBy},#{creationDate})
</insert>

insert元素的屬性:

➢ id:與select元素的id一樣,是名稱空間中唯一的識別符號,可以被用來引用該條語句。

➢ parameterType:與select元素的parameterType一樣,是傳入引數的型別的完全限定名或別名。別名的含義和用法見select元素中的解釋。

public int add(User user);

注意

對於增刪改(insert、update、delete)這類資料庫更新操作,需要注意兩點:(1)該型別的操作本身預設返回執行SQL語句影響的行數,所以DAO層的介面方法的返回值一般設定為int型別。最好不要返回boolean型別。

(2)insert、update、delete元素中均沒有resultType屬性,只有查詢操作需要對返回結果的型別(resultType/resultMap)進行相應的指定。

接下來修改測試類UserMapperTest.java,增加testAdd()方法,進行插入資料測試,並開啟事務控制,模擬異常,若發生異常則回滾以測試事務。

@Test
public void testAdd(){
   logger.debug("testAdd !===================");
   SqlSession sqlSession = null;
   int count = 0;
   try {
      sqlSession = MyBatisUtil.createSqlSession();
      User user = new User();
      user.setUserCode("test001");
      user.setUserName("測試使用者001");
      user.setUserPassword("1234567");
      Date birthday = new SimpleDateFormat("yyyy-MM-dd").parse("1984-12-12");
      user.setBirthday(birthday);
      user.setCreationDate(new Date());
      user.setAddress("地址測試");
      user.setGender(1);
      user.setPhone("13688783697");
      user.setUserRole(1);
      user.setCreatedBy(1);
      user.setCreationDate(new Date());
      count = sqlSession.getMapper(UserMapper.class).add(user);
      //模擬異常,進行回滾
      //int i = 2/0;
      sqlSession.commit();
   } catch (Exception e) {
      // TODO: handle exception
      e.printStackTrace();
      sqlSession.rollback();
      count = 0;
   }finally{
      MyBatisUtil.closeSqlSession(sqlSession);
   }
   logger.debug("testAdd count: " + count);
}

那麼在此處測試中,當sqlSession執行add()方法之後需要進行commit,完成資料的插入操作。若在執行的過程中丟擲異常,那麼就必須在catch中進行回滾,以此來保證資料的一致性,同時設定count為0。

2.2.2 修改操作

MyBatis實現修改操作,使用的是update元素來對映修改語句。具體的用法與insert類似,下面通過根據使用者id修改使用者資訊的操作例項來演示具體用法。首先在UserMapper介面裡增加modify()方法。

public int modify(User user);
要修改的User物件作為入參,返回值為int型別,
即返回執行SQL語句影響的行數。修改UserMapper.xml,增加修改語句,
<update id="modify" parameterType="User">
   update smbms_user set userCode=#{userCode},userName=#{userName},userPassword=#{userPassword},
               gender=#{gender},birthday=#{birthday},phone=#{phone},address=#{address},
               userRole=#{userRole},modifyBy=#{modifyBy},modifyDate=#{modifyDate}
         where id = #{id}
</update>

update元素的屬性id和parameterType的含義和用法等同於insert元素中的屬性用法,此處不再贅述。另外,由於是修改操作,因此更新的欄位中需更新modifyBy和modifyDate,而不需要更新createBy和creationDate。

接下來修改測試類UserMapperTest.java,增加testModify方法進行修改資料測試,並開啟事務控制,模擬異常,若發生異常則回滾以測試事務。

@Test
public void testModify(){
   logger.debug("testModify !===================");
   SqlSession sqlSession = null;
   int count = 0;
   try {
      User user = new User();
      user.setId(25);
      user.setUserCode("testmodify");
      user.setUserName("測試使用者修改");
      user.setUserPassword("0000000");
      Date birthday = new SimpleDateFormat("yyyy-MM-dd").parse("1980-10-10");
      user.setBirthday(birthday);
      user.setCreationDate(new Date());
      user.setAddress("地址測試修改");
      user.setGender(2);
      user.setPhone("13600002222");
      user.setUserRole(2);
      user.setModifyBy(1);
      user.setModifyDate(new Date());
      sqlSession = MyBatisUtil.createSqlSession();
      count = sqlSession.getMapper(UserMapper.class).modify(user);
      //模擬異常,進行回滾
      //int i = 2/0;
      sqlSession.commit();
   } catch (Exception e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
      sqlSession.rollback();
      count = 0;
   }finally{
      MyBatisUtil.closeSqlSession(sqlSession);
   }
   logger.debug("testModify count: " + count);
}

2.2.3 多引數入參

實現的是根據使用者id修改使用者資訊操作,超市訂單管理系統還有一個需求:修改個人密碼。此需求也是修改操作,但是傳入引數有兩個:使用者id和新密碼。若按照之前封裝成User物件的方式進行引數傳遞,並不是很合適,這裡可以用更靈活的方式處理,即直接進行多引數入參,程式碼可讀性高,可清晰地看出這個介面方法所需的引數是什麼。

具體示例程式碼如下,修改UserMapper介面,增加修改個人密碼的方法,當方法引數有多個時,每個引數前都需增加@Param註解:

public int updatePwd(@Param("id")Integer id, @Param("userPassword")String pwd);

使用註解@Param來傳入多個引數,如@Param(“userPassword”)String pwd,相當於將該引數pwd重新命名為userPassword,在對映的SQL中需要使用#{註解名稱},如#{userPassword}。

下面繼續修改UserMapper.xml,增加id為updatePwd的SQL對映

<!-- 修改當前使用者密碼 -->
<update id="updatePwd">
   update smbms_user set userPassword=#{userPassword} where id=#{id}
</update>

若不使用@Param註解,則會報錯,報錯資訊類似於Parameter ’引數名’ not found。探究原因,需要深入MyBatis原始碼,MyBatis的引數型別為Map,如果使用@Param註解引數,那麼就會記錄指定的引數名為key;如果引數前沒有加@Param,那麼就會使用“param”+它的序號作為Map的key。所以在進行多引數入參時,如果沒有使用@Param指定引數,那麼在對映的SQL語句中將獲取不到#{引數名},從而報錯。

最後修改測試類UserMapperTest.java,增加testUpdatePwd方法進行個人密碼修改測試

@Test
public void testUpdatePwd() {
   logger.debug("testUpdatePwd !===================");
   SqlSession sqlSession = null;
   String pwd = "8888888";
   Integer id = 1;
   int count = 0;
   try {
      sqlSession = MyBatisUtil.createSqlSession();
      count = sqlSession.getMapper(UserMapper.class).updatePwd(id, pwd);
      sqlSession.commit();
   } catch (Exception e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
      sqlSession.rollback();
      count = 0;
   }finally{
      MyBatisUtil.closeSqlSession(sqlSession);
   }
   logger.debug("testUpdatePwd count: " + count);
}

在該測試方法中,不需要再封裝User物件,直接進行兩個引數的入參即可,清晰明瞭。

經驗

在MyBatis中使用引數入參,何時需要封裝成物件入參,何時又需要使用多引數入參?

一般情況下,超過4個以上的引數最好封裝成物件入參(特別是在常規的增加和修改操作時,欄位較多,封裝成物件比較方便)。

對於引數固定的業務方法,最好使用多引數入參。因為這種方法比較靈活,程式碼的可讀性高,可以清晰地看出介面方法中所需的引數是什麼。並且對於固定的介面方法,引數一般是固定的,所以直接採用多引數入參,無需封裝物件。比如修改個人密碼、根據使用者id刪除使用者、根據使用者id檢視使用者明細,都可以採取這種入參方式。

需要注意的是,當引數為基礎資料型別時,不管是多引數入參,還是單獨的一個引數入參,都需要使用@Param註解來進行引數的傳遞。

2.2.4 刪除操作

MyBatis實現刪除操作是使用delete元素來對映刪除語句。具體的用法與insert、update類似,下面通過根據使用者id刪除使用者的操作示例來演示具體用法,首先在UserMapper介面中增加delete方法:

public int deleteUserById(@Param(“id”)Integer delId);

引數:delId(使用者id),使用@Param註解來指定引數名為id,返回值為int型別,即返回執行SQL語句影響的行數。

修改UserMapper.xml,增加刪除語句

<!-- 根據userId刪除使用者資訊 -->
<delete id="deleteUserById" parameterType="Integer">
   delete from smbms_user where id=#{id}
</delete>

接下來修改測試類UserMapperTest.java,增加testDeleteUserById方法,進行刪除資料測試,並開啟事務控制,模擬異常,若發生異常則回滾以測試事務。

Test
public void testDeleteUserById() {
   logger.debug("testDeleteUserById !===================");
   SqlSession sqlSession = null;
   Integer delId = 25;
   int count = 0;
   try {
      sqlSession = MyBatisUtil.createSqlSession();
      count = sqlSession.getMapper(UserMapper.class).deleteUserById(delId);
      sqlSession.commit();
   } catch (Exception e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
      sqlSession.rollback();
      count = 0;
   }finally{
      MyBatisUtil.closeSqlSession(sqlSession);
   }
   logger.debug("testDeleteUserById count: " + count);
}

任務3 實現高階結果對映

➢ 使用association實現根據使用者角色id獲取該角色下的使用者列表。

➢ 使用collection實現獲取指定使用者的相關資訊和地址列表。

2.3.1 resultMap的配置

1.屬性

➢ id:resultMap的唯一標識。

➢ type:表示該resultMap的對映結果型別(通常是Java實體類)。

2.子節點

➢ id:一般對應資料庫中該行的主鍵id,設定此項可以提升MyBatis效能。

➢ result:對映到JavaBean的某個“簡單型別”屬性,如基礎資料型別、包裝類等。

子節點id和result均可實現最基本的結果集對映,將列對映到簡單資料型別的屬性。這兩者的唯一不同是:在比較物件例項時id將作為結果集的標識屬性。這有助於提高總體效能,特別是在應用快取和巢狀結果對映的時候。而若要實現高階結果對映,就需要學習下面兩個配置項:association和collection。

2.3.2 使用association處理一對一關聯關係

association:對映到JavaBean的某個“複雜型別”屬性,比如JavaBean類,即JavaBean內部巢狀一個複雜資料型別(JavaBean)屬性,這種情況就屬於複雜型別的關聯。需要注意:association僅處理一對一的關聯關係。

下面通過一個示例來演示association的具體應用,示例需求:根據使用者角色id獲取該角色下的使用者列表。

首先修改User類,增加角色屬性(Role role),並增加其相應的getter和setter方法;註釋掉使用者角色名稱屬性(StringuserRoleName),並註釋掉其getter和setter方法

package cn.smbms.pojo;

import java.util.Date;

public class User {
   private Integer id; //id 
   private String userCode; //使用者編碼
   private String userName; //使用者名稱稱
   private String userPassword; //使用者密碼
   private Integer gender;  //性別
   private Date birthday;  //出生日期
   private String phone;   //電話
   private String address; //地址
   private Integer userRole;    //使用者角色ID
   private Integer createdBy;   //建立者
   private Date creationDate; //建立時間
   private Integer modifyBy;     //更新者
   private Date modifyDate;   //更新時間
   
   private Integer age;//年齡
   //private String userRoleName; //使用者角色名稱
   
   //association
   private Role role; //使用者角色
   
   
   public Role getRole() {
      return role;
   }
   public void setRole(Role role) {
      this.role = role;
   }

   public Integer getAge() {
      /*long time = System.currentTimeMillis()-birthday.getTime();
      Integer age = Long.valueOf(time/365/24/60/60/1000).IntegerValue();*/
      Date date = new Date();
      Integer age = date.getYear()-birthday.getYear();
      return age;
   }
   
/* public String getUserRoleName() {
      return userRoleName;
   }
   public void setUserRoleName(String userRoleName) {
      this.userRoleName = userRoleName;
   }*/
   public Integer getId() {
      return id;
   }
   public void setId(Integer id) {
      this.id = id;
   }
   public String getUserCode() {
      return userCode;
   }
   public void setUserCode(String userCode) {
      this.userCode = userCode;
   }
   public String getUserName() {
      return userName;
   }
   public void setUserName(String userName) {
      this.userName = userName;
   }
   public String getUserPassword() {
      return userPassword;
   }
   public void setUserPassword(String userPassword) {
      this.userPassword = userPassword;
   }
   public Integer getGender() {
      return gender;
   }
   public void setGender(Integer gender) {
      this.gender = gender;
   }
   public Date getBirthday() {
      return birthday;
   }
   public void setBirthday(Date birthday) {
      this.birthday = birthday;
   }
   public String getPhone() {
      return phone;
   }
   public void setPhone(String phone) {
      this.phone = phone;
   }
   public String getAddress() {
      return address;
   }
   public void setAddress(String address) {
      this.address = address;
   }
   public Integer getUserRole() {
      return userRole;
   }
   public void setUserRole(Integer userRole) {
      this.userRole = userRole;
   }
   public Integer getCreatedBy() {
      return createdBy;
   }
   public void setCreatedBy(Integer createdBy) {
      this.createdBy = createdBy;
   }
   public Date getCreationDate() {
      return creationDate;
   }
   public void setCreationDate(Date creationDate) {
      this.creationDate = creationDate;
   }
   public Integer getModifyBy() {
      return modifyBy;
   }
   public void setModifyBy(Integer modifyBy) {
      this.modifyBy = modifyBy;
   }
   public Date getModifyDate() {
      return modifyDate;
   }
   public void setModifyDate(Date modifyDate) {
      this.modifyDate = modifyDate;
   }
}

通過以上改造,我們的JavaBean:User物件內部巢狀了一個複雜資料型別的屬性:role。接下來在UserMapper介面裡增加根據角色id獲取使用者列表的方法,

public List<User> getUserListByRoleId(@Param("userRole")Integer roleId);

修改對應UserMapper.xml,增加getUserListByRoleId,該select查詢語句返回型別為resultMap,外部引用的resultMap的型別為User。由於User物件內嵌JavaBean物件(Role),因此需要使用association來實現結果對映。

<!-- 根據roleId獲取使用者列表 association start-->
<resultMap type="User" id="userRoleResult">
   <id property="id" column="id"/>
   <result property="userCode" column="userCode" />
   <result property="userName" column="userName" />
   <result property="userRole" column="userRole" />
   <association property="role" javaType="Role" >
      <id property="id" column="r_id"/>
      <result property="roleCode" column="roleCode"/>
      <result property="roleName" column="roleName"/>
   </association> 
</resultMap>

<select id="getUserListByRoleId" parameterType="Integer" resultMap="userRoleResult">
   select u.*,r.id as r_id,r.roleCode,r.roleName from smbms_user u,smbms_role r 
         where u.userRole = #{userRole} and u.userRole = r.id
</select>

從上述程式碼來簡單分析association的屬性。

➢ javaType:完整Java類名或者別名。若對映到一個JavaBean,則MyBatis會自行檢測到其型別;若對映到一個HashMap,則應該明確指定javaType,來確保所需行為。此處為Role。

➢ property:對映資料庫列的實體物件的屬性。此處為在User裡定義的屬性:role。association的子元素如下。

➢ result

♦ property:對映資料庫列的實體物件的屬性。此處為Role的屬性。

♦ column:資料庫列名或別名。在做結果對映的過程中,要確保所有的列名都是唯一且無歧義的。

注意

id子元素在巢狀結果對映中扮演了非常重要的角色,應該指定一個或者多個屬性來唯一標識這個結果集。實際上,即便沒有指定id, MyBatis也會工作,但是會導致嚴重的效能開銷,所以選擇儘量少的屬性來唯一標識結果,使用主鍵或者聯合主鍵均可。

最後修改測試類UserMapperTest.java,增加測試方法

@Test
public void getUserListByRoleIdTest(){
   SqlSession sqlSession = null;
   List<User> userList = new ArrayList<User>();
   Integer roleId = 3;
   try {
      sqlSession = MyBatisUtil.createSqlSession();
      userList = sqlSession.getMapper(UserMapper.class).getUserListByRoleId(roleId);
   } catch (Exception e) {
      // TODO: handle exception
      e.printStackTrace();
   }finally{
      MyBatisUtil.closeSqlSession(sqlSession);
   }
   
   logger.debug("getUserListByRoleIdTest userList.size : " + userList.size());
   for(User user:userList){
      logger.debug("userList =====> userName: " + user.getUserName() + ", Role: " 
                     + user.getRole().getId() + " --- " + user.getRole().getRoleCode() 
                     +" --- " + user.getRole().getRoleName());
   }
}

在測試方法中呼叫getUserListByRoleId()方法獲取userList,並進行結果輸出,關鍵是對映的使用者角色相關資訊。

通過上面的示例,我們瞭解了association的基本用法以及適用場景,現在再思考一個問題:上一個例子中使用“userRoleResult”聯合一個association的結果對映來載入User例項,那麼association的role結果對映是否可複用?

答案是肯定的,association提供了另一個屬性:resultMap。通過這個屬性可以擴充套件一個resultMap來進行聯合對映,這樣就可以使role結果對映重複使用。當然,若不需要複用,也可按照之前的寫法,直接巢狀這個聯合結果對映,如何使用要根據具體業務而定。下面就來改造剛才的示例,使用resultMap完成association的role對映結果的複用。

修改UserMapper.xml,增加resultMap來完成role的結果對映,association增加屬性resultMap來引用外部的“roleResult”

<!-- 根據roleId獲取使用者列表 association start-->
<resultMap type="User" id="userRoleResult">
   <id property="id" column="id"/>
   <result property="userCode" column="userCode" />
   <result property="userName" column="userName" />
   <result property="userRole" column="userRole" />
   <association property="role" javaType="Role" resultMap="roleResult"/>
</resultMap>

<resultMap type="Role" id="roleResult">
   <id property="id" column="r_id"/>
   <result property="roleCode" column="roleCode"/>
   <result property="roleName" column="roleName"/>
</resultMap>

<select id="getUserListByRoleId" parameterType="Integer" resultMap="userRoleResult">
   select u.*,r.id as r_id,r.roleCode,r.roleName from smbms_user u,smbms_role r 
         where u.userRole = #{userRole} and u.userRole = r.id
</select>

在上述程式碼中,把之前的角色結果對映程式碼抽取出來放在一個resultMap中,然後設定了association的resultMap屬性來引用外部的“roleResult”。這樣做就可以達到複用的效果,並且整體的結構較為清晰明瞭,特別適合association的結果對映比較多的情況。

association用於處理一對一的關聯關係,對於一對多的關聯關係的處理,則需要collection元素來實現。

2.3.3 使用collection處理一對多關聯關係

collection元素的作用和association元素的作用差不多。事實上,它們非常類似,collection也是對映到JavaBean的某個“複雜型別”屬性,只不過這個屬性是一個集合列表,即JavaBean內部巢狀一個複雜資料型別(集合)屬性。和association元素一樣,我們使用巢狀查詢,或者從連線中巢狀結果集。

下面通過一個示例來演示collection的具體應用,示例需求:獲取指定使用者的相關資訊和地址列表。

首先需要建立POJO:Address.java,根據資料庫表(smbms_address)設計相應的屬性,並增加getter和setter方法

package cn.smbms.pojo;

import java.util.Date;
import java.util.List;

public class User {
   private Integer id; //id 
   private String userCode; //使用者編碼
   private String userName; //使用者名稱稱
   private String userPassword; //使用者密碼
   private Integer gender;  //性別
   private Date birthday;  //出生日期
   private String phone;   //電話
   private String address; //地址
   private Integer userRole;    //使用者角色ID
   private Integer createdBy;   //建立者
   private Date creationDate; //建立時間
   private Integer modifyBy;     //更新者
   private Date modifyDate;   //更新時間
   
   private Integer age;//年齡
   //private String userRoleName; //使用者角色名稱
   
   //association
   private Role role; //使用者角色
   //然後修改User類,增加地址列表屬性(List<Address>addressList),並增加相應的getter和setter方法,
   //collection
   private List<Address> addressList;//使用者地址列表
   
   public List<Address> getAddressList() {
      return addressList;
   }
   public void setAddressList(List<Address> addressList) {
      this.addressList = addressList;
   }
   public Role getRole() {
      return role;
   }
   public void setRole(Role role) {
      this.role = role;
   }

   public Integer getAge() {
      /*long time = System.currentTimeMillis()-birthday.getTime();
      Integer age = Long.valueOf(time/365/24/60/60/1000).IntegerValue();*/
      Date date = new Date();
      Integer age = date.getYear()-birthday.getYear();
      return age;
   }
   
/* public String getUserRoleName() {
      return userRoleName;
   }
   public void setUserRoleName(String userRoleName) {
      this.userRoleName = userRoleName;
   }*/
   public Integer getId() {
      return id;
   }
   public void setId(Integer id) {
      this.id = id;
   }
   public String getUserCode() {
      return userCode;
   }
   public void setUserCode(String userCode) {
      this.userCode = userCode;
   }
   public String getUserName() {
      return userName;
   }
   public void setUserName(String userName) {
      this.userName = userName;
   }
   public String getUserPassword() {
      return userPassword;
   }
   public void setUserPassword(String userPassword) {
      this.userPassword = userPassword;
   }
   public Integer getGender() {
      return gender;
   }
   public void setGender(Integer gender) {
      this.gender = gender;
   }
   public Date getBirthday() {
      return birthday;
   }
   public void setBirthday(Date birthday) {
      this.birthday = birthday;
   }
   public String getPhone() {
      return phone;
   }
   public void setPhone(String phone) {
      this.phone = phone;
   }
   public String getAddress() {
      return address;
   }
   public void setAddress(String address) {
      this.address = address;
   }
   public Integer getUserRole() {
      return userRole;
   }
   public void setUserRole(Integer userRole) {
      this.userRole = userRole;
   }
   public Integer getCreatedBy() {
      return createdBy;
   }
   public void setCreatedBy(Integer createdBy) {
      this.createdBy = createdBy;
   }
   public Date getCreationDate() {
      return creationDate;
   }
   public void setCreationDate(Date creationDate) {
      this.creationDate = creationDate;
   }
   public Integer getModifyBy() {
      return modifyBy;
   }
   public void setModifyBy(Integer modifyBy) {
      this.modifyBy = modifyBy;
   }
   public Date getModifyDate() {
      return modifyDate;
   }
   public void setModifyDate(Date modifyDate) {
      this.modifyDate = modifyDate;
   }
}

我們的JavaBean:User物件內部巢狀了一個複雜資料型別的屬性addressList。接下來在UserMapper介面中增加根據使用者id獲取使用者資訊以及地址列表的方法

public List<User> getAddressListByUserId(@Param("id")Integer userId);

對應修改UserMapper.xml,增加getAddressListByUserId,該select查詢語句返回型別為resultMap,並且引用外部的resultMap型別為User。由於User物件內嵌集合物件(addressList),因此需要使用collection來實現結果對映。

<!-- 獲取指定使用者的地址列表(user表-address表:1對多關係) collection start-->
<resultMap type="User" id="userAddressResult">
   <id property="id" column="id"/>
   <result property="userCode" column="userCode"/>
   <result property="userName" column="userName"/>
    //可以理解為:一個名為addressList、元素型別為Address的ArrayList集合。
   <collection property="addressList" ofType="Address">
      <id property="id" column="a_id"/>
      <result property="postCode" column="postCode"/>
      <result property="tel" column="tel"/>
      <result property="contact" column="contact"/>
      <result property="addressDesc" column="addressDesc"/>
   </collection>
</resultMap>

<select id="getAddressListByUserId" parameterType="Integer" resultMap="userAddressResult">
   select u.*,a.id as a_id,a.contact,a.addressDesc,a.postCode,a.tel
         from smbms_user u,smbms_address a where u.id = a.userId and u.id=#{id}
</select>

簡單分析collection的屬性。

➢ ofType:完整Java類名或者別名,即集合所包含的型別。此處為Address。

➢ property:對映資料庫列的實體物件的屬性。此處為User裡定義的屬性:addressList。

最後修改測試類UserMapperTest.java,增加測試方法

@Test
public void getAddressListByUserIdTest(){
   SqlSession sqlSession = null;
   List<User> userList = new ArrayList<User>();
   Integer userId = 1;
   try {
      sqlSession = MyBatisUtil.createSqlSession();
      userList = sqlSession.getMapper(UserMapper.class).getAddressListByUserId(userId);
   } catch (Exception e) {
      // TODO: handle exception
      e.printStackTrace();
   }finally{
      MyBatisUtil.closeSqlSession(sqlSession);
   }
   for(User user:userList){
      logger.debug("userList(include:addresslist) =====> userCode: " + user.getUserCode() + ", userName: " + user.getUserName());
      for(Address address : user.getAddressList()){
         logger.debug("address ----> id: " + address.getId() + ", contact: " + address.getContact() 
               + ", addressDesc: " + address.getAddressDesc() + ", tel: " + address.getTel()
               + ", postCode: " + address.getPostCode());
      }
   }
}

在測試方法中呼叫getAddressListByUserId()方法獲取userList,並進行結果輸出,關鍵是對映的使用者地址列表的相關資訊,需要進一步迴圈addressList輸出。當然,通過之前學習association,我們就可以想到例子中的collection結果對映可以複用。提取相應程式碼到一個resultMap中,給collection增加resultMap屬性進行外部引用

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-hUNTR4j6-1609551381902)(C:\Users\pcy\AppData\Roaming\Typora\typora-user-images\image-20201216213129163.png)]

任務4 配置resultMap自動對映級別和MyBatis快取

關鍵步驟如下。

➢ 配置resultMap的自動對映級別。

➢ 瞭解MyBatis快取的配置方法。

2.4.1 resultMap自動對映級別

<resultMap type="User" id="userAddressResult">
   <id property="id" column="id"/>
   <result property="userCode" column="userCode"/>
   <result property="userName" column="userName"/>
   <collection property="addressList" ofType="Address" resultMap="addressResult"/>
</resultMap>

<resultMap type="Address" id="addressResult">
   <id property="id" column="a_id"/>
   <result property="postCode" column="postCode"/>
   <result property="tel" column="tel"/>
   <result property="contact" column="contact"/>
   <result property="addressDesc" column="addressDesc"/>
</resultMap>

<select id="getAddressListByUserId" parameterType="Integer" resultMap="userAddressResult">
   select u.*,a.id as a_id,a.contact,a.addressDesc,a.postCode,a.tel
         from smbms_user u LEFT JOIN smbms_address a ON u.id = a.userId where u.id=#{id}
</select>

User類中的userPassword屬性和Address類中的userId屬性均未在resultMap中進行匹配對映。現修改測試方法getAddressListByUserIdTest的程式碼,輸出未做匹配對映的屬性值

@Test
public void getAddressListByUserIdTest(){
   SqlSession sqlSession = null;
   User user = null;
   Integer userId = 1;
   try {
      sqlSession = MyBatisUtil.createSqlSession();
      user = sqlSession.getMapper(UserMapper.class).getAddressListByUserId(userId);
   } catch (Exception e) {
      // TODO: handle exception
      e.printStackTrace();
   }finally{
      MyBatisUtil.closeSqlSession(sqlSession);
   }
   if(null != user){
      logger.debug("userList(include:addresslist) =====> userCode: " + user.getUserCode() + ", userName: " + user.getUserName());
      if(user.getAddressList().size() > 0){
         for(Address address : user.getAddressList()){
            logger.debug("address ----> id: " + address.getId() + ", contact: " + address.getContact() 
                  + ", addressDesc: " + address.getAddressDesc() + ", tel: " + address.getTel()
                  + ", postCode: " + address.getPostCode());
         }
      }else{
         logger.debug("該使用者下無地址列表!");
      }
   }else{
      logger.debug("查無此使用者!");
   }
   
}

觀察輸出結果,發現當沒有設定autoMappingBehavior的時候,也就是預設情況下(PARTIAL),若是普通資料型別的屬性,會自動匹配,這在示例10中已經提到過。但若是有內部巢狀(association或者collection),那麼輸出結果就是null(如圖中的userPassword和userId),也就是說它不會自動匹配,除非手工設定autoMappingBehavior的value為FULL(自動匹配所有)。修改mybatis-config.xml程式碼如下:

<settings>
   <!-- 配置mybatis的log實現為LOG4J -->
   <setting name="logImpl" value="LOG4J" />
   <!-- 設定resultMap的自動對映級別為NONE(禁止自動匹配) -->
   <!-- <setting name="autoMappingBehavior" value="NONE" /> -->
</settings>

觀察輸出結果,發現autoMappingBehavior的value設定為FULL(自動匹配所有屬性)之後,未作對映的欄位userPassword和userId均有值輸出。

可以認識到MyBatis對resultMap自動對映的三個匹配級別:

➢ NONE:禁止自動匹配。

➢ PARTIAL(預設):自動匹配所有屬性,有內部巢狀(association、collection)的除外。

➢ FULL:自動匹配所有屬性。

2.4.2 MyBatis快取

1.一級快取一級快取是基於PerpetualCache(MyBatis自帶)的HashMap本地快取,作用範圍為session域內,當session flush或者close之後,該session中所有的cache就會被清空。

2.二級快取二級快取就是global caching,它超出session範圍之外,可以被所有SqlSession共享,開啟它只需要在MyBatis的核心配置檔案(mybatis-config.xml)的settings中設定即可。

一級快取快取的是SQL語句,二級快取快取的是結果物件。

3.二級快取的配置

(1)MyBatis的全域性cache配置,需要在mybatis-config.xml的settings中設定

<settings>
   <setting name="cacheEnable" value="true"/>
   <!-- 配置mybatis的log實現為LOG4J -->
   <setting name="logImpl" value="LOG4J" />
   <!-- 設定resultMap的自動對映級別為NONE(禁止自動匹配) -->
   <!-- <setting name="autoMappingBehavior" value="NONE" /> -->
</settings>

(2)在mapper檔案(如UserMapper.xml)中設定快取,預設情況下是未開啟快取的。需要注意的是,global caching的作用域是針對mapper的namespace而言的,即只有在此namespace內(cn.smbms.dao.user.UserMapper)的查詢才能共享這個cache,程式碼如下:

<mapper namespace="cn.smbms.dao.user.UserMapper">
   <cache eviction="FIFO"
         flushInterval="60000"
         size="512"
         readOnly="true"/>
</mapper>

(3)在mapper檔案配置支援cache後,如果需要對個別查詢進行調整,可以單獨設定cache,程式碼如下:

<select id="getAddressListByUserId" parameterType="Integer" resultMap="userAddressResult" useCache="true">
   select u.*,a.id as a_id,a.contact,a.addressDesc,a.postCode,a.tel
         from smbms_user u LEFT JOIN smbms_address a ON u.id = a.userId where u.id=#{id}
</select>

本章總結

對於MyBatis快取的內容僅做了解即可,因為面對一定規模的資料量,內建的Cache方式就派不上用場了;況且對查詢結果集做快取並不是MyBatis框架擅長的,它擅長做的應該是SQL對映。所以採用OSCache、Memcached等專門的快取伺服器來處理快取更為合理。

➢ MyBatis的SQL對映檔案提供select、insert、update、delete等元素來實現SQL語句的對映。

➢ SQL對映檔案的根節點是mapper元素,需要指定namespace來區別於其他的mapper,保證全域性唯一;並且其名稱必須跟介面同名,作用是繫結DAO介面,即面向介面程式設計。

➢ SQL對映檔案的select返回結果型別的對映可以使用resultMap或resultType,但不能同時使用。

➢ MyBatis的SQL語句引數入參,對於基礎資料型別的引數資料,使用@Param註解實現引數入參;對於複雜資料型別的引數資料,直接入參即可。

➢ resultMap的association和collection可以實現高階結果對映。

相關文章