0%

Mybatis中对于association的理解

mybatis是先处理sql,把sql执行完后把结果表映射到resultMap或者resultType上

如下面的:

一个project对应一个user

1
2
3
4
5
6
7
public class Project {
int id;
String name;
User user;
ItemState state;
Set<User> signer;
}
1
2
3
4
5
public class User{
public int id;
public String username;
public String password;
}

表为:

1
2
3
4
5
6
create table project(
id int primary key auto_increment,
name varchar(50),
user_id int,
state enum('created', 'planned', 'started', 'finished'),
)engine=innoDB, charset=utf8;

user表:
+———-+————-+——+—–+———+—————-+
| Field | Type | Null | Key | Default | Extra |
+———-+————-+——+—–+———+—————-+
| id | int(11) | NO | PRI | NULL | auto_increment |
| username | varchar(20) | NO | MUL | NULL | |
| password | varchar(18) | NO | | NULL | |
+———-+————-+——+—–+———+—————-+

当给定一个userid,要查询其对应的project及其对应的user。
首先如果结果集不是简单的POJO(POJO其属性不含其它bean的类型),就不能简单使用resultType,因为复杂映射映射不出来。要使用resultMap,且使用association和collection

下面称association和collection为“复杂映射”

所有的复杂映射都是根据结果表进行映射

如要使用userid去查询project及其对应的user,有以下办法:

用法一:

ProjectMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<mapper namespace="com.ljy.inspector.dao.ProjectDao">

<resultMap id="projectMap" type="com.ljy.inspector.entity.Project">
<id column="id" property="id"/>
<result column="state" property="state" typeHandler="com.ljy.inspector.type_handler.ItemStateHandler"/>
<association column="user_id" property="user" select="com.ljy.inspector.dao.UserDao.findById">
</association>
</resultMap>

<select id="findByUserId" resultMap="projectMap">
select
from project as p
<where>
p.user_id = #{userId}
</where>
</select>
</mapper>

UserMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<mapper namespace="com.ljy.inspector.dao.UserDao">

<resultMap id="UserMap" type="com.ljy.inspector.entity.User">
<id column="id" property="id" jdbcType="INTEGER"/>
<result column="username" property="username" jdbcType="VARCHAR"/>
<result column="password" property="password" jdbcType="VARCHAR"/>
</resultMap>

<select id="findById" resultType="com.ljy.inspector.entity.User">
SELECT * FROM user AS u
<where>
u.id = #{id}
</where>
</select>

</mapper>

association中嵌套select语句,这样的执行顺序是:先执行上层select找出resultMap中除了复杂映射的其他部分,默认开启部分自动映射,因此其他部分会被自动映射,除了有的映射不到,需要使用类型处理器,如ItemState。接着执行association中的select语句,并且将user_id作为参数传入findById,user_id就会被当做findById中的 #{id} (即使名字不一样,但就是这么做的)
这样做的话,会得到正确的结果:

1
2
3
4
5
6
7
8
9
10
{
"id": 10,
"name": "czxczc",
"state": "CREATED",
"user": {
"id": 2,
"username": "123",
"password": "11111111"
},
}

这样做相当于把查询分了两步:
1.在project中先select查询user_id为给定值的所有元组(行),把能自动映射或者指定了类型处理器的属性先映射到Project对象上

2.再根据上述元组中的user_id属性去select查询user表中id为userid中的行,并把查询结果集映射到User对象上,并且把这个User对象赋给上面的Project对象的user属性。

整体就是两步select,根据第一步查询出的结果的某个字段去进行第二步查询。
先构建出第一步查询的结果对象obj1,然后把第二步查询的结果对象obj2赋给obj1对应的属性。

(先得到结果表1,对其进行映射,再使用其中的某字段得到结果表2,再对结果表2进行映射,将映射结果2嵌入映射结果1的属性中。整个过程产生了两张结果表,因为有两个select)

用法二:

ProjectMapper.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
<mapper namespace="com.ljy.inspector.dao.ProjectDao">

<resultMap id="projectMap" type="com.ljy.inspector.entity.Project">
<id column="id" property="id"/>
<result column="state" property="state" typeHandler="com.ljy.inspector.type_handler.ItemStateHandler"/>
<association property="user" javaType="com.ljy.inspector.entity.User">
<id column="u_id" property="id"/>
<result column="username" property="username"/>
</association>
</resultMap>

<sql id="base_user">
p.*, u.id as u_id, u.username, u.password
</sql>

<select id="findByUserId" resultMap="projectMap">
select
<include refid="base_user"/>
from project as p,user as u
<where>
p.user_id = #{userId}
and
u.id = #{userId}
</where>
</select>
</mapper>

结果为:

1
2
3
4
5
6
7
8
9
10
{
"id": 10,
"name": null,
"state": "CREATED",
"user": {
"id": 2,
"username": "123",
"password": null
},
}

这种方法是执行一次select查询,一次查询两个表,然后再对这个联合结果表进行映射,这种方式完全是按照sql的思想理念去做的,只有最后才是对联合结果表进行一次映射。
这种方式的resultMap会自行关闭自动映射,原因在于结果表只有一张,如果两张表中有名称相同的字段,会产生歧义(如project的叫id,user的也叫id)。在sql查询中,这种情况不会有问题,结果表可以有两个列都叫id,但是对于映射来说列名是唯一标签,所以会产生歧义。所以这种情况下,要对有歧义的列名起别名,如上面的sql标签,将user中的id别名为u_id,那么最终产生的结果表中,只有一列叫id,即project.id,而user.id的这一列叫u_id,所以在association中列名叫做u_id。
这样得出一张结果表后,先整体映射为指定的对象类型,再把其中要映射为属性或者需要处理的列去映射成相应的属性。而把需要映射为association的列摘出来,根据提供的对象类型把它们映射成相应的对象,赋给上层的对象的属性

方法二还有一种做法是指定association的resultMap属性,两者其实是一样的。本质上都是从一张结果表中取结果列取映射。

所以对于association需要注意的点:

1.如果使用了select属性,则本质上要进行两次查询,得到两张结果表,因为是从不同的表中查询(相同也无所谓)且每次只查询一个表,所以二者互不干涉,所以不存在字段歧义的问题,自动映射会开启。
2.如果使用resultMap属性,或者association本身当做一个resultMap,则本质是对多张表的一次联合查询,只产生一张结果表。然后对这张结果表一次进行映射。可能存在字段歧义问题,自动映射会关闭(如果没有指明映射关系的字段会得到空值)。此时要把产生歧义的字段起别名,并且把所有需要映射的字段都显式写出来。

所有的mybatis的resultMap,不管其结构有多复杂,本质都是执行完sql后对结果表再进行映射(归根结底要很清晰执行完sql语句后结果表是什么样子),其中的column的名称是结果表的column名称而不是原表的colum名称(虽然不起别名的话二者相同)。只要明确这一点,很多问题就能迎刃而解。