mybatis解析-association实现原理详解

本文详细介绍了MyBatis中association标签的三种用法,包括嵌套结果映射、独立resultMap以及延迟加载。通过示例展示了如何在映射文件中配置association,以实现对象关系的映射,包括Person与IdCard类的一对一关系。同时,解释了association实现延迟加载的原理,涉及代理对象的创建和懒加载机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文基于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"/>
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值