10.1 MyBatis关联映射 10.1.1 一对一联系

10.1 MyBatis关联映射

什么是关联关系

客观世界中的对象很少有孤立存在的,例如班级,往往与班级的学生存在关联关系,如果得到某个班级的实例,那么应该可以直接获取班级对应的全部学生。反过来,如果已经得到一个学生的实例,那么也应该可以访问该学生对应的班级。这种实例之间的互相访问就是关联关系

关联关系分类

关联关系是面向对象分析、面向对象设计最重要的知识,MyBatis完全可以理解这种关联关系,如果映射得当,MyBatis的关联映射将可以大大简化持久层数据的访问。关联关系大致有如下分类。

  • 一对一
  • 一对多
  • 多对多

10.1.1 一对一联系

在实际项目开发中,经常存在一对一关系,比如一个人只能有一个身份证,一个身份证只能给一个人使用,这就是一对一的关系。

一对一关系推荐使用唯一主外键关联,即两张表使用外键关联关系,由于是一对一关联,因此还需要给外键列增加unique唯一约束。下面我们就用一个示例来看看MyBatis怎么处理一对一关系。

示例: OneToOneTest

项目结构

展开/折叠
D:\Desktop\随书源码\Spring+Mybatis企业应用实战(第2版)\codes\10\OneToOneTest
├─lib\
│ └─mysql-connector-java-5.1.44-bin.jar
├─src\
│ ├─db.properties
│ ├─log4j.xml
│ ├─mybatis-config.xml
│ └─org\
│   └─fkit\
│     ├─domain\
│     │ ├─Card.java
│     │ └─Person.java
│     ├─factory\
│     │ └─FKSqlSessionFactory.java
│     ├─mapper\
│     │ ├─CardMapper.xml
│     │ ├─PersonMapper.java
│     │ └─PersonMapper.xml
│     └─test\
│       ├─OneToOneTest.java
│       └─OneToOneTest2.java
└─WebContent\
  ├─META-INF\
  │ └─MANIFEST.MF
  └─WEB-INF\
    ├─lib\
    │ ├─ant-1.9.6.jar
    │ ├─ant-launcher-1.9.6.jar
    │ ├─asm-5.2.jar
    │ ├─cglib-3.2.5.jar
    │ ├─commons-logging-1.2.jar
    │ ├─javassist-3.22.0-CR2.jar
    │ ├─log4j-1.2.17.jar
    │ ├─log4j-api-2.3.jar
    │ ├─log4j-core-2.3.jar
    │ ├─mybatis-3.4.5.jar
    │ ├─mysql-connector-java-8.0.17.jar
    │ ├─ognl-3.1.15.jar
    │ ├─slf4j-api-1.7.25.jar
    │ └─slf4j-log4j12-1.7.25.jar
    └─web.xml

创建数据库表

首先,给之前创建的mybatis数据库创建两个表tb_cardtb_person,并插入测试数据。SQL脚本如下
使用方式:保存成.sql文件,然后使用Navicat导入(设置编码格式utf-8,免得乱码).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
use mybatis;
-- 取消外键检查
SET FOREIGN_KEY_CHECKS=0;

-- 创建tb_card表
DROP TABLE IF EXISTS `tb_card`;
CREATE TABLE `tb_card` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`code` varchar(18) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 插入一条数据
INSERT INTO `tb_card` VALUES ('1', '43280119980091');

