第三天:mybatis的深入和多表
mybatis框架 学习计划
共四天
第一天:mybatis入门
mybatis的概述
mybatis的环境搭建
mybatis入门案例
自定义mybatis框架(主要的目的是为了让大家了解mybatis中执行细节)
第二天:mybatis基本使用
mybatis的单表crud操作
mybatis的参数和返回值
mybatis的dao编写
mybatis配置的细节
几个标签的使用
第三天:mybatis的深入和多表
mybatis的连接池
mybatis的事务控制及设计的方法
mybatis的多表查询
一对多(多对一)
多对多
第四天:mybatis的缓存和注解开发
mybatis中的加载时机(查询的时机)
mybatis中的一级缓存和二级缓存
mybatis的注解开发
单表CRUD
多表查询
3.1 Mybatis 连接池与事务深入
3.1.1 Mybatis 的连接池技术
我们在前面的 WEB 课程中也学习过类似的连接池技术,而在 Mybatis 中也有连接池技术,但是它采用的是自己的连接池技术。在 Mybatis 的 SqlMapConfig.xml 配置文件中,通过<dataSource type=”pooled”>
来实现 Mybatis 中连接池的配置。
Mybatis 连接池的分类
3.1.1.1 Mybatis 连接池的分类
在 Mybatis 中我们将它的数据源 dataSource
分为以下几类:
UNPOOLED
不使用连接池的数据源
POOLED
使用连接池的数据源
JNDI
使用 JNDI 实现的数据源
相应地,MyBatis 内部分别定义了实现了 java.sql.DataSource 接口的 UnpooledDataSource,
PooledDataSource 类来表示 UNPOOLED、POOLED 类型的数据源。
在这三种数据源中,我们一般采用的是 POOLED 数据源(很多时候我们所说的数据源就是为了更好的管理数据库连接,也就是我们所说的连接池技术)。
Mybatis 中数据源的配置
3.1.1.2 Mybatis 中数据源的配置
我们的数据源配置就是在 SqlMapConfig.xml
文件中,具体配置如下:
<!-- 配置数据源(连接池)信息 -->
<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 实例,然后返回使用
3.1.1.3 Mybatis 中 DataSource
的存取
MyBatis 是 通 过 工 厂 模 式 来 创 建 数 据 源 DataSource 对 象 的 , MyBatis 定 义 了 抽 象 的 工 厂 接口 :org.apache.ibatis.datasource.DataSourceFactory
,通过其 getDataSource()
方法返回数据源DataSource
。
下面是 DataSourceFactory 源码,具体如下:
package org.apache.ibatis.datasource;
import java.util.Properties;
import javax.sql.DataSource;
/**
* @author Clinton Begin
*/
public interface DataSourceFactory {
void setProperties(Properties props);
DataSource getDataSource();
}
MyBatis 创建了 DataSource
实例后,会将其放到 Configuration
对象内的 Environment
对象中, 供以后使用。
具体分析过程如下:
1.先进入 XMLConfigBuilder
类中,可以找到如下代码:
2.分析 configuration
对象的 environment
属性,结果如下:
3.1.1.4 Mybatis 中连接的获取过程分析
当我们需要创建 SqlSession
对象并需要执行 SQL 语句时,这时候 MyBatis 才会去调用 dataSource
对象来创建 java.sql.Connection
对象。也就是说,java.sql.Connection
对象的创建一直延迟到执行 SQL 语句的时候。
@Test
public void testSql() throws Exception {
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
SqlSession sqlSession = factory.openSession();
List<User> list = sqlSession.selectList("findUserById",41);
System.out.println(list.size());
}
只有当第 4 句 sqlSession.selectList("findUserById")
,才会触发 MyBatis 在底层执行下面这个方
法来创建 java.sql.Connection
对象。
如何证明它的加载过程呢?
我们可以通过断点调试,在 PooledDataSource
中找到如下 popConnection()
方法,如下所示:
分析源代码,得出 PooledDataSource
工作原理如下:
下面是连接获取的源代码:
最后我们可以发现,真正连接打开的时间点,只是在我们执行 SQL 语句时,才会进行。其实这样做我们也可以进一步发现,数据库连接是我们最为宝贵的资源,只有在要用到的时候,才去获取并打开连接,当我们用完了就再立即将数据库连接归还到连接池中。
3.1.2 Mybatis 的事务控制
3.1.2.1 Mybatis 中手动提交事务
Mybatis 中事务的提交方式,本质上就是调用 JDBC 的 setAutoCommit()
来实现事务控制。
我们运行之前所写的代码:
@Test
public void testSaveUser() throws Exception {
User user = new User();
user.setUsername("mybatis user09");
//6.执行操作
int res = userDao.saveUser(user);
System.out.println(res);
System.out.println(user.getId());
}
@Before//在测试方法执行之前执行
public void init()throws Exception {
//1.读取配置文件
in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.创建构建者对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//3.创建 SqlSession 工厂对象
factory = builder.build(in);
//4.创建 SqlSession 对象
session = factory.openSession();
//5.创建 Dao 的代理对象
userDao = session.getMapper(IUserDao.class);
}
@After//在测试方法执行完成之后执行
public void destroy() throws Exception{
//7.提交事务
session.commit();
//8.释放资源
session.close();
in.close();
}
这是我们的Connection
的整个变化过程,通过分析我们能够发现之前的 CUD 操作过程中,我们都要手动进行事务的提交,原因是 setAutoCommit()
方法,在执行时它的值被设置为 false
了,所以我们在 CUD 操作中,必须通过 sqlSession.commit()
方法来执行提交操作
3.1.2.2 Mybatis 自动提交事务的设置
通过上面的研究和分析,现在我们一起思考,为什么 CUD 过程中必须使用 sqlSession.commit()
提交事务?主要原因就是在连接池中取出的连接,都会将调用 connection.setAutoCommit(false)
方法,这样我们就必须使用 sqlSession.commit()
方法,相当于使用了 JDBC 中的 connection.commit()
方法实现事务提交。
明白这一点后,我们现在一起尝试不进行手动提交,一样实现 CUD 操作。
@Before//在测试方法执行之前执行
public void init()throws Exception {
//1.读取配置文件
in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.创建构建者对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//3.创建 SqlSession 工厂对象
factory = builder.build(in);
//4.创建 SqlSession 对象
session = factory.openSession(true);
//5.创建 Dao 的代理对象
userDao = session.getMapper(IUserDao.class);
}
@After//在测试方法执行完成之后执行
public void destroy() throws Exception{
//7.释放资源
session.close();
in.close();
}
我们发现,此时事务就设置为自动提交了,同样可以实现 CUD 操作时记录的保存。虽然这也是一种方式,但就编程而言,设置为自动提交方式为 false 再根据情况决定是否进行提交,这种方式更常用。因为我们可以根据业务情况来决定提交是否进行提交。
3.2 Mybatis 的动态 SQL 语句
3.2.1 动态 SQL 之<if>
标签
我们根据实体类的不同取值,使用不同的 SQL 语句来进行查询。比如在 id 如果不为空时可以根据 id 查询,如果 username 不同空时还要加入用户名作为条件。这种情况在我们的多条件组合查询中经常会碰到。
- 持久层 Dao 接口
/**
* 根据传入参数条件
* @param user 查询的条件,有可能有用户名,有可能有性别,也有可能有地址,还有可能是都有
* @return
*/
List<User> findUserByCondition(User user);
- 持久层 Dao 映射配置
<!--根据条件查询-->
<select id="findUserByCondition" resultType="org.example.domain.User" parameterType="org.example.domain.User">
select * from user
where 1=1
<if test="username != null and username != '' ">
and username like concat('%',#{username},'%')
</if>
<if test="address != null and address != '' ">
and address like concat('%',#{address},'%')
</if>
</select>
注意:<if>标签的 test 属性中写的是对象的属性名,如果是包装类的对象要使用 OGNL 表达式的写法。
另外要注意 where 1=1 的作用~!
-
测试
@Test public void testFindUserByCondition(){ User user = new User(); user.setUsername("王"); List<User> list = userDao.findUserByCondition(user); for (User u: list) { System.out.println(u); } }
3.2.2 动态 SQL 之<where>
标签
为了简化上面 where 1=1 的条件拼装,我们可以采用<where>
标签来简化开发。
持久层 Dao 映射配置
<!--根据条件查询-->
<select id="findUserByCondition" resultType="org.example.domain.User" parameterType="org.example.domain.User">
select * from user
<where>
<if test="username != null and username != '' ">
and username like concat('%',#{username},'%')
</if>
<if test="address != null and address != '' ">
and address like concat('%',#{address},'%')
</if>
</where>
</select>
3.2.3 动态标签之<foreach>
标签
传入多个 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)
这样我们在进行范围查询时,就要将一个集合中的值,作为参数动态添加进来。
这样我们将如何进行参数的传递?
- 在 QueryVo 中加入一个 List 集合用于封装参数
public class QueryVo implements Serializable {
private User user;
private List<Integer> ids;
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public List<Integer> getIds() {
return ids;
}
public void setIds(List<Integer> ids) {
this.ids = ids;
}
}
-
持久层 Dao 接口
/** * 根据queryVo提供的id集合,查询用户信息 * @param vo * @return */ List<User> findUserInIds(QueryVo vo);
持久层 Dao 映射配置
<!--根据queryVo提供的id集合,查询用户信息-->
<select id="findUserInIds" resultType="org.example.domain.User" parameterType="org.example.domain.QueryVo">
select * from user
<where>
<if test="ids != null and ids.size() > 0">
<foreach collection="ids" open="id in (" close=")" item="id" separator=",">
#{id}
</foreach>
</if>
</where>
</select>
SQL 语句:
select 字段 from user where id in (?)
<foreach>
标签用于遍历集合,它的属性:
collection
: 代表要遍历的集合元素,注意编写时不要写#{}
open:
代表语句的开始部分
close
: 代表结束部分
item="id" item
: 代表遍历集合的每个元素,生成的变量名
sperator
: 代表分隔符
-
编写测试方法
/** * foreach 标签的使用 */ @Test public void testFindUserInIds(){ QueryVo vo = new QueryVo(); List<Integer> ids = new ArrayList<Integer>(); ids.add(41); ids.add(42); ids.add(51); vo.setIds(ids); List<User> list = userDao.findUserInIds(vo); for (User u: list) { System.out.println(u); } }
3.2.4 Mybatis 中简化编写的 SQL 片段
Sql 中可将重复的 sql
提取出来,使用时用 include
引用即可,最终达到 sql 重用的目的。
-
定义代码片段
<sql id="defaultUser"> select * from user </sql>
-
引用代码片段
<!-- 配置查询所有操作 --> <select id="findAll" resultType="org.example.domain.User"> <include refid="defaultUser"></include> <!-- select * from user--> </select>
3.3 Mybatis 多表查询之一对多
3.3.1 一对一查询(多对一)
使用 resultMap
,定义专门的 resultMap
用于映射一对一查询结果。
通过面向对象的(has a)关系可以得知,我们可以在 Account 类中加入一个 User 类的对象来代表这个账户是哪个用户的。
1. 定义账户信息的实体类
/**
* 账号实体类
*/
public class Account implements Serializable {
private Integer id;
private Integer uid;
private Double money;
//从表实体应该包含一个主体实体的对象引用
private User user;
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getUid() {
return uid;
}
public void setUid(Integer uid) {
this.uid = uid;
}
public Double getMoney() {
return money;
}
public void setMoney(Double money) {
this.money = money;
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", uid=" + uid +
", money=" + money +
", user=" + user +
'}';
}
}
2. 编写 Sql 语句
select b.*,a.id as aid,a.uid,a.money from account a, user b where a.uid = b.id;
3. 定义账户的持久层 Dao 接口
/**
* 查询所有账号,同时还要获取到当前账号的所属用户信息
* @return
*/
List<Account> findAll();
4. 定义 AccountDao.xml
文件中的查询配置信息
<!-- 定义account和user的resultMap-->
<resultMap id="accountUserMap" type="org.example.domain.Account">
<id property="id" column="aid"></id>
<result property="uid" column="uid"></result>
<result property="money" column="money"></result>
<!-- 一对一的关系映射: 配置封装user的内容 -->
<association property="user" column="uid" javaType="org.example.domain.User">
<id property="id" column="id"></id>
<result property="username" column="username"></result>
<result property="address" column="address"></result>
<result property="sex" column="sex"></result>
<result property="birthday" column="birthday"></result>
</association>
</resultMap>
<!-- 查询所有账号-->
<select id="findAll" resultMap="accountUserMap">
select b.*,a.id as aid,a.uid,a.money from account a, user b where a.uid = b.id;
</select>
5. 测试方法
/**
* 查询所有账号
*/
@Test
public void testFindAll() {
//6.使用代理对象执行查询所有方法
List<Account> accounts = accountDao.findAll();
for(Account account : accounts) {
System.out.println(account);
//System.out.println(account.getUser());
}
}
3.3.2 一对多查询
需求:
查询所有用户信息及用户关联的账户信息。
分析:
用户信息和他的账户信息为一对多关系,并且查询过程中如果用户没有账户信息,此时也要将用户信息
查询出来,我们想到了左外连接查询比较合适。
1 . 编写 SQL 语句
SELECT
u.*,
a.ID as aid,
a.MONEY
FROM
user u
LEFT JOIN account a on
u.id = a.UID
2 . User 类加入 List<Account>
/**
* 用户的实体类
*/
public class User implements Serializable {
private Integer id;
private String username;
private Date birthday;
private String sex;
private String address;
// 一对多关系映射; 主表实体应该包含从表实体的集合引用
private List<Account> accounts;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public List<Account> getAccounts() {
return accounts;
}
public void setAccounts(List<Account> accounts) {
this.accounts = accounts;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", birthday=" + birthday +
", sex='" + sex + '\'' +
", address='" + address + '\'' +
'}';
}
}
3. 用户持久层 Dao 接口中加入查询方法
/**
* 查询所有用户操作
* @return
*/
List<User> findAll();
4. 用户持久层 Dao 映射文件配置
<!-- 定义User的resultMap -->
<resultMap id="userAccountMap" type="org.example.domain.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>
<!--配置user对象中国accounts集合映射-->
<collection property="accounts" ofType="org.example.domain.Account">
<id property="id" column="aid"></id>
<result property="uid" column="uid"></result>
<result property="money" column="money"></result>
</collection>
</resultMap>
<!-- 配置查询所有操作 -->
<select id="findAll" resultMap="userAccountMap">
SELECT
u.*,
a.ID as aid,
a.MONEY
FROM
user u
LEFT JOIN account a on
u.id = a.UID
</select>
5. 测试方法
@Test
public void testFindAll() {
//6.使用代理对象执行查询所有方法
List<User> users = userDao.findAll();
for(User user : users) {
System.out.println("---------------------------------");
System.out.println(user);
for (Account account : user.getAccounts()){
System.out.println(account);
}
}
}
3.4 Mybatis 多表查询之多对多
3.4.1 实现 Role 到 User 多对多
通过前面的学习,我们使用 Mybatis 实现一对多关系的维护。多对多关系其实我们看成是双向的一对多关系。
1 业务要求及实现 SQL需求:
实现查询所有对象并且加载它所分配的用户信息。
分析:
查询角色我们需要用到 Role 表,但角色分配的用户的信息我们并不能直接找到用户信息,而是要通过中间表(USER_ROLE 表)才能关联到用户信息。
下面是实现的 SQL 语句:
SELECT
r.id as rid,
r.role_name,
r.role_desc,
u.id ,
u.username,
u.birthday,
u.sex,
u.address
FROM
`role` r
LEFT JOIN user_role ur ON
r.ID = ur.RID
LEFT JOIN `user` u ON u.id = ur.UID
2 编写角色实体类
public class Role implements Serializable {
private Integer roleId;
private String roleName;
private String roleDesc;
// 多对多的关系映射: 一个角色可以赋予多个用户
private List<User> users;
public List<User> getUsers() {
return users;
}
public void setUsers(List<User> users) {
this.users = users;
}
public Integer getRoleId() {
return roleId;
}
public void setRoleId(Integer roleId) {
this.roleId = roleId;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
public String getRoleDesc() {
return roleDesc;
}
public void setRoleDesc(String roleDesc) {
this.roleDesc = roleDesc;
}
@Override
public String toString() {
return "Role{" +
"roleId=" + roleId +
", roleName='" + roleName + '\'' +
", roleDesc='" + roleDesc + '\'' +
'}';
}
}
3 编写 Role 持久层接口
/**
* 查询所有角色
* @return
*/
List<Role> findAll();
}
4 编写映射文件
<!--定义Role表的ResultMap-->
<resultMap id="roleMap" type="org.example.domain.Role">
<id property="roleId" column="rid"></id>
<result property="roleName" column="ROLE_NAME"></result>
<result property="roleDesc" column="ROLE_DESC"></result>
<collection property="users" ofType="org.example.domain.User">
<id property="id" column="id"></id>
<result property="username" column="username"></result>
<result property="birthday" column="birthday"></result>
<result property="sex" column="sex"></result>
<result property="address" column="address"></result>
</collection>
</resultMap>
<!-- 查询所有角色-->
<select id="findAll" resultMap="roleMap">
SELECT
r.id as rid,
r.role_name,
r.role_desc,
u.id ,
u.username,
u.birthday,
u.sex,
u.address
FROM
`role` r
LEFT JOIN user_role ur ON
r.ID = ur.RID
LEFT JOIN `user` u ON u.id = ur.UID
</select>
5 编写测试类
public class RoleTest {
private InputStream in;
private SqlSession sqlSession;
private RoleDao roleDao;
@Before // test方法执行之前执行
public void init() throws IOException {
//1.读取配置文件
in = Resources.getResourceAsStream("SqlMapConfig.xml");
//2.创建 SqlSessionFactory 的构建者对象
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
//3.使用构建者创建工厂对象 SqlSessionFactory
SqlSessionFactory factory = builder.build(in);
//4.使用 SqlSessionFactory 生产 SqlSession 对象
sqlSession = factory.openSession();
//5.使用 SqlSession 创建 dao 接口的代理对象
roleDao = sqlSession.getMapper(RoleDao.class);
}
@After // test方法执行之后执行
public void destroy() throws IOException {
//提交事务
sqlSession.commit();
//7.释放资源
sqlSession.close();
in.close();
}
/**
* 查询所有账号
*/
@Test
public void testFindAll() {
//6.使用代理对象执行查询所有方法
List<Role> roles = roleDao.findAll();
for(Role role : roles) {
System.out.println(role);
System.out.println(role.getUsers());
}
}
}
12
3.4.2 实现 User 到 Role 的多对多
从 User 出发,我们也可以发现一个用户可以具有多个角色,这样用户到角色的关系也还是一对多关系。这样我们就可以认为 User 与 Role 的多对多关系,可以被拆解成两个一对多关系来实现。
1 业务要求及实现 SQL需求:
SELECT
u.id ,
u.username,
u.birthday,
u.sex,
u.address,
r.id as rid,
r.role_name,
r.role_desc
FROM
`user` u
LEFT JOIN user_role ur ON
u.ID = ur.UID
LEFT JOIN `role` r ON r.id = ur.RID
2 编写角色实体类
/**
* 用户的实体类
*/
public class User implements Serializable {
private Integer id;
private String username;
private Date birthday;
private String sex;
private String address;
// 一对多关系映射; 主表实体应该包含从表实体的集合引用
private List<Account> accounts;
// 多对多的关系映射: 一个用户可以具有多个角色
private List<Role> roles;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public List<Account> getAccounts() {
return accounts;
}
public void setAccounts(List<Account> accounts) {
this.accounts = accounts;
}
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", birthday=" + birthday +
", sex='" + sex + '\'' +
", address='" + address + '\'' +
'}';
}
}
3 编写 Role 持久层接口
/**
* 查询所有用户及权限信息
* @return
*/
List<User> findAllRole();
4 编写映射文件
<!-- 定义User权限的resultMap -->
<resultMap id="userRoleMap" type="org.example.domain.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>
<!--配置user对象中国accounts集合映射-->
<collection property="roles" ofType="org.example.domain.Role">
<id property="roleId" column="rid"></id>
<result property="roleName" column="role_name"></result>
<result property="roleDesc" column="role_desc"></result>
</collection>
</resultMap>
<select id="findAllRole" resultMap="userRoleMap">
SELECT
u.id ,
u.username,
u.birthday,
u.sex,
u.address,
r.id as rid,
r.role_name,
r.role_desc
FROM
`user` u
LEFT JOIN user_role ur ON
u.ID = ur.UID
LEFT JOIN `role` r ON r.id = ur.RID
</select>
5 编写测试类
@Test
public void testFindAllRole(){
List<User> users = userDao.findAllRole();
for (User user : users){
System.out.println(user);
System.out.println(user.getRoles());
}
}