Mybatis(三)

  • Mybatis中的连接池以及事务控制(了解原理,会用)
    • Mybatis中连接池使用及分析
    • Mybatis事务控制的分析
  • Mybatis基于XML配置的动态SQL语句(会用即可)
    • mappers配置文件中的几个标签:<if><where><foreach><sql>
  • Mybatis中的多表操作
    • 一对一(多对一)
    • 一对多
    • 多对多

Mybatis中的连接池以及事务控制

  • Mybatis连接池技术
  • Mybatis事务控制

Mybatis连接池技术

我们在前面的 WEB 学习中我们已经接触过过类似的连接池技术(数据库连接池笔记),而在 Mybatis 中也有连接池技术,但是它采用的是自的连接池技术。在 Mybatis 的 SqlMapConfig.xml 配置文件中,通过<dataSource type="pooled">来实现 Mybatis 中连接池的配置。(环境配置(environments)

我们在实际开发中都会使用连接池,它可以减少我们获取连接所消耗的时间

连接池介绍

视频讲解

Mybatis中的连接池

数据源(dataSource)

Mybatis连接池提供了3种方式的配置。

主配置文件SqlMapConfig.xml中的**dataSource标签**,type属性就是表示采用何种连接池方式。如:<dataSource type="POOLED">

type属性的取值:

  • POOLED:采用传统的javax.sql.DataSource规范中的连接池,mybatis中有针对规范的实现
  • UNPOOLED:采用传统的获取连接的方式,虽然也实现Javax.sql.DataSource接口,但是并没有使用池的思想
  • JNDI:采用服务器提供的JNDI技术实现,来获取DataSource对象,不同的服务器所能拿到DataSource是不一样

注意:如果不是web或者maven的war工程,是不能使用的。(我们学习时使用的是tomcat服务器,采用连接池是dbcp连接池)

例子:在之前的学习中,我们的SqlMapConfig.xml文件中的相关配置如下:

1
2
3
4
5
6
7
<dataSource type="POOLED">
<!--配置连接数据库的四个基本信息-->
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>

MyBatis 在初始化时,根据<dataSource>的 type 属性来创建相应类型的的数据源 DataSource,即:

type=”POOLED”:MyBatis 会创建 PooledDataSource 实例

type=”UNPOOLED” : MyBatis 会创建 UnpooledDataSource 实例

type=”JNDI”:MyBatis 会从 JNDI 服务上查找 DataSource 实例,然后返回使用

这里我们就只介绍一下POOLED和UNPOOLED,JNDI可以自行查看。在后续开发中使用更多的还是POOLED

Mybatis中使用unpooled配置连接池的原理分析

UNPOOLED– 这个数据源的实现会每次请求时打开和关闭连接。虽然有点慢,但对那些数据库连接可用性要求不高的简单应用程序来说,是一个很好的选择。 性能表现则依赖于使用的数据库,对某些数据库来说,使用连接池并不重要,这个配置就很适合这种情形。UNPOOLED 类型的数据源仅仅需要配置以下 5 种属性:

  • driver – 这是 JDBC 驱动的 Java 类全限定名(并不是 JDBC 驱动中可能包含的数据源类)
  • url – 这是数据库的 JDBC URL 地址
  • username – 登录数据库的用户名
  • password – 登录数据库的密码
  • defaultTransactionIsolationLevel – 默认的连接事务隔离级别
  • defaultNetworkTimeout – 等待数据库操作完成的默认网络超时时间(单位:毫秒)

作为可选项,你也可以传递属性给数据库驱动。只需在属性名加上“driver.”前缀即可,例如:

  • driver.encoding=UTF8

这将通过 DriverManager.getConnection(url, driverProperties) 方法传递值为 UTF8encoding 属性给数据库驱动。

UNPOOLED和UNPOOLED分析

视频讲解

Mybatis中使用pooled配置连接池的原理分析

POOLED– 这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。 这种处理方式很流行,能使并发 Web 应用快速响应请求。

除了上述提到 UNPOOLED 下的属性外,还有更多属性用来配置 POOLED 的数据源: (UNPOOLED 的属性POOLED也有

  • poolMaximumActiveConnections – 在任意时间可存在的活动(正在使用)连接数量,默认值:10
  • poolMaximumIdleConnections – 任意时间可能存在的空闲连接数
  • poolMaximumCheckoutTime – 在被强制返回之前,池中连接被检出(checked out)时间,默认值:20000 毫秒(即 20 秒)
  • poolTimeToWait – 这是一个底层设置,如果获取连接花费了相当长的时间,连接池会打印状态日志并重新尝试获取一个连接(避免在误配置的情况下一直失败且不打印日志),默认值:20000 毫秒(即 20 秒)
  • poolMaximumLocalBadConnectionTolerance – 这是一个关于坏连接容忍度的底层设置, 作用于每一个尝试从缓存池获取连接的线程。 如果这个线程获取到的是一个坏的连接,那么这个数据源允许这个线程尝试重新获取一个新的连接,但是这个重新尝试的次数不应该超过 poolMaximumIdleConnectionspoolMaximumLocalBadConnectionTolerance 之和。 默认值:3(新增于 3.4.5)
  • poolPingQuery – 发送到数据库的侦测查询,用来检验连接是否正常工作并准备接受请求。默认是“NO PING QUERY SET”,这会导致多数数据库驱动出错时返回恰当的错误消息
  • poolPingEnabled – 是否启用侦测查询。若开启,需要设置 poolPingQuery 属性为一个可执行的 SQL 语句(最好是一个速度非常快的 SQL 语句),默认值:false
  • poolPingConnectionsNotUsedFor – 配置 poolPingQuery 的频率。可以被设置为和数据库连接超时时间一样,来避免不必要的侦测,默认值:0(即所有连接每一时刻都被侦测 — 当然仅当 poolPingEnabled 为 true 时适用)

mybatis_pooled的过程

视频讲解

Mybatis中使用JNDI配置连接池的原理分析

JNDI是 Java 命名与目录接口(Java Naming and Directory Interface),在J2EE规范中是重要的规范之一,不少专家认为,没有透彻理解JNDI的意义和作用,就没有真正掌握J2EE特别是EJB的知识。

JNDI是什么,怎么理解

视频讲解

Mybatis中的事务原理和自动提交设置

在之前MySQL的笔记中我们已经接触过事务了,这里我们可以根据之前的笔记再去了解一下:事务的四大特性、不考虑隔离性会产生的3个问题以及解决办法(四种隔离级别),它是通过sqlsession对象的commit方法和rollback方法实现事务的提交和回滚

事务管理器(transactionManager)

在SQLMapConfig中配置事务管理的是<transactionManager type="JDBC"></transactionManager>

在 MyBatis 中有两种类型的事务管理器(即type有两种取值):

  • JDBC – 这个配置直接使用了 JDBC 的提交和回滚设施,它依赖从数据源获得的连接来管理事务作用域

  • MANAGED – 这个配置几乎没做什么。它从不提交或回滚一个连接,而是让容器来管理事务的整个生命周期(比如 JEE 应用服务器的上下文)。 默认情况下它会关闭连接。然而一些容器并不希望连接被关闭,因此需要将 closeConnection 属性设置为 false 来阻止默认的关闭行为。例如:

    1
    2
    3
    <transactionManager type="MANAGED">
    <property name="closeConnection" value="false"/>
    </transactionManager>

提示:如果你正在**使用 Spring + MyBatis,则没有必要配置事务管理器**,因为 Spring 模块会使用自带的管理器来覆盖前面的配置。

在我们之前的之前的增删改操作过程中,我们都要手动进行事务的提交,即要写上session.commit();。原因是 setAutoCommit()方法,在执行时它的值被设置为 false 了,所以我们在增删改操作中,必须通过 sqlSession.commit()方法来执行提交操作。

想要设置事务的自动提交其实非常简单,只需要**将原来的session = factory.openSession();代码改成session = factory.openSession(true);**即可。

视频讲解


Mybatis的动态SQL语句

官网:动态 SQL

W3C教程:MyBatis 动态SQL

动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号(在之前的Maven项目之黑马旅游网中,我们就用到了sql拼接)。利用动态 SQL,可以彻底摆脱这种痛苦。

使用动态 SQL 并非一件易事,但借助可用于任何 SQL 映射语句中的强大的动态 SQL 语言,MyBatis 显著地提升了这一特性的易用性。

借助功能强大的基于 OGNL 的表达式,MyBatis 3 替换了之前的大部分元素,大大精简了元素种类,现在要学习的元素种类比原来的一半还要少:

  • <if>
  • <choose> (<when>, <otherwise>)
  • <trim> (<where>, <set>)
  • <foreach>

动态SQL之<if>标签

官网:if

我们根据实体类的不同取值,使用不同的 SQL 语句来进行查询。比如在 id 如果不为空时可以根据 id 查询,如果 username 不同空时还要加入用户名作为条件。这种情况在我们的多条件组合查询中经常会碰到(在之前的Maven项目之黑马旅游网中,我们就这样使用过了,即客户端想服务端传递cid和cname两个参数,cname可能为空(即用户没有搜索))

举个例子,我们要写一个根据条件查询的接口,条件有可能是用户名,有可能是性别,或者都有。在以前我们可能需要用到SQL拼接,但是现在我们只需要借助Mybatis的动态SQL之**<if>标签**就可以完成。

首先在持久层接口(UserDao)添加代码如下:

1
2
3
4
5
6
/**
* 根据传入的条件查询用户
* @param user 查询的条件,有可能是用户名,有可能是性别,或者都有
* @return
*/
List<User> findByCondition(User user);

然后在映射配置文件(UserDao.xml)中添加代码如下:

1
2
3
4
5
6
7
8
9
10
<!--根据条件查询用户-->
<select id="findByCondition" resultType="top.qing.domain.User">
select * from user where 1 = 1
<if test="username != null and username != '' ">
and username like #{username}
</if>
<if test="sex != null and sex != '' ">
and sex = #{sex}
</if>
</select>

注意:<if>标签的 test 属性中写的是对象的属性名,如果是包装类的对象要使用 OGNL 表达式的写法

最后在测试类中(执行代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void testFindByCondition() throws IOException {
User user1 = new User();
User user2 = new User();
user1.setUsername("%王%");
user2.setUsername("%王%");
user2.setSex("男");
// 使用useDao代理对象执行方法
List<User> users1 = userDao.findByCondition(user1);
List<User> users2 = userDao.findByCondition(user2);
System.out.println("users1结果为:");
for (User user : users1) {
System.out.println(user);
}
System.out.println("users2结果为:");
for (User user : users2) {
System.out.println(user);
}
}

结果如下:

动态SQL之<where>标签

官网:trim、where、set

为了简化上面 where 1=1 的条件拼装,我们可以采用<where>标签来简化开发。

还是上面的例子,只不过这次我们使用<where>标签

只需要将映射配置文件(UserDao.xml)中添加修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--根据条件查询用户-->
<select id="findByCondition" resultType="top.qing.domain.User">
select * from user
<where>
<if test="username != null and username != '' ">
and username like #{username}
</if>
<if test="sex != null and sex != '' ">
and sex = #{sex}
</if>
</where>

</select>

结果和上面是一样的:

动态SQL之<foreach>标签

动态 SQL 的另一个常见使用场景是对集合进行遍历(尤其是在构建 IN 条件语句的时候

需求:传入多个 id 查询用户信息,用下边两个 sql 实现:

SELECT * FROM USERS WHERE username LIKE '%张%' AND (id =10 OR id =89 OR id=16)

SELECT * FROM USERS WHERE username LIKE '%张%' AND id IN (10,89,16)

这样我们在进行范围查询时,就要将一个集合中的值,作为参数动态添加进来。

这样我们将如何进行参数的传递?

这种情况就需要使用<foreach>标签了

首先在QueryVo类中添加属性private List<Integer> ids;

首先在持久层接口(UserDao)添加代码如下:

1
2
3
4
5
6
/**
* 根据QueryVo中提供的id集合,查询用户信息
* @param vo
* @return
*/
List<User> findUserInIds(QueryVo vo);

然后在映射配置文件(UserDao.xml)中添加代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
<!--据QueryVo中提供的id集合,查询用户信息-->
<select id="findUserInIds" resultType="top.qing.domain.User">
select * from user
<where>
<if test="ids != null and ids.size() > 0">
<foreach collection="ids" open=" id in (" close=")" separator="," item="uid" index="index">
#{uid}
</foreach>
</if>
</where>

</select>

这里我们要执行的语句的SQL 语句为:select id from user where id in (?)。而<foreach>标签内的相关语句就是为了拼接sql语句中select * from user后面的内容

<foreach>标签用于遍历集合,它的属性有:

  • collection:代表要遍历的集合元素,注意编写时不要写#{}(这里我们要遍历QueryVo对象中的属性ids(一个List集合),所以写ids)
  • open:代表语句的开始部分(这里我们以 id in (作为开头)
  • close:代表结束部分(这里我们以)结尾)
  • item:代表遍历集合的每个元素,生成的变量名(这里我们取名uid,所以在<foreach>标签里写上#{uid}
  • sperator:代表分隔符(这里我们以,作为分隔符)

foreach标签的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的**集合项(item)索引(index)**变量。它也允许你指定开头与结尾的字符串以及集合项迭代之间的分隔符。这个元素也不会错误地添加多余的分隔符

提示: 你可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数传递给 foreach

  • 当使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素
  • 当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值

最后在测试类中(执行代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testFindUserInIds() throws IOException {
QueryVo queryVo = new QueryVo();
List<Integer> ids = new ArrayList<Integer>();
ids.add(43);
ids.add(46);
ids.add(48);
queryVo.setIds(ids);
// 使用useDao代理对象执行方法
List<User> users = userDao.findUserInIds(queryVo);
for (User user : users) {
System.out.println(user);
}
}

结果如下:

动态SQL之<sql>标签

在映射配置文件(User.xml)中,可以发先有一些重复的sql。可以利用<sql>标签将重复的 sql 提取出来,使用时用 <include>标签 引用即可,最终达到 sql 重用的目的。

例子:在映射配置文件中修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<sql id="defaultSql">
select * from user
</sql>

<!--配置查询所有结果-->
<select id="findAll" resultType="top.qing.domain.User">
<include refid="defaultSql"></include>
</select>

<!--据QueryVo中提供的id集合,查询用户信息-->
<select id="findUserInIds" resultType="top.qing.domain.User">
<include refid="defaultSql"></include>
<where>
<if test="ids != null and ids.size() > 0">
<foreach collection="ids" open=" id in (" close=")" separator="," item="uid" index="index">
#{uid}
</foreach>
</if>
</where>

</select>

其中<sql>标签的id属性和<include>标签的refid属性很容易就可以看出是什么作用。


Mybatis多表查询

视频讲解

关于数据库中的表与表之间的关系我们在之前的笔记中已经提到过了,这里我再简单提一下:

表之间的关系:

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

举例:

  • 用户和订单就是一对多(一个用户可以下多个订单)
  • 订单和用户就是多对一(多个订单属于同一个用户)
  • 人和身份证号就是一对一(一个人只能有一个身份证号,一个身份证号只能属于一个人)
  • 老师和学生之间就是多对多(一个学生可以被多个老师教过,一个老师可以交多个学生)

注意:我们拿出每一个订单,都只能属于一个用户,所以**Mybatis就把多对一看成了一对一**

Mybatis多表查询之一对一查询(多对一)

本次案例主要以最为简单的用户和账户的模型来分析 Mybatis 多表关系。用户为 user 表,账户为account表。一个用户(User)可以有多个账户(Account)。具体关系如下:

  • 一个用户可以有多个账户
  • 一个账户只能属于一个用户(多个账户也可以属于同一个用户)

步骤:

  1. 建立两张表:用户表,账户表。让用户表和账户表之间具备一对多的关系:需要在账户表中添加外键
  2. 建立两个实体类:用户实体类和账户实体类。让用户和账户的实体类能体现出来一对多的关系(即用户实体类中要有一个账户实体类集合的属性)
  3. 建立两个映射配置文件:
    • 用户的配置文件(UserDao.xml)
    • 账户的配置文件(AccountDao.xml)
  4. 实现配置:
    • 当我们查询用户时,可以同时得到用户下所包含的账户信息
    • 当我们查询账户时,可以同时得到账户的所属用户信息

需求:查询所有账户信息,关联查询下单用户信息(多表查询)

注意:因为一个账户信息只能供某个用户使用,所以从查询账户信息出发关联查询用户信息为一对一查询。如果从用户信息出发查询用户下的账户信息则为一对多查询,因为一个用户可以有多个账户。

  • 方法一:定义专门的PO类作为输出类型,其中定义了 sql 查询结果集所有的字段(此方法较为简单,企业中使用普遍
  • 方法二:使用 resultMap,定义专门的 resultMap 用于映射一对一查询结果

方法一

定义专门的PO类作为输出类型,其中定义了 sql 查询结果集所有的字段(此方法较为简单,企业中使用普遍)

首先定义账户信息的实体类Account,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
package top.qing.domain;

import lombok.Data;

import java.io.Serializable;

@Data
public class Account implements Serializable {
private Integer ID;
private Integer UID;
private Double MONEY;

}

我们要查询账户信息以及账户信息所对应的用户信息(用户信息中不需要id,因为id就是account表中的uid,同时为了方便打印我们也去掉birthday信息),SQL语句中是这样的:

1
2
3
4
5
SELECT
account.*,user.username,`user`.sex,user.address
FROM
account,user
WHERE account.uid = user.id

结果为:

接下来我们要定义一个PO类AccountUser,用于封装上面 SQL 语句的查询结果。由于结果中既包含账户信息同时还要包含用户信息,所以我们要在定义AccountUser类时可以继承 Account类(因为Account类的所有属性都要封装到结果,而User类中只是部分属性)。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package top.qing.domain;

import lombok.Data;

import java.util.Date;

@Data
public class AccountUser extends Account {
private String username;
private String sex;
private String address;

@Override
public String toString() {
return super.toString() + " AccountUser{" +
"username='" + username + '\'' +
", sex='" + sex + '\'' +
", address='" + address + '\'' +
'}';
}
}

首先在持久层接口添加代码如下:

1
2
3
4
5
/**
* 查询所有账户信息,同时还要获取当前账户所属用户的相关信息(多表查询)
* @return
*/
List<AccountUser> findAllAccount();

然后在映射配置文件中添加代码如下:

1
2
3
4
5
6
7
8
<!--查询所有账户信息,同时还要获取当前账户所属用户的相关信息,除去id和birthday(多表查询)-->
<select id="findAllAccount" resultType="top.qing.domain.AccountUser">
SELECT
account.*,user.username,`user`.sex,user.address
FROM
account,user
WHERE account.uid = user.id
</select>

最后在测试类中(执行代码):

1
2
3
4
5
6
7
8
9
// 注意别忘了init()方法和destroy()方法
@Test
public void testFindAllAccount() {
// 使用代理对象accountDao执行查询所有方法
List<AccountUser> allAccount = accountDao.findAllAccount();
for (AccountUser accountUser : allAccount) {
System.out.println(accountUser);
}
}

结果如下:

方式二

使用resultMap定义专门的resultMap 用于映射一对一查询结果

首先修改Account类,在Account类中添加一个User类的对象作为属性,修改后的Account类代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package top.qing.domain;

import lombok.Data;

import java.io.Serializable;

@Data
public class Account implements Serializable {
private Integer ID;
private Integer UID;
private Double MONEY;

// 多对一(一对一)关系映射
private User user;
}

这里我们修改findAll()方法,在持久层接口(UserDao)修改代码如下:

1
2
3
4
5
/**
* 查询所有账户信息(包括账户所对应的用户的信息)
* @return
*/
List<Account> findAll();

然后在映射配置文件(UserDao.xml)中添加代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!--定义封装account和user的resultMap-->
<resultMap id="accountUserMap" type="top.qing.domain.Account">
<!--主键字段的对应-->
<id property="ID" column="aid"/>
<!--非主键字段的对应-->
<result property="UID" column="UID"/>
<result property="MONEY" column="MONEY"/>
<!--一对一的关系映射,配置封装user的内容-->
<!--<association>标签:一个复杂类型的关联,许多结果将包装成这种类型-->
<!--简单地说,用于指定从表的应用实体(这里当然就是user)的属性-->
<association property="user">
<id property="id" column="id"></id>
<result property="username" column="username"></result>
<result property="sex" column="sex"></result>
<result property="birthday" column="birthday"></result>
<result property="address" column="address"></result>
</association>
</resultMap>

<!--查询所有账户信息(包括账户所对应的用户的信息)-->
<select id="findAll" resultMap="accountUserMap">
select u.*,a.id as aid,a.uid,a.money from account a,user u where a.uid =u.id;
</select>

注意:**这里的<association>标签下的<id property="id" column="id"></id>等语句是爆红的,但是并不影响最后的运行结果!**写成<id property="user.id" column="id"></id>虽然xml文件不会报错,但是无法正常运行!!

这里的xml文件爆红并不会影响最后结果的运行!

之前的笔记中已经提到过resultMap了,这里再补充一下

结果映射<resultMap>标签下的相关标签:

  • <constructor>:用于在实例化类时,注入结果到构造方法中
    • <idArg>:ID 参数;标记出作为 ID 的结果可以帮助提高整体性能
    • <arg>:将被注入到构造方法的一个普通结果
  • <id>: 一个 ID 结果;标记出作为 ID 的结果可以帮助提高整体性能**(通常用于指定主键字段)**
  • <result>:注入到字段或 JavaBean 属性的普通结果**(通常用于指定非主键字段)**
  • <association>一个复杂类型的关联;许多结果将包装成这种类型
    • 嵌套结果映射:关联可以是 resultMap 元素,或是对其它结果映射的引用
  • <collection>一个复杂类型的集合
    • 嵌套结果映射:集合可以是 resultMap 元素,或是对其它结果映射的引用
  • <discriminator>:使用结果值来决定使用哪个 resultMap
    • <case>:基于某些值的结果映射
      • 嵌套结果映射:case 也是一个结果映射,因此具有相同的结构和元素;或者引用其它的结果映射

具体可查看官网:结果映射

关于<association>更详细的说明可查看官网:关联

关于<collection>更详细的说明可查看官网:集合

最后在测试类中(执行代码):

1
2
3
4
5
6
7
8
9
@Test
public void testFindAll(){
// 使用代理对象accountDao执行查询所有方法
List<Account> accounts = accountDao.findAll();
for (Account account : accounts) {
System.out.println("-------------每个account的信息(含所对应的user信息)---------------");
System.out.println(account);
}
}

结果如下:

Mybatis多表查询之一对多查询

需求:查询所有用户信息及用户关联的账户信息

分析:用户信息和他的账户信息为一对多关系并且查询过程中如果用户没有账户信息,此时也要将用户信息查询出来,我们想到了左外连接查询比较合适

对应的sql语句为:

1
SELECT * FROM `user` LEFT JOIN account ON `user`.id = account.UID

查询结果为:

修改User类:(添加了一个Account集合)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package top.qing.domain;

import lombok.Data;

import java.io.Serializable;
import java.util.Date;
import java.util.List;

@Data
public class User implements Serializable {
private Integer id;
private String username;
private Date birthday;
private String sex;
private String address;

// 一对多关系映射(一个用户可能有多个账单)
private List<Account> accounts;
}

首先在持久层接口(UserDao)添加代码如下:

1
2
3
4
5
/**
* 查询所有用户(如果用户有账单,要把账单信息也查询出来)
* @return
*/
List<User> findAll();

然后在映射配置文件(UserDao.xml)中添加代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<resultMap id="userAccountMap" type="top.qing.domain.User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="birthday" column="birthday"/>
<result property="sex" column="sex"/>
<result property="address" column="address"/>
<!--collection 是用于建立一对多中集合属性的对应关系,ofType 用于指定集合的元素数据类型-->
<collection property="accounts" ofType="top.qing.domain.Account">
<id property="ID" column="aid"/>
<result property="UID" column="UID"/>
<result property="MONEY" column="MONEY"/>
</collection>
</resultMap>

<!--配置查询所有结果-->
<select id="findAll" resultMap="userAccountMap">
SELECT account.ID aid,account.UID,account.MONEY,`user`.* FROM `user` LEFT JOIN account ON `user`.id = account.UID;
</select>

<collection>,部分定义了用户关联的账户信息。表示关联查询结果集

  • property=“accounts”,表示关联查询的结果集存储在 User 对象的上哪个属性(我们这里当然是User对象的accounts属性)
  • ofType=“top.qing.domain.Account”,指定关联查询的结果集中的对象类型即List中的对象类型。此处可以使用别名,也可以使用全限定名(我们这里accounts里的元素类型是Account,所以就写Account的全限定类名了)

最后在测试类中(执行代码):

1
2
3
4
5
6
7
8
@Test
public void testFindAll() throws IOException {
// 使用刚刚创建的代理对象userDao执行查询所有方法
List<User> users = userDao.findAll();
for (User user : users) {
System.out.println(user);
}
}

结果如下:

小结:在<resultMap>

  • 多对一(一对一):使用<association>标签
  • 一对多:使用<collection>标签

Mybatis多表查询之多对多查询

上面我们使用 Mybatis实现了一对多关系的维护。多对多关系其实我们看成是双向的一对多关系

多对多关系举例:用户和角色

  • 一个用户可以有多个角色
  • 一个角色可以赋予多个用户

步骤:

  1. 建立两张表:用户表,角色表(让用户表和角色表具有多对多的关系。需要使用中间表,中间表中包含各自的主键,在中间表中是外键
  2. 建立两个实体类:用户实体类和角色实体类(让用户和角色的实体类能体现出来多对多的关系各自包含对方一个集合引用
  3. 建立两个配置文件
    • 用户的配置文件
    • 角色的配置文件
  4. 实现配置:
    • 当我们查询角色时,可以同时得到角色的所赋予的用户信息
    • 当我们查询用户时,可以同时得到用户所包含的角色信息

本次案例用户与角色的多对多关系模型如下:

对应的SQL语句为:

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
DROP TABLE IF EXISTS `role`;

CREATE TABLE `role` (
`ID` int(11) NOT NULL COMMENT '编号',
`ROLE_NAME` varchar(30) default NULL COMMENT '角色名称',
`ROLE_DESC` varchar(60) default NULL COMMENT '角色描述',
PRIMARY KEY (`ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;



insert into `role`(`ID`,`ROLE_NAME`,`ROLE_DESC`) values (1,'院长','管理整个学院'),(2,'总裁','管理整个公司'),(3,'校长','管理整个学校');


DROP TABLE IF EXISTS `user_role`;

CREATE TABLE `user_role` (
`UID` int(11) NOT NULL COMMENT '用户编号',
`RID` int(11) NOT NULL COMMENT '角色编号',
PRIMARY KEY (`UID`,`RID`),
KEY `FK_Reference_10` (`RID`),
CONSTRAINT `FK_Reference_10` FOREIGN KEY (`RID`) REFERENCES `role` (`ID`),
CONSTRAINT `FK_Reference_9` FOREIGN KEY (`UID`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert into `user_role`(`UID`,`RID`) values (41,1),(45,1),(41,2);

然后创建User和Role的实体类,分别为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package top.qing.domain;

import lombok.Data;

import java.io.Serializable;
import java.util.Date;
import java.util.List;

@Data
public class User implements Serializable {
private Integer id;
private String username;
private Date birthday;
private String sex;
private String address;

// 实现多对多关系(其实可以看做是双向一对多)
private List<Role> roles;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package top.qing.domain;

import lombok.Data;

import java.io.Serializable;
import java.util.List;

@Data
public class Role implements Serializable {
private Integer roleId;
private String roleName;
private String roleDesc;

// 实现多对多关系(其实可以看做是双向一对多)
private List<User> users;
}

注意:这里在Role实体类中我们并没有完全对应数据库的字段名,而是使用了Java的命名规范(即小驼峰命名),如数据库字段名ROLE_NAME在Java实体类属性中命名为roleName,所以后续就需要使用<resultMap>

查询角色同时得到角色的所赋予的用户信息

首先来看一下SQL语句。注意我们这里是先查询角色,然后在查询该角色所赋予的用户信息重点在角色,即所有角色都要查询到,如果有用户信息就查出用户信息,没有用户信息就返回null

所以就不能简单地使用内连接查询了,而应该使用两次外连接。SQL语句如下:

1
2
3
4
SELECT role.ID AS rid, role.ROLE_NAME, role.ROLE_DESC, `user`.*
FROM role
LEFT JOIN user_role ON role.ID = user_role.RID
LEFT JOIN `user` ON `user`.id = user_role.UID

首先在持久层接口(RoleDao)添加代码如下:

1
2
3
4
5
/**
* 查询所有角色,同时得到角色的所赋予的用户信息
* @return
*/
List<Role> findAll();

然后在映射配置文件(RoleDao.xml)中添加代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!--注意别忘了配置resultMap,因为我们Role实体类的属性和数据库字段名称不一致-->
<resultMap id="roleMap" type="top.qing.domain.Role">
<id property="roleId" column="rid"/>
<result property="roleName" column="ROLE_NAME"/>
<result property="roleDesc" column="ROLE_DESC"/>
<collection property="users" ofType="top.qing.domain.User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="birthday" column="birthday"/>
<result property="sex" column="sex"/>
<result property="address" column="address"/>
</collection>
</resultMap>


<!--查询所有角色,同时得到角色的所赋予的用户信息-->
<select id="findAll" resultMap="roleMap">
SELECT role.ID AS rid, role.ROLE_NAME, role.ROLE_DESC, `user`.*
FROM role
LEFT JOIN user_role ON role.ID = user_role.RID
LEFT JOIN `user` ON `user`.id = user_role.UID
</select>

最后在测试类中(执行代码):

1
2
3
4
5
6
7
8
// 别忘了init()和destroy()方法
@Test
public void testFindAll() throws IOException {
List<Role> roleList = roleDao.findAll();
for (Role role : roleList) {
System.out.println(role);
}
}

结果如下:

查询用户同时得到用户所包含的角色信息

首先还是先看一下SQL语句。大体思路同上,只不过这一次主体是用户。SQL如下:

1
2
3
4
SELECT `user`.*, role.ID AS rid, role.ROLE_NAME, role.ROLE_DESC
FROM `user`
LEFT JOIN user_role ON `user`.id = user_role.UID
LEFT JOIN role ON role.ID = user_role.RID;

结果如下:

首先在持久层接口(UserDao)添加代码如下:

1
2
3
4
5
/**
* 查询所有用户(如果用户有角色,要把角色信息也查询出来)
* @return
*/
List<User> findAll();

然后在映射配置文件(UserDao.xml)中添加代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 <resultMap id="userMap" type="top.qing.domain.User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="birthday" column="birthday"/>
<result property="sex" column="sex"/>
<result property="address" column="address"/>
<collection property="roles" ofType="top.qing.domain.Role">
<id property="roleId" column="rid"/>
<result property="roleName" column="ROLE_NAME"/>
<result property="roleDesc" column="ROLE_DESC"/>
</collection>
</resultMap>


<!--配置查询所有结果-->
<select id="findAll" resultMap="userMap">
SELECT `user`.*, role.ID AS rid, role.ROLE_NAME, role.ROLE_DESC
FROM `user`
LEFT JOIN user_role ON `user`.id = user_role.UID
LEFT JOIN role ON role.ID = user_role.RID;
</select>

最后在测试类中(执行代码):

1
2
3
4
5
6
7
8
9
// 别忘了init()和destroy()方法
@Test
public void testFindAll() throws IOException {
// 使用刚刚创建的代理对象userDao执行查询所有方法
List<User> users = userDao.findAll();
for (User user : users) {
System.out.println(user);
}
}

结果如下:

(Mybatis会将重复结果智能放在一起)