【Java EE】映射器

映射器

映射器是MyBatis最複雜且最重要得組件,它由一個接口加上XML文件(或者註解)組成。在映射器中可以配置參數、各類的SQL語句、存儲過程、緩存、級聯等複雜的內容,並且通過簡易的映射規則映射到指定的POJO或者其他對象上,映射器能有效消除JDBC底層的代碼。
MyBatis的映射器使用註解的方式應用不廣的原因有三:

  • 面對複雜性,SQL會顯得無力
  • 註解的可讀性較差
  • 功能上註解丟失了XML上下文相互引用的功能。

概述

映射器的配置元素主要有:select、insert、update、delete、sql、resultMap、cache、cache-ref。

select元素——查詢語句

在映射器中select元素代表SQL的select語句,用於查詢。select元素的配置有id、parameterType、resultMap、flushCache、useCache、timeout、fetchSize、statementType、resultSetType、databaseId、resultOrdered、resultSets。
實際工作中用的最多的是id、parameterType、resultType、resultMap,如果要設置緩存,還會使用到flushCache、useCache,其他的都是不常用的元素。

簡單的select元素的應用

一個簡單的select元素應用:

<select id="countUserByFirstName" parameterType="string" resultType="int">
	select count(*) total from t_user where user_name like concat(#{firstName},'%')
</select>

其中元素的含義是:

  • id配合Mapper的全限定名,聯合成爲一個唯一的標識,用於標識這條SQL
  • parameterType標識這條SQL接受的參數類型
  • resultType標識這條SQL返回的結果類型
  • #{firstName}是被傳遞進去的參數

還需要給一個接口方法程序才能運行起來:

public Integer countUserByFirstName(String firstName);

自動映射和駝峯映射

MyBatis提供了自動映射的功能,在默認的情況下,自動映射功能是開啓的。
在setting元素中有兩個可以配置的選項autoMappingBehavior和mapUnderscoreToCamelCase,它們是控制自動映射和駝峯映射的開關。一般自動映射會用的多一些。
配置自動映射的autoMappingBehavior選項的取值範圍是:

  • NONE,不進行自動映射
  • PARTIAL,默認值,只對沒有嵌套結果集進行自動映射
  • FULL,對所有的結果集進行自動映射,包括嵌套結果集。

爲了實現自動映射,首先要給出一個POJO:

public class Role {
	private Long id;
	private String roleName;
	private String note;

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getRoleName() {
		return roleName;
	}

	public void setRoleName(String roleName) {
		this.roleName = roleName;
	}

	public String getNote() {
		return note;
	}

	public void setNote(String note) {
		this.note = note;
	}
}

如果編寫的SQL列名和屬性名稱保持一致,那麼就會形成自動映射,如下:

<select id="getRole" parameterType="long" resultType="com.learn.ssm.pojo.Role">
	select id, role_name as roleName, note from t_role where id = #{id}
</select>

如果系統都嚴格按照駝峯命名法,那麼只需要在配置項把mapUnderscoreToCamelCase設置爲true即可,然後修改SQL語句:

select id, role_name, note from t_role where id = #{id}

MyBatis會嚴格按照駝峯命名法的方式做自動映射,但是降低了靈活性。
自動映射和駝峯映射都建立在SQL列名與POJO屬性名的映射關係上。

傳遞多個參數

1. 使用map接口傳遞參數
在MyBatis中允許map接口通過鍵值對傳遞多個參數。把接口方法定義爲:

public List<Role> findRolesByMap(Map<String, Object> parameterMap)

使用它在SQL中設置對應的參數:

<select id="findRolesByMap" parameterType="map" resultType="role">
	select id, role_name as roleName, note from t_role 
	where role_name like concat('%',#{roleName},'%')
	and note like concat('%',#{note},'%')
</select>

注意:參數roleName和note,要求的是map的鍵,即:

RoleMapperroleMapper = sqlSession.getMapper(RoleMapper.class);
Map<String, Object>parameterMap = new HashMap<String, Object>();
parameterMap.put("roleName","1");
parameterMap.put("note","1");
List<Role> roles = roleMapper.findRolesByMap(parameterMap);

該方法用的不多,主要在於:map是一個鍵值對應的集合,使用者要通過閱讀它的鍵,才能明白其作用;其次,使用map不能限定其傳遞的數據類型,因此業務性質不強,可讀性差。
2. 使用註解傳遞多個參數
MyBatis爲開發者提供了一個註解@Param(org.apache.ibatis.annotations.Param),可以通過它去定義映射器的參數名稱,使用它可以得到更好的可讀性。接口方法如下:

public List<Role> findRolesByAnnotation(@Param("roleName") String rolename, @Param("note") String note);

然後修改映射文件代碼:

<select id="findRolesByAnnotation" resultType="role">
	select id, role_name as roleName, note from t_role 
	where role_name like concat('%',#{roleName},'%')
	and note like concat('%',#{note},'%')
</select>

注意,此時不需要給出parameterType屬性。但是這種方法只適合參數小於5個的情況。
3. 通過JavaBean傳遞多個參數
先定義一個參數的POJO:

public class RoleParams{
	private String roleName;
	private String note;
	/*setter and getter*/
}

定義接口方法:

public List<Role> findRolesByBean(RoleParams roleParam);

修改映射文件:

<select id="findRolesByBean" parameterType = "com.learn.ssm.param.RoleParams" resultType="role">
	select id, role_name as roleName, note from t_role 
	where role_name like concat('%',#{roleName},'%')
	and note like concat('%',#{note},'%')
</select>

引入JavaBean定義的屬性作爲參數,然後查詢:

RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
RoleParams roleParams = new RoleParams();
roleParams.setRoleName("1");
roleParams.setNote("1");
List<Role> roles = roleMapper.findRolesByBean(roleParams);

4. 混合使用
混合使用幾種方法來傳遞參數,例如支持分頁,分頁的POJO實現如下:

public class PageParams{
	private int start;
	private int limit;
	/* setter and getter*/
	......
}

接口設計如下:

public List<Role> findByMix(@Param("params") RoleParams roleParams, @Params("page") PageParams pageParams);

修改映射文件:

<select id="findByMix" resultType="role">
	select id, role_name as roleName, note from t_role 
	where role_name like 
	concat('%',#{params.roleName},'%')
	and note like concat('%',#{params.note},'%')
	limit #{page.start}, #{page.limit}
</select>

5. 總結

  • 使用map傳遞參數導致了業務可讀性的喪失
  • 使用@Param註解傳遞了多個參數,受到參數個數(n)的影響
  • 當參數個數多於5個時,推薦使用JavaBean方式
  • 對於使用混合參數,要明確參數的合理性

使用resultMap映射結果集

爲了支持複雜的映射,select元素提供了resultMap屬性。先定義resultMap屬性。如下所示:

<mapper namespace="com.ssm.mapper.RoleMapper">
	<resultMap type="role" id="roleMap">
		<id column="id" property="id" />
		<result column="role_name" property="roleName" />
		<result column="note" property="note" />
	</resultMap>

	<select id="getRoleUserResultMap" parameterType="long" resultMap="roleMap">
		select id, role_name, note from t_role where id = #{id}
	</select>
</mapper>

其中:

  • resultMap元素定義了一個roleMap,id代表它的標識,type代表使用哪個類作爲其映射的類,可以是別名或者全限定名。
  • 子元素id代表resultMap的主鍵,而result代表其屬性,column代表SQL的列名
  • 在select元素中的屬性resultMap制定了採用哪個resultMap作爲其映射規則。

分頁參數RowBounds

MyBatis不僅支持分頁,還內置了一個專門處理分頁的類——RowBounds。源碼如下:

public class RowBounds {

  public static final int NO_ROW_OFFSET = 0;
  public static final int NO_ROW_LIMIT = Integer.MAX_VALUE;
  public static final RowBounds DEFAULT = new RowBounds();

  private final int offset;
  private final int limit;

  public RowBounds() {
    this.offset = NO_ROW_OFFSET;
    this.limit = NO_ROW_LIMIT;
  }

  public RowBounds(int offset, int limit) {
    this.offset = offset;
    this.limit = limit;
  }

  public int getOffset() {
    return offset;
  }

  public int getLimit() {
    return limit;
  }

}

offset是偏移量,即從第幾行開始讀取數據。limit是限制條數。
使用這個類只要給接口增加一個RowBounds參數即可:

public List<Role> findByRowBounds(@Param("roleName") String rolename, @Params("note") String note, RowBounds rowBounds);

而映射文件中不需要RowBounds的內容,MyBatis會自動識別它,據此進行分頁。具體如下:

<select id="findByRowBounds" resultType="role">
	select id, role_name as roleName, note from t_role 
	where role_name like 
	concat('%',#{roleName},'%')
	and note like concat('%',#{note},'%')
</select>

測試代碼如下:

SqlSession sqlSession = null;
	try {
		sqlSession = SqlSessionFactoryUtils.openSqlSession();
		RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
		RowBounds rowBounds = new RowBounds(0,20);
		List<Role> roleList = roleMapper.findByRowBounds("role_name","note",rowBounds);
		System.err.println(roleList.size());
	} catch(Exception ex) {
		ex.printStackTrace();
	} finally {
		if (sqlSession != null) {
			sqlSession.close();
		}
}

注意,RowBounds分頁只能運用在一些小數據量的查詢。RowBounds分頁的原理是執行SQL的查詢,按照偏移量和限制條數返回查詢結果,所以對於大量的數據查詢並不適用。

insert元素——插入語句

概述

MyBatis中insert語句可以配置以下屬性:id、parameterType、flushCache、timeout、statementType、useGenerateKeys、keyColumn、databaseId。
MyBatis在執行完一條insert語句後,會返回一個整數標識其影響記錄數。

簡單的insert語句的應用

寫一個SQL插入角色,這是一條最簡單的插入語句:

<insert id = "insertRole" parameterType="role">
	insert into t_role(role_name, note) values(#{roleName},#{note})
</insert>

說明:

  • id標識出這條SQL
  • parameterType代表傳入參數類型
    沒有配置的屬性將採用默認值。

主鍵回填

JDBC中的Statement對象在執行插入的SQL後,可以通過getGeneratedKeys方法獲得數據庫生成的主鍵(需要數據庫驅動支持),這樣便能達到獲取主鍵的功能。在insert語句中有一個開關屬性useGeneratedKeys,用來控制是否打開這個功能,它的默認值爲false。當打開了這個開關,還要配置其屬性keyProperty或keyColumn,告訴系統把生成的主鍵放入哪個屬性中。如果存在多個主鍵,就要用逗號(,)將它們隔開。

<insert id = "insertRole" parameterType="role">
	<useGeneratedKeys = "true" keyProperty = "id">
	insert into t_role(role_name, note) values(#{roleName},#{note})
</insert>

useGeneratedKeys 代表採用JDBC的Statement對象的getGeneratedKeys方法返回主鍵,而keyProperty則代表將用哪個POJO的屬性去匹配這個主鍵。

自定義主鍵

MyBatis主要依賴於selectKey元素進行支持自定義鍵值的生成規則。代碼如下:

<insert id = "insertRole" parameterType="role">
	<selectKey keyProperty= "id" resultType = "long" order="BEFORE">
		select if (max(id)==null, 1, max(id) + 3) from t_role
	</selectKey>
	insert into t_role(id, role_name, note) values(#{id}, #{roleName},#{note})
</insert>

keyProperty指定了採用哪個屬性作爲POJO的主鍵,resultType告訴MyBatis將返回一個long型的結果集,order設置爲BEFORE,說明它將於當前定義的SQL前執行。如果有一些特殊需要,可以把它設置爲AFTER,這樣就會在插入語句後執行了。

update元素和delete元素

update元素和delete元素與insert的屬性差不多,執行完之後會返回一個整數,標識該SQL語句影響了數據庫的記錄行數。

<update id = "updateRole" parameterType="role">
	update t_role set role_name = #{roleName}, note = #{note}
	where id = #{id}
</update>
<delete id = "deleteRole" parameterType = "long">
	delete from t_role where id = #{id}
</delete>

sql元素

sql元素的作用在於可以定義一條SQL的一部分,方便後面的SQL引用它。例如:

<mapper namespace="com.ssm.chapter5.mapper.RoleMapper">
	<resultMap id="roleMap" type="role">
		<id property="id" column="id" />
		<result property="roleName" column="role_name" />
		<result property="note" column="note" />
	</resultMap>
	<sql id = "roleCols">
		id, role_name, note
	<sql>
	<select id="getRole" parameterType="long" resultType="roleMap">
		select <include refid = "roleCols" /> from t_role where id = #{id}
	</select>

	<insert id = "insertRole" parameterType="role">
	<selectKey keyProperty= "id" resultType = "long" order="BEFORE" statementType="PREPARED">
		select if (max(id)==null, 1, max(id) + 3) from t_role
	</selectKey>
	insert into t_role(<include refid = "roleCols" />) values(#{id}, #{roleName},#{note})
	</insert>
</mapper>

通過sql元素進行了定義,就可以通過include元素引入到各個SQL中了。
sql元素支持變量傳遞。

	<sql id = "roleCols">
		${alias}.id, ${alias}.role_name, ${alias}.note
	<sql>
	<select id="getRole" parameterType="long" resultType="roleMap">
		select <include refid = "roleCols">
			<property name="alias" value="r" />
		</include>
		from t_role where id = #{id}
	</select>

參數

概述

MyBatis跟根據javaType和jdbcType去檢測使用哪個typeHandler。如果是一個沒有註冊的類型,那麼就會發生異常。此時可以自定義typeHandler,通過類似的辦法指定,就不會拋出異常了。例如:

#{age, javaType=int, jdbcType=NUMERIC, typeHandler=MyTypeHandler}

MyBatis也提供了一些對控制數值的精度支持,類似於:

#{width, javaType=double, jdbcType=NUMERIC, numericScale=2}

存儲過程參數文件

MyBatis對存儲過程也進行了支持,在存儲過程中存在:輸入(IN)參數,輸出(OUT)參數和輸入輸出(INOUT)參數3種類型。輸入參數是外界需要傳遞給存儲過程的;輸出參數是存儲過程經過處理後返回的;輸入輸出參數是一方面需要外界可以傳遞給它,另一方面是在最後存儲過程種也會將它返回給調用者。
對於簡單的輸出參數(比如INT、VARCHAR、DECIMAL)可以使用POJO通過映射來完成。存儲過程的參數類型有3種:

#{id, mode = IN}
#{id, mode = OUT}
#{id, mode = INOUT}

特殊字符串的替換和處理(#和$)

在MyBatis種,構建動態列名常常要傳遞類似於字符串的columns="col1, col2, col3, …"給SQL,讓其組裝成爲SQL語句。如果不想被MyBatis像處理普通參數一樣把它設爲“ col1, col2, col3, …”那麼可以寫成select ${columns} from t_tablename, 這樣MyBatis就不會轉移columns。但是這樣對SQL而言是不安全的。

resultMap元素

resultMap的作用是定義映射規則、級聯的更新、定製類型轉化器等。resultMap定義的主要是一個結果集的映射關係,也就是SQL到java Bean的映射關係定義,它也支持級聯等特性。

resultMap元素的構成

resultMap元素的子元素:

<resultMap>
	<constructor>
		<idArg/>
		<arg/>
	</constructor>
	<id/>
	<result/>
	<association/>
	<collection/>
	<discriminator>
		<case/>
	<discriminator/>
</resultMap>

其中constructor元素用於配置構造方法。一個POJO可能不存在沒有參數的構造防範,可以使用constructor進行配置。示例如下:

<resultMap ...>
	<constructor >
		<idArg column="id" javaType="int" />
		<arg column = "role_name" javaType="string"/>
	</constructor>
......
</resultMap>

id元素表示哪個列是主鍵,允許多個主鍵,多個主鍵則成爲聯合主鍵。result是配置POJO到SQL列名的映射關係。result元素和idArg元素的屬性有:property、column、javaType、jdbcType、typeHandler。此外還有association、collection和discriminator這些元素。一條查詢SQL執行後,就會返回結果,而結果可以使用map存儲,也可以使用POJO存儲。

使用map存儲過程集

一般而言,任何select語句都可以使用map存儲。即:

<select id="findColorByNote" parameterType="string" resultType="map">
	select id, color,note from t_color where note like concat('%', #{note}, '%')
</select>

但是更多的時候推薦使用POJO方式。

使用POJO存儲結果集

POJO是最常用的方式,一方面可以使用自動映射,有時候需要更爲複雜的映射或者級聯,這個時候還可以使用select語句的resultMap屬性配置映射集合,只是使用前要配置類似的resultMap。如下:

<resultMap id="roleResultMap" type="com.learn.ssm.pojo.Role">
	<id property = "id" column="id"/>
	<result property="roleName" column = "rolw_name"/>
	<result property="note" column="note">
</resultMap>

type代表需要映射的POJO。配置映射文件:

<select parameterType="long" id="getRole" resultMap="roleResultMap">
	select id, role_name, note from t_role where id=#{id}
</select>

SQL語句的列名和roleResultMap的column是一一對應的,使用XML配置的結果集,還可以配置typeHandler、javaType、jdbcType等,但是配置了

級聯

級聯是resultMap中的配置,有一對多和一對一的級聯。在MyBatis中還有一種被稱爲鑑別器的級聯,它是一種可以選擇具體實現類的級聯。
級聯不是必須的,級聯的好處是獲取關聯數據十分便捷,缺點是級聯過多影響系統性能。

MyBatis中的級聯

MyBatis的級聯分爲3種:

  • 鑑別器(discriminator):根據某些條件決定採用具體實現類級聯的方案
  • 一對一(association)
  • 一對多(collection)

注意: MyBatis沒有多對多級聯。將多對多關係轉換爲兩個一對多關係進行替換。
級聯模型建表SQL:

DROP TABLE IF EXISTS t_female_health_form;
DROP TABLE IF EXISTS t_male_health_form;
DROP TABLE IF EXISTS t_task;
DROP TABLE IF EXISTS t_work_card;
DROP TABLE IF EXISTS t_employee_task;
DROP TABLE IF EXISTS t_employee;

/*==============================================================*/
/* Table: t_employee                                            */
/*==============================================================*/
CREATE TABLE t_employee
(
   id                   INT(12) NOT NULL AUTO_INCREMENT,
   real_name            VARCHAR(60) NOT NULL,
   sex                  INT(2) NOT NULL COMMENT '1 - ÄÐ 
            0 -Å®',
   birthday             DATE NOT NULL,
   mobile               VARCHAR(20) NOT NULL,
   email                VARCHAR(60) NOT NULL,
   POSITION             VARCHAR(20) NOT NULL,
   note                 VARCHAR(256),
   PRIMARY KEY (id)
);

/*==============================================================*/
/* Table: t_employee_task                                       */
/*==============================================================*/
CREATE TABLE t_employee_task
(
   id                   INT(12) NOT NULL auto_increment,
   emp_id               INT(12) NOT NULL,
   task_id              INT(12) NOT NULL,
   task_name            VARCHAR(60) NOT NULL,
   note                 VARCHAR(256),
   PRIMARY KEY (id)
);

/*==============================================================*/
/* Table: t_female_health_form                                  */
/*==============================================================*/
CREATE TABLE t_female_health_form
(
   id                   INT(12) NOT NULL AUTO_INCREMENT,
   emp_id               INT(12) NOT NULL,
   heart                VARCHAR(64) NOT NULL,
   liver                VARCHAR(64) NOT NULL,
   spleen               VARCHAR(64) NOT NULL,
   lung                 VARCHAR(64) NOT NULL,
   kidney               VARCHAR(64) NOT NULL,
   uterus               VARCHAR(64) NOT NULL,
   note                 VARCHAR(256),
   PRIMARY KEY (id)
);

/*==============================================================*/
/* Table: t_male_health_form                                    */
/*==============================================================*/
CREATE TABLE t_male_health_form
(
   id                   INT(12) NOT NULL AUTO_INCREMENT,
   emp_id               INT(12) NOT NULL,
   heart                VARCHAR(64) NOT NULL,
   liver                VARCHAR(64) NOT NULL,
   spleen               VARCHAR(64) NOT NULL,
   lung                 VARCHAR(64) NOT NULL,
   kidney               VARCHAR(64) NOT NULL,
   prostate             VARCHAR(64) NOT NULL,
   note                 VARCHAR(256),
   PRIMARY KEY (id)
);

/*==============================================================*/
/* Table: t_task                                                */
/*==============================================================*/
CREATE TABLE t_task
(
   id                   INT(12) NOT NULL auto_increment,
   title                VARCHAR(60) NOT NULL,
   context              VARCHAR(256) NOT NULL,
   note                 VARCHAR(256),
   PRIMARY KEY (id)
);

/*==============================================================*/
/* Table: t_work_card                                           */
/*==============================================================*/
CREATE TABLE t_work_card
(
   id                   INT(12) NOT NULL AUTO_INCREMENT,
   emp_id               INT(12) NOT NULL,
   real_name            VARCHAR(60) NOT NULL,
   department           VARCHAR(20) NOT NULL,
   mobile               VARCHAR(20) NOT NULL,
   POSITION             VARCHAR(30) NOT NULL,
   note                 VARCHAR(256),
   PRIMARY KEY (id)
);

ALTER TABLE t_employee_task ADD CONSTRAINT FK_Reference_4 FOREIGN KEY (emp_id)
      REFERENCES t_employee (id) ON DELETE RESTRICT ON UPDATE RESTRICT;
ALTER TABLE t_employee_task ADD CONSTRAINT FK_Reference_8 FOREIGN KEY (task_id)
      REFERENCES t_task (id) ON DELETE RESTRICT ON UPDATE RESTRICT;
ALTER TABLE t_female_health_form ADD CONSTRAINT FK_Reference_5 FOREIGN KEY (emp_id)
      REFERENCES t_employee (id) ON DELETE RESTRICT ON UPDATE RESTRICT;
ALTER TABLE t_male_health_form ADD CONSTRAINT FK_Reference_6 FOREIGN KEY (emp_id)
      REFERENCES t_employee (id) ON DELETE RESTRICT ON UPDATE RESTRICT;
ALTER TABLE t_work_card ADD CONSTRAINT FK_Reference_7 FOREIGN KEY (emp_id)
      REFERENCES t_employee (id) ON DELETE RESTRICT ON UPDATE RESTRICT;

建立POJO

// 體檢表父表
public class HealthForm {
	
	private Long id;
	private Long empId;
	private String heart;
	private String liver;
	private String spleen;
	private String lung;
	private String kidney;
	private String note;
	/* setter and getter */	
}
// 女性體檢表
public class FemaleHealthForm extends HealthForm {

	private String uterus;
	/* setter and getter */	
}
// 男性提交表
public class MaleHealthForm extends HealthForm {
	
	private String prostate;
	/*setter and getter */
}
// 工牌表
public class WorkCard {
	private Long id;
	private Long empId;
	private String realName;
	private String department;
	private String mobile;
	private String position;
	private String note;
	/*setter and getter */
}
// 任務表
public class Task {
	private Long id;
	private String title;
	private String context;
	private String note;
	/*setter and getter */
}
// 僱員任務POJO
public class EmployeeTask {
	private Long id;
	private Long empId;
	private Task task = null;
	private String taskName;
	private String note;
	/*setter and getter */
}
// 僱員父類
public class Employee {

	private Long id;
	private String realName;
	private SexEnum sex = null;
	private Date birthday;
	private String mobile;
	private String email;
	private String position;
	private String note;
    //工牌按一對一級聯
	private WorkCard workCard;
	//僱員任務,一對多級聯
	private List<EmployeeTask> employeeTaskList = null;
	/*setter and getter */
}
// 男僱員類
public class MaleEmployee extends Employee {

	private MaleHealthForm maleHealthForm = null;
	/*setter and getter */
}
// 女僱員類
public class FemaleEmployee extends Employee {

	private FemaleHealthForm femaleHealthForm = null;
	/*setter and getter */
}

配置映射文件

配置映射文件是級聯的核心內容。

<?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.ssm.chapter5.mapper.TaskMapper">
    <select id="getTask" parameterType="long" resultType="com.ssm.chapter5.pojo.Task">
        select id, title, context, note from t_task where id = #{id}
    </select>
</mapper>
<?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.ssm.chapter5.mapper.WorkCardMapper">
    <select id="getWorkCardByEmpId" parameterType="long" resultType="com.ssm.chapter5.pojo.WorkCard">
        SELECT  id, emp_id as empId, real_name as realName, department, mobile, position, note FROM t_work_card
        where emp_id = #{empId} 
    </select>
</mapper>

僱員任務是一對一的級聯關係,通過task_id級聯,使用association元素。

<?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.ssm.chapter5.mapper.EmployeeTaskMapper">

    <resultMap type="com.ssm.chapter5.pojo.EmployeeTask" id="EmployeeTaskMap">
        <id column="id" property="id"/>
        <result column="emp_id" property="empId"/>
        <result column="task_name" property="taskName"/>
        <result column="note" property="note"/>
        <association property="task" column="task_id"
            select="com.ssm.chapter5.mapper.TaskMapper.getTask"/>
    </resultMap>
    
    <select id="getEmployeeTaskByEmpId" resultMap="EmployeeTaskMap">
        select id, emp_id, task_name, task_id, note from t_employee_task 
        where emp_id = #{empId}
    </select>
</mapper>

select配置是命名空間+SQL id的形式,這樣便可以指向對應Mapper的SQL,MyBatis就會通過對應的SQL將數據查詢回來。column代表SQL的列,用作參數傳遞給select屬性制定的SQL,如果是多個參數,則需要用逗號隔開。

<?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.ssm.chapter5.mapper.MaleHealthFormMapper">
	<select id="getMaleHealthForm" parameterType="long"
		resultType="com.ssm.chapter5.pojo.MaleHealthForm">
		select id, heart, liver, spleen, lung, kidney, prostate, note from
		t_male_health_form where emp_id = #{id}
	</select>
</mapper>
<?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.ssm.chapter5.mapper.FemaleHealthFormMapper">
	<select id="getFemaleHealthForm" parameterType="long"
		resultType="com.ssm.chapter5.pojo.FemaleHealthForm">
		select id, heart, liver, spleen, lung, kidney, uterus, note from
		t_female_health_form where emp_id = #{id}
	</select>
</mapper>

這兩個映射器都主要通過僱員編號找到對應體檢表的記錄,爲僱員查詢時提供查詢體檢表的SQL。

<?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.ssm.chapter5.mapper.EmployeeMapper">
	<resultMap type="com.ssm.chapter5.pojo.Employee" id="employee">
		<id column="id" property="id" />
		<result column="real_name" property="realName" />
		<result column="sex" property="sex"
			typeHandler="com.ssm.chapter5.typeHandler.SexTypeHandler" />
		<result column="birthday" property="birthday" />
		<result column="mobile" property="mobile" />
		<result column="email" property="email" />
		<result column="position" property="position" />
		<result column="note" property="note" />
		<association property="workCard" column="id"
			select="com.ssm.chapter5.mapper.WorkCardMapper.getWorkCardByEmpId" />
		<collection property="employeeTaskList" column="id"
			fetchType="eager"
			select="com.ssm.chapter5.mapper.EmployeeTaskMapper.getEmployeeTaskByEmpId" />
		<discriminator javaType="long" column="sex">
			<case value="1" resultMap="maleHealthFormMapper" />
			<case value="2" resultMap="femaleHealthFormMapper" />
		</discriminator>
	</resultMap>

	<resultMap type="com.ssm.chapter5.pojo.FemaleEmployee" id="femaleHealthFormMapper"
		extends="employee">
		<association property="femaleHealthForm" column="id"
			select="com.ssm.chapter5.mapper.FemaleHealthFormMapper.getFemaleHealthForm" />
	</resultMap>

	<resultMap type="com.ssm.chapter5.pojo.MaleEmployee" id="maleHealthFormMapper"
		extends="employee">
		<association property="maleHealthForm" column="id"
			select="com.ssm.chapter5.mapper.MaleHealthFormMapper.getMaleHealthForm" />
	</resultMap>

	<select id="getEmployee" parameterType="long" resultMap="employee">
		select
		id, real_name as realName, sex, birthday, mobile, email, position,
		note from t_employee where id = #{id}
	</select>

	<resultMap id="employee2" type="com.ssm.chapter5.pojo.Employee">
		<id column="id" property="id" />
		<result column="real_name" property="realName" />
		<result column="sex" property="sex"
			typeHandler="com.ssm.chapter5.typeHandler.SexTypeHandler" />
		<result column="birthday" property="birthday" />
		<result column="mobile" property="mobile" />
		<result column="email" property="email" />
		<result column="position" property="position" />
		<association property="workCard" javaType="com.ssm.chapter5.pojo.WorkCard"
			column="id">
			<id column="wc_id" property="id" />
			<result column="id" property="empId" />
			<result column="wc_real_name" property="realName" />
			<result column="wc_department" property="department" />
			<result column="wc_mobile" property="mobile" />
			<result column="wc_position" property="position" />
			<result column="wc_note" property="note" />
		</association>
		<collection property="employeeTaskList" ofType="com.ssm.chapter5.pojo.EmployeeTask"
			column="id">
			<id column="et_id" property="id" />
			<result column="id" property="empId" />
			<result column="task_name" property="taskName" />
			<result column="note" property="note" />
			<association property="task" javaType="com.ssm.chapter5.pojo.Task"
				column="et_task_id">
				<id column="t_id" property="id" />
				<result column="t_title" property="title" />
				<result column="t_context" property="context" />
				<result column="t_note" property="note" />
			</association>
		</collection>
		<discriminator javaType="int" column="sex">
			<case value="1" resultMap="maleHealthFormMapper2" />
			<case value="2" resultMap="femaleHealthFormMapper2" />
		</discriminator>
	</resultMap>


	<resultMap type="com.ssm.chapter5.pojo.MaleEmployee" id="maleHealthFormMapper2"
		extends="employee2">
		<association property="maleHealthForm" column="id"
			javaType="com.ssm.chapter5.pojo.MaleHealthForm">
			<id column="h_id" property="id" />
			<result column="h_heart" property="heart" />
			<result column="h_liver" property="liver" />
			<result column="h_spleen" property="spleen" />
			<result column="h_lung" property="lung" />
			<result column="h_kidney" property="kidney" />
			<result column="h_prostate" property="prostate" />
			<result column="h_note" property="note" />
		</association>
	</resultMap>

	<resultMap type="com.ssm.chapter5.pojo.FemaleEmployee" id="femaleHealthFormMapper2"
		extends="employee">
		<association property="femaleHealthForm" column="id"
			javaType="com.ssm.chapter5.pojo.FemaleHealthForm">
			<id column="h_id" property="id" />
			<result column="h_heart" property="heart" />
			<result column="h_liver" property="liver" />
			<result column="h_spleen" property="spleen" />
			<result column="h_lung" property="lung" />
			<result column="h_kidney" property="kidney" />
			<result column="h_uterus" property="uterus" />
			<result column="h_note" property="note" />
		</association>
	</resultMap>

	<select id="getEmployee2" parameterType="long" resultMap="employee2">
		select
		emp.id, emp.real_name, emp.sex, emp.birthday,
		emp.mobile, emp.email,
		emp.position, emp.note,
		et.id as et_id, et.task_id as et_task_id,
		et.task_name as et_task_name,
		et.note as et_note,
		if (emp.sex = 1,
		mhf.id, fhf.id) as h_id,
		if (emp.sex = 1, mhf.heart, fhf.heart) as
		h_heart,
		if (emp.sex = 1, mhf.liver, fhf.liver) as h_liver,
		if (emp.sex
		= 1, mhf.spleen, fhf.spleen) as h_spleen,
		if (emp.sex = 1, mhf.lung,
		fhf.lung) as h_lung,
		if (emp.sex = 1, mhf.kidney, fhf.kidney) as
		h_kidney,
		if (emp.sex = 1, mhf.note, fhf.note) as h_note,
		mhf.prostate
		as h_prostate, fhf.uterus as h_uterus,
		wc.id wc_id, wc.real_name
		wc_real_name, wc.department wc_department,
		wc.mobile wc_mobile,
		wc.position wc_position, wc.note as wc_note,
		t.id as t_id, t.title as
		t_title, t.context as t_context, t.note as t_note
		from t_employee emp
		left join t_employee_task et on emp.id = et.emp_id
		left join
		t_female_health_form fhf on emp.id = fhf.emp_id
		left join
		t_male_health_form mhf on emp.id = mhf.emp_id
		left join t_work_card wc
		on emp.id = wc.emp_id
		left join t_task t on et.task_id = t.id
		where
		emp.id = #{id}
	</select>
</mapper>

N+1問題

假設現有N個關聯關係完成了級聯,那麼只要再加入一個關聯關係,就變成了N+1個級聯,所有的級聯SQL都會被執行,造成資源浪費,這就是N+1問題。
爲了應對N+1問題,MyBatis提供了延遲加載功能。

延遲加載

在MyBatis的settings配置種存在兩個元素可以配置級聯,即lazyLoadingEnabled和aggressiveLazyLoading。前者是一個開關,決定開不開啓延遲加載,默認值爲false,後者是一個層級開關,當設置爲true時,它是一個開啓了層級開關的延遲加載。默認爲false。配置示例如下:

<settings>
	<setting name="lazyLoadingEnabled" value="true"/>
	<setting name="aggressiveLazyLoading" value="true"/>
</settings>

選項lazyLoadingEnabled決定是否開啓延遲加載,選項aggressiveLazyLoading控制是否採用層級加載,但是它們都是全局性的配置。在MyBatis中使用fetchType屬性,可以處理全局定義無法處理的問題,進行自定義。
fetchType出現在級聯元素(association、 collection, 注意 discriminator沒有這個屬性可配置)中,它存在兩個值:

  • eager,獲得當前POJO後立即加載對應的數據。
  • lazy,獲得當前POJO後延遲加載對應的數據。

修改配置如下:

<collection property="employeeTaskList" column="id" fetchType="eager"
select = "com.learn.ssm.mapper.EmployeeTaskMapper.getEmployeeTaskByEmpId"
/>

fetchType屬性會忽略全局配置項lazyLoadingEnabled和aggressiveLazyLoading。

另一種級聯

MyBatis還提供了另一張級聯方式,它是基於SQL表連接的基礎,進行再次設計的。
先定義SQL語句,然後對複雜的SQL進行級聯配置,在這個過程中:

  • 每一個級聯元素(association、 discriminator、collection)中屬性id的配置和POJO實體配置的id一一對應,形成級聯。
  • 在級聯元素中,association是通過javaType的定義去聲明實體映射的,而collection則是使用ofType進行聲明的。
  • discrimination元素定義使用何種具體的resultMap進行級聯。
    但是這樣仍然存在SQL比較複雜,配置項較多,而且一次性取數據造成內存浪費的問題。同時維護也比較困難。

多對多級聯

多對多的問題往往會被拆分爲兩個一對多來處理。例如,用戶和角色:

//角色POJO
public class Role2 {
	private Long id;
	private String roleName;
	private String note;
	// 關聯用戶信息,一對多關聯
	private List<User2> userList;
	/* setter and getter */	
}
//用戶POJO
public class User2 {
	private Long id;
	private String userName;
	private String realName;
	private SexEnum sex;
	private String moble;
	private String email;
	private String note;
	// 對角色一對多關聯
	private List<Role2> roleList;
	/* setter and getter */	
}

注意,這裏的兩個List類型的屬性是專門做一對多級聯用的,使用collection元素去完成,得到兩個Mapper。

<?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.ssm.chapter5.mapper2.RoleMapper2">
	<resultMap type="com.ssm.chapter5.pojo2.Role2" id="roleMapper">
		<id column="id" property="id" />
		<result column="role_name" property="roleName" />
		<result column="note" property="note" />
		<collection property="userList" column="id" fetchType="lazy"
			select="com.ssm.chapter5.mapper2.UserMapper2.findUserByRoleId" />
	</resultMap>

	<select id="getRole" parameterType="long" resultMap="roleMapper">
		select id, role_name, note from t_role where id = #{id}
	</select>

	<select id="findRoleByUserId" parameterType="long" resultMap="roleMapper">
		select r.id, r.role_name, r.note from t_role r, t_user_role ur
		where r.id = ur.role_id and ur.user_id = #{userId}
	</select>
</mapper>
<?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.ssm.chapter5.mapper2.UserMapper2">
	<resultMap type="com.ssm.chapter5.pojo2.User2" id="userMapper">
		<id column="id" property="id" />
		<result column="user_name" property="userName" />
		<result column="real_name" property="realName" />
		<result column="sex" property="sex"
			typeHandler="com.ssm.chapter5.typeHandler.SexTypeHandler" />
		<result column="mobile" property="moble" />
		<result column="email" property="email" />
		<result column="position" property="position" />
		<result column="note" property="note" />
		<collection property="roleList" column="id" fetchType="lazy"
			select="com.ssm.chapter5.mapper2.RoleMapper2.findRoleByUserId" />
	</resultMap>
	<select id="getUser" parameterType="long" resultMap="userMapper">
		select id, user_name, real_name, sex, moble, email, note from t_user where
		id =#{id}
	</select>
	<select id="findUserByRoleId" parameterType="long" resultMap="userMapper">
		select u.id, u.user_name, u.real_name, u.sex, u.moble, u.email, u.note
		from
		t_user u , t_user_role ur where u.id = ur.user_id and ur.role_id =#{roleId}
	</select>
</mapper>

使用fetchType設置爲lazy,就能夠進行延遲加載。

緩存

在Mybatis中允許使用緩存,緩存一般都放置在可高速讀/寫的存儲器上。分爲一級緩存和二級緩存,同時也可以配置關於緩存的設置。

一級緩存和二級緩存

一級緩存是在SqlSession上的緩存,二級緩存是在SqlSessionFactory上的緩存,默認情況下會開啓一級緩存,這個不需要POJO對象可序列化。
注意在通過SqlSession獲取對象的時候commit()方法的使用,如果不進行commit(),是不會有一級緩存存在的。
一級緩存是在SqlSession層面,對於不同的SqlSession對象是不能共享的。因此,要使其共享,則開啓二級緩存,即只要在映射文件中加入代碼:

<cache/>

這個時候MyBatis會序列化和反序列化對應的POJO,即要求POJO是一個可序列化的對象,那麼就必須實現java.io.Serializable接口。

緩存配置項、自定義和引用

緩存要明確cache元素的配置項,有:blocking、readOnly、eviction、flushInterval、type、size。
對於自定義的緩存,只要實現類需要實現MyBatis的接口org.apache.ibatis.cache.Cache,即:

public interface Cache {

  /**
   * @return The identifier of this cache
   */
  String getId();

  /**
   * @param key Can be any object but usually it is a {@link CacheKey}
   * @param value The result of a select.
   */
  void putObject(Object key, Object value);

  /**
   * @param key The key
   * @return The object stored in the cache.
   */
  Object getObject(Object key);

  /**
   * As of 3.3.0 this method is only called during a rollback
   * for any previous value that was missing in the cache.
   * This lets any blocking cache to release the lock that
   * may have previously put on the key.
   * A blocking cache puts a lock when a value is null
   * and releases it when the value is back again.
   * This way other threads will wait for the value to be
   * available instead of hitting the database.
   *
   *
   * @param key The key
   * @return Not used
   */
  Object removeObject(Object key);

  /**
   * Clears this cache instance.
   */
  void clear();

  /**
   * Optional. This method is not called by the core.
   *
   * @return The number of elements stored in the cache (not its capacity).
   */
  int getSize();

  /**
   * Optional. As of 3.2.6 this method is no longer called by the core.
   * <p>
   * Any locking needed by the cache must be provided internally by the cache provider.
   *
   * @return A ReadWriteLock
   */
  default ReadWriteLock getReadWriteLock() {
    return null;
  }

}

現實中,可以使用Redis,MongoDB或者其他常用的緩存,假設存在一個Redis的緩存實現類com.learn.ssm.cache.RedisCache,那麼可以這樣配置:

<cache type="com.learn.ssm.cache.RedisCache">
	<property name="host" value="localhost"/>
</cache>

配置後,MyBatis會啓用緩存,同時調用setHost(String host)方法,去設置配置的內容。另外,還可以自定義配置:

<select ... flushCache="false" useCache="true"/>
<insert ... flushCache="true"/>
<update ... flushCache="true"/>
<delete ... flushCache="true"/>

flushCache代表是否刷新緩存,useCache屬性則是select特有的,表示是否需要使用緩存。

存儲過程

存儲過程是數據庫的一個概念,它是數據庫預先編譯好,放在數據庫內存中的一個程序片段,所以具備性能高,可以重複使用的特定。它還定義了3種類型的參數:輸入參數、輸出參數、輸入輸出參數。

IN和OUT參數存儲過程

定義一個簡單的存儲過程:

create or replace
PROCEDURE count_role(
	p_role_name in varchar,
	count_total out int,
	exec_date out date
)
IS
BEGIN
select count(*) into count_total from t_role where role_name like '%'||p_role_name||'%';
select sysdate into exec_date from dual;
End;

設計一個POJO:

public class PdCountRoleParam{
	private String roleName;
	private int total,
	private Date execDate;
	/* setter and getter */
}

修改配置文件:

<select id="countRole" parameterType="com.learn.ssm.pojo.PdCountRoleParam" statementType="CALLABLE">
	{call count_role(
	#{roleName, mode=IN,jdbcType=VARCHAR},
	#{total, mode=OUT, jdbcType=INTEGER},
	#{execData, mode=OUT, jdbcType=DATE}
	)}
</select>

其中:

  • 指定statementType爲CALLABLE,說明它是在使用存儲過程
  • 定義parameterType爲PdCountRoleParams參數
  • 在調度存儲過程种放入參數對應的屬性,並在在屬性上通過mode設置了其輸入或者輸出參數,指定對應的jdbcType。

遊標的使用

如果把jdbcType聲明爲CURSOR,那麼它就會使用ResultSet對象處理對應的結果,只要設置映射關係,MyBatis就會把結果映射出來。爲了使得ResultSet對應能夠映射爲POJO,設置resultMap爲roleMap,如下所示,這樣MyBatis就會採用配置的映射規則將其映射爲POJO了。

<resultMap type="role" id="roleMap">
	<id property="id" column="id"/>
	<result property="roleName" column="role_name"/>
	<result property="note" column="note"/>
</resultMap>
<select id="countRole" parameterType="com.learn.ssm.pojo.PdCountRoleParam" statementType="CALLABLE">
	{call count_role(
	#{roleName, mode=IN,jdbcType=VARCHAR},
	#{total, mode=OUT, jdbcType=INTEGER},
	#{execData, mode=OUT, jdbcType=DATE},
	#{roleList,mode=OUT,jdbcType=CURSOR,javaType=ResultSet,resultMap=roleMap}
	)}
</select>
發佈了245 篇原創文章 · 獲贊 140 · 訪問量 26萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章