-- 创建tb_person表
DROP TABLE IF EXISTS `tb_person`;
CREATE TABLE `tb_person` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(18) NOT NULL,
`sex` varchar(18) NOT NULL,
`age` int(11) DEFAULT NULL,
`card_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `card_id` (`card_id`),
CONSTRAINT `tb_person_ibfk_1` FOREIGN KEY (`card_id`)
REFERENCES `tb_card` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 插入一条数据
INSERT INTO `tb_person` VALUES ('1', '小明', '男', '22', '1');

tb_person表的card_id作为外键参照tb_card表的主键id,因为是一对一关系,即一个card_id只能让一个person使用,所以把card_id做成了唯一键约束。如此一来,person使用了一个card_id之后,其他的person就不能使用该card_id了.
所以,一对一关系就是外键约束加上唯一约束
mybatis数据库中执行SQL脚本,完成创建数据库和表的操作。

创建持久化类

接下来,创建一个Card对象和一个Person对象分别映射tb_cardtb_Person

Card.java

/OneToOneTest/src/org/fkit/domain/Card.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package org.fkit.domain;
import java.io.Serializable;
public class Card implements Serializable
{
private static final long serialVersionUID = 1164164518527094243L;
private Integer id; // 主键id
private String code; // 身份证编号
public Card() {
super();
// TODO Auto-generated constructor stub
}
// 此处省略getter和setter方法,请自己补上
@Override
public String toString()
{
return "Card [id=" + id + ", code=" + code + "]";
}
}

Person.java

/OneToOneTest/src/org/fkit/domain/Person.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package org.fkit.domain;
import java.io.Serializable;
//# 创建tb_person表
//DROP TABLE IF EXISTS `tb_person`;
//CREATE TABLE `tb_person` (
// `id` int(11) NOT NULL AUTO_INCREMENT,
// `name` varchar(18) NOT NULL,
// `sex` varchar(18) NOT NULL,
// `age` int(11) DEFAULT NULL,
// `card_id` int(11) DEFAULT NULL,
// PRIMARY KEY (`id`),
// UNIQUE KEY `card_id` (`card_id`),
// CONSTRAINT `tb_person_ibfk_1` FOREIGN KEY (`card_id`)
// REFERENCES `tb_card` (`id`)
//) ENGINE=InnoDB DEFAULT CHARSET=utf8;
//# 插入一条数据
//INSERT INTO `tb_person` VALUES ('1', '小明', '男', '22', '1');
public class Person implements Serializable
{
private static final long serialVersionUID = -6685680170973264712L;
private Integer id; // 主键id
private String name; // 姓名
private String sex; // 性别
private Integer age; // 年龄
// 人和身份证是一对一的关系,即一个人只有一个身份证
private Card card;
public Person() {
super();
// TODO Auto-generated constructor stub
}
// 此处省略getter和setter方法,请自己补上
@Override
public String toString()
{
return "Person [id=" + id + ", name=" + name + ", sex=" + sex + ", age=" + age + "]";
}
}

人和身份证号码之间是一对一的关系,即一个人只有一个身份证。在Person类中定义了一个card属性,该属性是一个card类型,用来映射一对一的关联关系,表示这个人的身份证。

XML映射文件

再接下来是XML映射文件。

PersonMapper.xml

/OneToOneTest/src/org/fkit/mapper/PersonMapper.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?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">
<!-- namespace指用户自定义的命名空间。 -->
<mapper namespace="org.fkit.mapper.PersonMapper">
<!-- 根据id查询Person,返回resultMap -->
<select
id="selectPersonById"
parameterType="int"
resultMap="personMapper">
SELECT * from tb_person where id = #{id}
</select>
<!-- 映射Person对象的resultMap -->
<resultMap
type="org.fkit.domain.Person"
id="personMapper">
<id
property="id"
column="id"/>
<result
property="name"
column="name"/>
<result
property="sex"
column="sex"/>
<result
property="age"
column="age"/>
<!-- 一对一关联映射:association -->
<association
property="card"
column="card_id"
select="org.fkit.mapper.CardMapper.selectCardById"
javaType="org.fkit.domain.Card"/>
</resultMap>
</mapper>

PersonMapper.xml中定义了一个select标签,其根据id查询Person信息,由于Person类除了简单的属性idnamesexage之外,还有一个关联对象card,所以返回的是一个名为personMapperresultMappersonMapper中使用了association元素映射一对一的关联关系,

  • select属性表示会使用column属性card_id值作为参数执行CardMapper.xml中定义的selectCardById查询对应的card数据.
  • 查询出的数据将被封装到property表示的card对象当中.

CardMapper.xml

/OneToOneTest/src/org/fkit/mapper/CardMapper.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
<?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">
<!-- namespace指用户自定义的命名空间。 -->
<mapper namespace="org.fkit.mapper.CardMapper">
<!-- 根据id查询Card,返回Card对象 -->
<select
id="selectCardById"
parameterType="int"
resultType="org.fkit.domain.Card">
SELECT * from tb_card where id = #{id}
</select>
</mapper>

mapper接口

之前的测试都是使用SqlSession对象调用insertupdatedeleteselect方法进行测试。实际上,Mybatis官方手册建议通过mapper接口的代理对象访问Mybatis,该对象关联了SqlSession对象,开发者可以通过mapper接口的代理对象直接调用方法操作数据库

mapper接口类命名规则

下面定义一个mapper接口对象,需要注意的是:

  • mapper接口的类名必须和之前的XML文件中的mappernamespace一致,
  • mapper接口的方法的方法名必须和XML文件中的select元素的id属性值一致
    • 方法的参数名必须和XML文件中的select元素的parameterType属性值一致。

PersonMapper.java

/OneToOneTest/src/org/fkit/mapper/PersonMapper.java
1
2
3
4
5
6
7
8
9
10
11
12
13
package org.fkit.mapper;

import org.fkit.domain.Person;

public interface PersonMapper
{
/**
* 方法名称要和select元素的id属性一致,参数名必须和select元素的parameterType属性一致.
* @param id
* @return Person对象
*/
Person selectPersonById(Integer id);
}

测试 使用mapper接口 OneToOneTest.java

/OneToOneTest/src/org/fkit/test/OneToOneTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package org.fkit.test;

import org.apache.ibatis.session.SqlSession;
import org.fkit.domain.Person;
import org.fkit.factory.FKSqlSessionFactory;
import org.fkit.mapper.PersonMapper;

public class OneToOneTest {

public static void main(String[] args)
{
// 定义SqlSession变量
SqlSession sqlSession = null;
try
{
// 1.创建SqlSession实例
sqlSession = FKSqlSessionFactory.getSqlSession();
// 2.获得mapper接口的代理对象
PersonMapper pm = sqlSession.getMapper(PersonMapper.class);
// 3.直接调用接口的方法,查询id为1的Peson数据
Person p = pm.selectPersonById(1);
// 4.1打印Peson对象
System.out.println(p);
// 4.2打印Person对象关联的Card对象
System.out.println(p.getCard());
// 5.提交事务
sqlSession.commit();
} catch (Exception e)
{
// 6.回滚事务
sqlSession.rollback();
e.printStackTrace();
} finally
{
// 7.关闭SqlSession
if (sqlSession != null)
sqlSession.close();
}
}

}

运行OneToOneTest类的main方法,通过SqlSessiongetMapper(Class<T> type)方法获得mapper接口的代理对象PersonMapper,调用selectPersonById方法时会执行PersonMapper.xmlid="selectPersonById"select元素中定义的SQL语句。控制台显示如下:

1
2
3
4
5
6
7
8
DEBUG [main] ==>  Preparing: SELECT * from tb_person where id = ? 
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] ====> Preparing: SELECT * from tb_card where id = ?
DEBUG [main] ====> Parameters: 1(Integer)
DEBUG [main] <==== Total: 1
DEBUG [main] <== Total: 1
Person [id=1, name=小明, sex=男, age=22]
Card [id=1, code=43280119980091]

可以看到,查询Person信息时Person对应的Card对象也被查询出来了。

测试 使用mapper文件

/OneToOneTest/src/org/fkit/test/OneToOneTest2.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package org.fkit.test;

import org.apache.ibatis.session.SqlSession;
import org.fkit.domain.Person;
import org.fkit.factory.FKSqlSessionFactory;

public class OneToOneTest2
{
public static void main(String[] args)
{
// 创建SQLSession实例
SqlSession sqlSession = FKSqlSessionFactory.getSqlSession();
try
{
Person p = sqlSession.selectOne("org.fkit.mapper.PersonMapper.selectPersonById", 1);
System.out.println(p);
System.out.println(p.getCard());
// 提交事务
sqlSession.commit();
} catch (Exception e)
{
e.printStackTrace();
sqlSession.rollback();
} finally
{
sqlSession.close();
}
}

}

运行结果:

1
2
3
4
5
6
7
8
DEBUG [main] ==>  Preparing: SELECT * from tb_person where id = ? 
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] ====> Preparing: SELECT * from tb_card where id = ?
DEBUG [main] ====> Parameters: 1(Integer)
DEBUG [main] <==== Total: 1
DEBUG [main] <== Total: 1
Person [id=1, name=小明, sex=男, age=22]
Card [id=1, code=43280119980091]

可以看到,使用Mapper接口方式时,通过鼠标点击就可以跳转到要调用的方法,这种方式调试起来更方便。