Mybatis通過一條SQL查出關聯的對象
以往在做對象的查詢時如果需要把關聯的對象一起查出來是通過resultMap的子查詢來進行的。通過子查詢來進行的關聯對象的查詢時,Mybatis會重新發起一次數據庫請求,這在有的時候性能方面不是特別的好,我們期望可以用一條SQL語句就把主體對象以及關聯的對象都查出來,Hibernate其實是有對應的實現,Mybatis現在也有對應的支持(筆者以前剛開始接觸Mybatis時Mybatis還沒有這個機制,不知道是從哪個版本開始有了這個功能,挺好的)。
現在假設我們有兩張表,sys_wf_process表和sys_wf_node表。sys_wf_process表是流程實例表,sys_wf_node是流程節點表,流程節點表通過process_id字段關聯sys_wf_process表的主鍵id,一個流程實例會有很多流程節點,二者的表結構如下所示。
sys_wf_process表
字段名 | 類型 | 備註 |
id | integer | 主鍵 |
template_id | integer | 模板ID |
creator | integer | 創建人的ID |
create_time | timestamp | 創建時間 |
sys_wf_node表
字段名 | 類型 | 備註 |
id | integer | 主鍵 |
process_id | integer | 流程實例ID |
node_code | varchar(10) | 節點編號 |
node_name | varchar(100) | 節點名稱 |
針對這兩張表,我們分別建立了對應的實體類與之對應,sys_wf_process表對應的實體對象是SysWfProcess,sys_wf_node表對應的實體類是SysWfNode。二者的代碼如下(對應的set和get方法將被省略)。
public class SysWfProcess {
private Integer id;
private Integer templateId;//模板ID
private Integer creator;
private Date createTime;
private List<SysWfNode> nodes;//包含的流程節點
//…省略get和set方法
}
public class SysWfNode {
private Integer nodeId;//主鍵
private Integer processId;//流程實例ID
private SysWfProcess process;//關聯的流程實例
private String nodeCode;//節點編號
private String nodeName;//節點名稱
//…省略get和set方法
}
SysWfProcess和SysWfNode是一對多的關係,下面將分三種情況來討論對應的配置,通過1拿多,通過多拿1,以及相互拿(即雙向的引用)。
1.1 通過1拿多
在本例中1的一方是SysWfProcess,其通過屬性nodes引用多的一方。如果通過一條SQL在查詢1的一方自動把多的一方也查詢出來,我們通常是通過表的關聯查詢來查詢出所有相關的信息。爲此我們在對應的Mapper.xml文件中加入如下查詢配置。
<!-- 只用一條SQL查出一對多關係 -->
<select id="singleSql1ToN" parameterType="java.lang.Integer"resultMap="SingleSql1ToNResult">
select
a.id,a.template_id,a.creator,a.create_time,b.id node_id,b.node_code, b.node_name
from sys_wf_process a
left join sys_wf_node b
on a.id=b.process_id
where a.id=#{id}
</select>
以及對應的resultMap配置。
<resultMap id="SingleSql1ToNResult"type="com.elim.learn.mybatis.model.SysWfProcess">
<!-- id非常重要,用來區分記錄 -->
<id property="id" column="id"/>
<result property="creator" column="creator"/>
<result property="templateId" column="template_id"/>
<result property="createTime" column="create_time"/>
<!-- 指定關聯的集合屬性的數據映射,ofType屬性指定集合元素對應的數據類型 -->
<collection property="nodes"ofType="com.elim.learn.mybatis.model.SysWfNode">
<id property="nodeId" column="node_id"/>
<result property="nodeCode" column="node_code"/>
<result property="nodeName" column="node_name"/>
<result property="processId" column="id"/>
</collection>
</resultMap>
關聯的集合屬性是通過collection元素來定義的,跟通過resultMap指定子查詢使用的元素是一樣的,只是這裏不指定子查詢,而是直接配置從同一個查詢結果集裏面進行映射。按照上面這種配置Mybatis會把結果集裏面的每一行的node_id、node_code、node_name和id字段取出根據映射關係構造爲一個SysWfNode對象。
然後我們也在對應的Mapper接口裏面加入剛剛定義的查詢方法。
public interface SysWfProcessMapper {
SysWfProcess singleSql1ToN(Integer id);
}
測試一下,看是否能正常拿到SysWfProcess關聯的SysWfNode,代碼如下:
public class BasicTest {
private SqlSessionFactory sessionFactory = SqlSessionFactoryUtil.getSqlSessionFactory();
private SqlSession session = null;
@Before
public void before() {
session = sessionFactory.openSession();
}
@After
public void after() {
session.commit();
session.close();
}
@Test
public void test3() {
SysWfProcessMapper mapper = session.getMapper(SysWfProcessMapper.class);
SysWfProcess process = mapper.singleSql1ToN(1);
List<SysWfNode> nodes = process.getNodes();
System.out.println(nodes);//這裏可以輸出獲取到的SysWfNode信息
}
}
1.2 通過多拿1
通過一條SQL語句在查詢一的一方時把多的一方也查出來,我們需要在查詢時把所有關聯的信息都查詢出來,在對應的Mapper.xml文件中添加如下配置。
<!-- 只用一條SQL查出多對一關係 -->
<select id="singleSqlNTo1" parameterType="java.lang.Integer"resultMap="SingleSqlNTo1Result">
select
a.id,a.node_code,a.node_name,a.process_id, b.template_id,b.creator,b.create_time
from
sys_wf_node a, sys_wf_process b
where
a.process_id=b.id and a.id=#{id}
</select>
<resultMap type="com.elim.learn.mybatis.model.SysWfNode"id="SingleSqlNTo1Result">
<id property="nodeId" column="id"/>
<result property="nodeCode" column="node_code"/>
<result property="nodeName" column="node_name"/>
<result property="processId" column="process_id"/>
<!-- 單個對象的關聯是通過association元素來定義的 -->
<association property="process"javaType="com.elim.learn.mybatis.model.SysWfProcess">
<id property="id" column="process_id"/>
<result property="templateId" column="template_id"/>
<result property="creator" column="creator"/>
<result property="createTime" column="create_time"/>
</association>
</resultMap>
然後我們在對應的Mapper接口中定義與查詢id相同名稱的方法。
public interface SysWfNodeMapper {
SysWfNode singleSqlNTo1(Integer id);
}
測試如下:
@Test
public void test4() {
SysWfNodeMapper mapper = session.getMapper(SysWfNodeMapper.class);
SysWfNode node = mapper.singleSqlNTo1(2);
SysWfProcess process = node.getProcess();
System.out.println(process);//這裏能拿到對應的SysWfProcess
}
1.3 雙向引用
當需要兩邊都持有對應的引用,引用裏面又持有對應的引用時,基於單條查詢SQL的方式好像配置不出來。筆者將SingleSql1ToNResult中的SysWfProcess引用的SysWfNode再引用對應的SysWfProcess,指定其解析的resultMap爲SingleSql1ToNResult時將拋出java.lang.StackOverflowError,因爲它們在進行循環引用,循環的新建對象、賦值。配置如下:
<resultMap id="SingleSql1ToNResult"type="com.elim.learn.mybatis.model.SysWfProcess">
<!-- id非常重要,用來區分記錄 -->
<id property="id" column="id"/>
<result property="creator" column="creator"/>
<result property="templateId" column="template_id"/>
<result property="createTime" column="create_time"/>
<!-- 指定關聯的集合屬性的數據映射,ofType屬性指定集合元素對應的數據類型 -->
<collection property="nodes"ofType="com.elim.learn.mybatis.model.SysWfNode">
<id property="nodeId" column="node_id"/>
<result property="nodeCode" column="node_code"/>
<result property="nodeName" column="node_name"/>
<result property="processId" column="id"/>
<association property="process"javaType="com.elim.learn.mybatis.model.SysWfProcess"resultMap="SingleSql1ToNResult"/>
</collection>
</resultMap>
所以這種循環相互擁有對方引用的通過配置是不OK的,但是如果我們僅僅是對其某些屬性感興趣,我們可以在裏面的assocation時再通過result指定一層映射關係,這個時候我們就可以拿到SysWfNode對應的SysWfProcess對象了,但是它跟我們的擁有SysWfNode的SysWfProcess對象已經不是同一個對象了。基於這種情況的配置如下:
<resultMap id="SingleSql1ToNResult"type="com.elim.learn.mybatis.model.SysWfProcess">
<!-- id非常重要,用來區分記錄 -->
<id property="id" column="id"/>
<result property="creator" column="creator"/>
<result property="templateId" column="template_id"/>
<result property="createTime" column="create_time"/>
<!-- 指定關聯的集合屬性的數據映射,ofType屬性指定集合元素對應的數據類型 -->
<collection property="nodes"ofType="com.elim.learn.mybatis.model.SysWfNode">
<id property="nodeId" column="node_id"/>
<result property="nodeCode" column="node_code"/>
<result property="nodeName" column="node_name"/>
<result property="processId" column="id"/>
<association property="process"javaType="com.elim.learn.mybatis.model.SysWfProcess"resultMap="SysWfProcess"/>
</collection>
</resultMap>
<resultMap id="SysWfProcess"type="com.elim.learn.mybatis.model.SysWfProcess">
<id property="id" column="id"/>
<result property="creator" column="creator"/>
<result property="templateId" column="template_id"/>
<result property="createTime" column="create_time"/>
</resultMap>
對應的測試代碼如下:
@Test
public void test5() {
SysWfProcessMapper mapper = session.getMapper(SysWfProcessMapper.class);
SysWfProcess process = mapper.singleSql1ToN(1);
List<SysWfNode> nodes = process.getNodes();
SysWfNode node = nodes.get(0);
System.out.println(node.getProcess());//不爲null
System.out.println(process == node.getProcess());//false
}
1.4 columnPrefix
有的時候我們的查詢裏面可能需要多次關聯同一張表,因爲查詢出來的不同信息都是基於同一張表的。打個比方,我們有一張部門表,其有一個字段存的是部門經理ID,有一個字段存的是副經理ID,我們需要查一個部門的信息的時候把這個部門的經理和副經理的信息一起查出來,部門經理和副經理的信息都是存在人員信息表中的。部門表的結構是t_dept(id,name,manager_id,vice_manager_id),人員表的結果是t_person(id,no,name),那麼基於上面的場景,我們的查詢語句大概是這樣的。
<select id="findById" parameterType="java.lang.Long"resultMap="BaseResultMap">
SELECT
id,
name,
manager_id,
vice_manager_id,
b.no manager_no,
b.name manager_name,
c.no vice_manager_no,
c.name vice_manager_name
FROM
t_dept a
LEFT JOIN
t_person b ON a.manager_id = b.id
LEFT JOIN
t_person c ON a.vice_manager_id = c.id
WHERE
a.id=#{id}
</select>
對應的映射結果大概是這樣的。
<resultMap type="com.elim.learn.mybatis.model.Department"id="BaseResultMap">
<id column="id" property="id"/>
<result column="name" property="name"/>
<association property="manager"javaType="com.elim.learn.mybatis.model.Person">
<id column="manager_id" property="id"/>
<result column="manager_no" property="no"/>
<result column="manager_name" property="name"/>
</association>
<association property="viceManager"javaType="com.elim.learn.mybatis.model.Person">
<id column="vice_manager_id" property="id"/>
<result column="vice_manager_no" property="no"/>
<result column="vice_manager_name" property="name"/>
</association>
</resultMap>
我們關聯了兩個Person對象,就寫了兩遍<association>,只是映射的column不一樣而已,如果我們需呀關聯5個Person對象,按照這樣的方式就得配置5遍。Mybatis爲我們提供了一個columnPrefix可以簡化這種配置。它的大概意思是先配置一個通用的ResultMap,在配置<association>的時候指定resultMap爲我們配置的通用的ResultMap,同時指定在匹配通用的resultMap時需要使用的列的前綴,columnPrefix。文字描述可能不是很好理解,我們來看一個示例。基於columnPrefix我們的上述ResultMap應該配置成如下這樣。
<resultMap type="com.elim.learn.mybatis.model.Department"id="BaseResultMap">
<id column="id" property="id"/>
<result column="name" property="name"/>
<association property="manager" resultMap="PersonResultMap"columnPrefix="manager_"/>
<association property="viceManager"resultMap="PersonResultMap" columnPrefix="vice_manager_"/>
</resultMap>
<resultMap type="com.elim.learn.mybatis.model.Person"id="PersonResultMap">
<id column="id" property="id"/>
<result column="no" property="no"/>
<result column="name" property="name"/>
</resultMap>
PersonResultMap是最基礎的映射,然後對於manager屬性,我們指定了其columnPrefix=”manager_”,這樣在使用PersonResultMap映射結果集時,都會把對應的column加上指定的前綴“manager_”,即會把列manager_id與屬性id映射,而不是原來的列id。對於集合類型的屬性的映射也是一樣的,我們也可以在對應的<collection>標籤上指定columnPrefix。
參考文檔
http://www.mybatis.org/mybatis-3/sqlmap-xml.html
(注:本文是基於Mybatis3.3.1所寫)
(注:該博客第一次於2016年12月20日發表於本人的iteye博客,http://elim.iteye.com/blog/2346389)