本文基于mybatis-spring 1.3.1和mybatis 3.4.4版本
mybatis中的标签association
主要用于解决“有一个”类型的关系,它表示一个对象至多有一个关联对象。比如一般情况下,每个人都一个身份证,可能有些人没有(比如黑户和婴儿),人和身份证的关系就可以使用association表示。
本文接下来介绍association的三种用法,然后介绍其实现原理。
一、使用association
先定义两个类,一个是Person,一个是IDCard,分别表示人和身份证。
public class Person{
private int id;
private String name;
private String sex;
private Date birthday;
private IdCard card;//IdCard类为Person的属性
//get和set方法省略
}
public class IdCard{
private int id;
private String number;
private Date expiredTime;
//get和set方法省略
}
这两个类分别对应了两个表:
CREATE TABLE `person` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(10) NOT NULL,
`sex` varchar(2) DEFAULT NULL,
`birthday` date DEFAULT NULL,
`card_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `id_card` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`number` varchar(255) DEFAULT NULL,
`expiredTime` date DEFAULT NULL,
PRIMARY KEY (`id`)
);
以如下SQL语句做介绍:
select * from person p,id_card card where p.card_id=card.id and name='张三';
上面这个SQL语句一次性查询出了Person和IdCard两个对象的数据,我想让mybatis返回一个Person对象,并且Person对象里面组装好了IdCard对象,现在来看看如何使用标签association达成这个目标。
方法一
在resultMap标签里面嵌套association标签,resultMap表示Person对象,association表示IdCard对象。
<resultMap id="associationResultMap" type="com.crawl.db.domain.Person">
<id column="id" jdbcType="INTEGER" property="id" />
<result column="name" jdbcType="VARCHAR" property="name" />
<result column="sex" jdbcType="VARCHAR" property="sex" />
<result column="birthday" jdbcType="DATE" property="birthday" />
<association property="idCard" column="card_id" javaType="com.crawl.db.domain.IdCard">
<id column="cid" jdbcType="INTEGER" property="id" />
<result column="number" jdbcType="VARCHAR" property="number" />
<result column="expiredTime" jdbcType="DATE" property="expiredtime" />
</association>
</resultMap>
<select id="findPersonByName" parameterType="java.lang.String" resultMap="associationResultMap">
select p.*,card.id as cid,card.number as number,card.expiredTime expiredTime from person p,id_card card where p.card_id=card.id and name=#{name,jdbcType=VARCHAR}
</select>
执行findPersonByName可以得到一个Person对象,且Person对象关联上了对应的IdCard对象。
注意association子标签里面的column属性不能与resultMap里面的column属性有相同的值,否则造成association的对象的属性值赋值错误。而且association标签里面的column属性不是必须的,对于上面的例子也就是column="card_id"
是可以去掉的。
方法二
如果采用方法一,当有另外一个resultMap也想嵌套相同的association时,那么需要在内部再定义一次,这会造成重复,因此可以将相同的association提取出来,写成如下形式:
<resultMap id="associationResultMap" type="com.crawl.db.domain.Person">
<id column="id" jdbcType="INTEGER" property="id" />
<result column="name" jdbcType="VARCHAR" property="name" />
<result column="sex" jdbcType="VARCHAR" property="sex" />
<result column="birthday" jdbcType="DATE" property="birthday" />
<association property="idCard" javaType="com.crawl.db.domain.IdCard" resultMap="cardResultMap"/>
</resultMap>
<resultMap id="cardResultMap" type="com.crawl.db.domain.IdCard">
<id column="cid" jdbcType="INTEGER" property="id" />
<result column="number" jdbcType="VARCHAR" property="number" />
<result column="expiredTime" jdbcType="DATE" property="expiredtime" />
</resultMap>
方法三
方法一和方法二都是一次将所有的属性查询出来,但是可能程序中只需要Person,不需要IdCard,那有没有一种方法可以在需要的时候才查询IdCard?
答案是有。这也是association的第三个应用:关联的嵌套 Select 查询。方法一和方法二叫做关联的嵌套结果映射。
<resultMap id="associationResultMap" type="com.crawl.db.domain.Person">
<id column="id" jdbcType="INTEGER" property="id" />
<result column="name" jdbcType="VARCHAR" property="name" />
<result column="sex" jdbcType="VARCHAR" property="sex" />
<result column="birthday" jdbcType="DATE" property="birthday" />
<association property="idCard" column="card_id" javaType="com.crawl.db.domain.IdCard"
select="findIdCard" fetchType="lazy"/>
</resultMap>
<select id="findPersonByName" parameterType="java.lang.String" resultMap="associationResultMap">
select * from person where name=#{name,jdbcType=VARCHAR}
</select>
<select id="findIdCard" parameterType="INTEGER" resultType="com.crawl.db.domain.IdCard">
SELECT * FROM id_card WHERE id = #{id,jdbcType=INTEGER}
</select>
关联的嵌套 Select 查询是在resultMap嵌入一个association,与方法一不同的是,这里多了三个字段:column,select和fetchType。
- select:加载关联对象的映射语句ID,当需要加载关联对象时,mybatis调用该属性指定的映射语句,从数据库查询出结果,并映射为关联对象;
- column:数据库的列名或者别名,用于指定输入列,select属性映射的SQL语句一般需要参数,比如id,这个参数就等于输入列的值,也就是映射语句根据column指定的输入列进行查询,就上例来说,association执行是SQL语句相当于:
SELECT * FROM id_card WHERE id = card_id
,如果需要指定多个参数,column可以写成:column="{prop1=col1,prop2=col2}"
,其中prop1,prop2是association执行是SQL语句中的参数名; - fetchType:可选的。有效值为 lazy 和 eager。 指定属性后,将在映射中忽略全局配置参数 lazyLoadingEnabled,默认是eager。该属性表示是否将关联SQL一起执行,lazy表示需要关联对象的时候再执行,这也称作延迟加载或者懒加载。
fetchType需要和aggressiveLazyLoading联合使用,aggressiveLazyLoading是在mybatis的配置文件中配置,有两个值:true和false,在3.4.1及之前的版本中默认为 true。当该属性为true时,调用findPersonByName方法,mybatis不会执行任何SQL,如果访问Person对象的任何方法,这时会同时执行两个SQL;当该属性为false时,调用findPersonByName方法,mybatis会执行findPersonByName映射的SQL语句,只有当访问IdCard对象时,才会执行findIdCard映射的SQL语句。
mybatis配置文件中提供了参数lazyLoadingEnabled,它是延迟加载的全局开关。
association标签里面无需再写result标签,mybatis会忽略result标签。
当选择fetchType=lazy
后,执行findPersonByName的SQL语句,得到的Person对象的idCard属性为null,当访问idCard对象的属性的时候,mybatis会先执行findIdCard,根据数据库返回值构建出IdCard对象。
二、association实现原理
上面介绍了association的使用方法,方法一和方法二的原理相对来说比较简单,本文注解介绍方法三的实现原理。
当调用findPersonByName时,mybatis根据findPersonByName的入参解析映射语句和resultMap,将resultMap中的每个子节点解析为一个ResultMapping对象。ResultMapping里面有两个属性比较关键:nestedQueryId和lazy。
- lazy:布尔型,当fetchType=lazy时,该属性为true;
- nestedQueryId:String类,和association标签里面的select属性值一样。
如果ResultMapping对象里面nestedQueryId属性不为null且lazy=true,那么表示需要使用懒加载,当需要关联对象的时候才能执行关联SQL。那么mybatis如何知道什么时候需要关联对象?
答案藏在DefaultResultSetHandler.createResultObject()方法中。mybatis执行SQL语句后,需要将SQL返回值赋值到返回对象中,在赋值之前,需要调用createResultObject()方法创建返回对象,下面看一下这个方法:
private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
this.useConstructorMappings = false;
final List<Class<?>> constructorArgTypes = new ArrayList<Class<?>>();
final List<Object> constructorArgs = new ArrayList<Object>();
//根据resultMap标签的type属性或者resultType属性创建返回对象
//这个调用构造方法直接创建对象
Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
//遍历每个ResultMapping对象
for (ResultMapping propertyMapping : propertyMappings) {
//判断是否需要懒加载
if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
//调用代理工厂创建代理resultObject的代理对象
resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
break;
}
}
}
this.useConstructorMappings = (resultObject != null && !constructorArgTypes.isEmpty()); // set current mapping result
return resultObject;
}
createResultObject()方法如果发现需要懒加载,那么便创建一个代理对象,并且将该代理对象返回给调用方。当需要访问关联SQL的时候,代理对象会检查当前访问的属性是否需要执行关联SQL,如果需要便调用映射ID执行查询。
mybatis提供了两种创建代理的方式:CGLIB和JAVASSIST,默认是JAVASSIST,可以在配置文件中使用proxyFactory修改代理方式:
<setting name="proxyFactory" value="CGLIB"/>