3.2 一对一关联:association 的两种缝法 本节摘要:一对一关联用 association 元素缝合,有嵌套结果(join 一次查询、结果里装配)与嵌套查询(分步查询、可懒加载)两种缝法。本节以"用户带证件"为病例完整演示两种写法,对比各自的 SQL 形态与查询次数,讲清 column 传参与懒加载开关,并给出选择依据。 上一节把单表的列对上了属性。这一节让对象里长出另一个对象——用户实体里那个类型为 IdCard 的字段。 病例背景:查用户要连证件一起出 用 resultType 无法把两表的结果装进嵌套结构——自动映射只认平铺的同名列。这时 association 登场。
本节摘要:一对一关联用 association 元素缝合,有嵌套结果(join 一次查询、结果里装配)与嵌套查询(分步查询、可懒加载)两种缝法。本节以"用户带证件"为病例完整演示两种写法,对比各自的 SQL 形态与查询次数,讲清 column 传参与懒加载开关,并给出选择依据。
上一节把单表的列对上了属性。这一节让对象里长出另一个对象——用户实体里那个类型为 IdCard 的字段。
-- 两张表:一个用户对应一张证件(一对一) CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, id_card_id INT ); CREATE TABLE id_cards ( id INT PRIMARY KEY AUTO_INCREMENT, card_no VARCHAR(30) NOT NULL, -- 证件号 issuing_authority VARCHAR(100) -- 签发机关 );
// 实体:User 内嵌一个 IdCard public class User { private Integer id; private String username; private IdCard idCard; // 关联对象 // 省略 getter 与 setter } public class IdCard { private Integer id; private String cardNo; private String issuingAuthority; // 省略 getter 与 setter }
用 resultType 无法把两表的结果装进嵌套结构——自动映射只认平铺的同名列。这时 association 登场。
一条 SQL 把两表 join 成宽行,ResultMap 声明"哪些列装配进内嵌对象":
<resultMap id="userWithCardMap" type="User"> <id property="id" column="u_id"/> <result property="username" column="username"/> <!-- association:一对一内嵌对象,javaType 指明目标类型 --> <association property="idCard" javaType="IdCard"> <id property="id" column="c_id"/> <result property="cardNo" column="card_no"/> <result property="issuingAuthority" column="issuing_authority"/> </association> </resultMap> <select id="findUserWithCard" parameterType="int" resultMap="userWithCardMap"> SELECT u.id AS u_id, u.username, c.id AS c_id, c.card_no, c.issuing_authority FROM users u LEFT JOIN id_cards c ON u.id_card_id = c.id WHERE u.id = #{id} </select>
User u = mapper.findUserWithCard(1); // 输出:User{id=1, username='zhangsan', // idCard=IdCard{id=5, cardNo='110...X', issuingAuthority='公安局'}} System.out.println(u);
两个细节值得盯住。列别名防撞车:两表都有 id 列,别名 u_id 与 c_id 把它们分开,ResultMap 按别名各取各的。内层也有 id 元素:一对一虽然不涉及多行合并,但写上主键标记是好习惯——它让框架在缓存场景能正确标识这个内嵌对象。
主查询只查用户表,association 声明"拿主查询的某列去执行另一条语句":
<resultMap id="userWithCardStepMap" type="User"> <id property="id" column="id"/> <result property="username" column="username"/> <!-- 嵌套查询:column 指定主查询哪一列传给子查询作参数 --> <association property="idCard" column="id_card_id" select="findCardById" fetchType="lazy"/> </resultMap> <select id="findUserWithCardStep" parameterType="int" resultMap="userWithCardStepMap"> SELECT id, username, id_card_id FROM users WHERE id = #{id} </select> <!-- 子查询:独立语句,可被其他映射复用 --> <select id="findCardById" parameterType="int" resultType="IdCard"> SELECT id, card_no AS cardNo, issuing_authority AS issuingAuthority FROM id_cards WHERE id = #{id} </select>
执行日志会打出两条独立的 SQL:
==> Preparing: SELECT id, username, id_card_id FROM users WHERE id = ? ==> Parameters: 1(Integer) ==> Preparing: SELECT id, card_no, issuing_authority FROM id_cards WHERE id = ? ==> Parameters: 3(Integer)
第二条语句的参数 3 来自第一条结果的 id_card_id 列——这就是 column 传参:把主查询结果的一列,作为子查询的入参。

选择依据压成两句:查列表用嵌套结果(一次往返,join 代价可控);查单条且关联对象不常用时用嵌套查询加懒加载(默认不触发子查询,用到才查)。
fetchType="lazy" 让子查询推迟到真正访问关联属性的那一刻:
User u = mapper.findUserWithCardStep(1); // 此刻日志只有主查询一条 SQL System.out.println(u.getUsername()); // 不触发子查询 System.out.println(u.getIdCard().getCardNo()); // 访问 idCard 属性时,日志才出现子查询那条 SQL
懒加载的实现是给返回对象套一层代理,首次访问关联属性时代理补发子查询。全局开关 lazyLoadingEnabled 可把所有嵌套查询默认置为懒(aggressiveLazyLoading 关闭后,访问非关联属性不触发加载)。一条使用纪律:懒加载的会话必须仍处于打开状态——会话已关闭再访问懒属性,会抛异常。这也是 1.4 节"会话与工作单元同寿"红线的另一个理由。
⚠️ 常见坑:嵌套查询用在列表上。查 20 个用户、每个用户触发一次证件子查询,就是 21 条 SQL——这就是著名的 N 加一问题。6.3 节性能优化会给出完整的检测与消灭手段;本节的结论很简单:列表查询一律嵌套结果。
💡 关键直觉:association 的 column 属性在两种缝法里语义不同——嵌套结果里它是"列的记号"(配合别名),嵌套查询里它是"传给子查询的参数"。看同一个属性名,先分清脚下是哪种缝法。
一对一缝合完毕,下一节难度升一级:一个用户名下多张订单,结果集的行会按主表行数翻倍展开——去重与分页的两个经典坑都在那里等着。