这篇文章是以上一篇文章的代码为基础的。基于代理Dao实现CRUD
文章目录
Mybatis 连接池简单介绍
我们在实际开发中都会使用连接池,因为它可以减少我们获取连接所消耗的时间。我们来看看下图对连接池的分析:
线程1
和线程2
分别获取1号
和2号
连接之后,连接池里的连接将重新进行排序。当线程1
和线程2
释放连接之后,1号
和2号
将会重新进入连接池的尾部,并有新的序号。
Mybatis连接池提供了三种方式的配置,其配置的位置为:
- 主配置文件
SqlMapConfig.xml
中的dataSource
标签,type
属性就是表示采用何种连接池方式。
在 Mybatis 中,数据源 dataSource 中type属性的取值共有三类,分别是:
POOLED
:使用连接池的数据源,采用池的思想,采用传统的javax.sql.DataSource
规范中的连接池,Mybatis 中有针对规范的实现UNPOOLED
:不使用连接池的数据源。采用传统的获取连接方法,虽然也有Javax.sql.DataSource
接口,但是没有池的思想。JNDI
:使用 JNDI 实现的数据源,采用服务器提供的 JNDI 技术实现,来获取 DataSource 对象,不同的服务器所能拿到的 DataSource 是不一样的。
注意,如果不是 Web 或者 Maven 的war工程,是不能使用 JNDI 的。
接下来我们来看看POOLED和UNPOOLED的区别:
上面效果就是执行测试代码的
findAll()
方法,大家可以自行去更改type属性的值测试一下。
MyBatis 内部分别定义了实现了 java.sql.DataSource
接口的 UnpooledDataSource
,PooledDataSource
类来表示UNPOOLED
、POOLED
类型的数据源。
- 查看
POOLED
的实现PooledDataSource
,可以看出获取连接时采用了池的思想,大概流程如下图:(通过Ctrl+n
可以快速找到你想要找到的类)
上面的原理那是怎么实现的呢?看下图:
- 查看 UNPOOLED 的实现 UnpooledDataSource ,可以看出每次获取连接时都会注册驱动并创建新连接,大概流程如下图:
实际开发中我们一般使用的都是POOLED,使用池的思想来管理连接。
MyBatis中的事务
再了解mybatis中的事务前,先来看看几个问题
什么是事务?
事务是数据库操作的最小工作单元,是作为单个逻辑工作单元执行的一系列操作;这些操作作为一个整体一起向系统提交,要么都执行、要么都不执行;事务是一组不可再分割的操作集合(工作逻辑单元);
MyBatis中的事务是通过sqlsession
对象的commit
方法和rollback
方法实现事务的提交和回滚。
事务的四大特性ACID
原子性
事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做一致性
事 务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统 运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是 不一致的状态。隔离性
一个事务的执行不能其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。持续性
也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。
不考虑隔离会导致的三个问题
- **
脏读
:**脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。 - **
不可重复读
:**一个事务两次读取同一行的数据,结果得到不同状态的结果,中间正好另一个事务更新了该数据,两次结果相异,不可被信任。通俗来讲就是:事务T1在读取某一数据,而事务T2立马修改了这个数据并且提交事务给数据库,事务T1再次读取该数据就得到了不同的结果,发送了不可重复读。 幻读(虚读)
:一个事务执行两次查询,第二次结果集包含第一次中没有或某些行已经被删除的数据,造成两次结果不一致,只是另一个事务在这两次查询中间插入或删除了数据造成的。通俗来讲就是:例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读
解决办法(四种隔离级别)
Read Uncommited(读取未提交内容)
读未提交,顾名思义,就是一个事务可以读取另一个未提交事务的数据。但是,读未提交产生了脏读,采用读提交可以解决脏读问题
Read Commited(读取提交内容)
读提交,顾名思义,就是一个事务要等另一个事务提交后才能读取数据。读提交,若有事务对数据进行更新(UPDATE)操作时,读操作事务要等待这个更新操作事务提交后才能读取数据,可以解决脏读问题。但在这个事例中,出现了一个事务范围内两个相同的查询却返回了不同数据,这就是不可重复读。但是,读提交两次查询会产生不同的查询结果,就会造成不可重复读问题,采用重复读可以解决此问题。
Repeatable Read(重复读)
重复读,就是在开始读取数据(事务开启)时,不再允许修改操作。重复读可以解决不可重复读问题。应该明白的一点就是,不可重复读对应的是修改,即UPDATE操作。但是可能还会有幻读问题。因为幻读问题对应的是插入INSERT操作,而不是UPDATE操作。采用Serializable可以解决幻读问题
Serializable(可串行化)
Serializable 是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。
Mybatis 中事务提交方式
Mybatis 中事务的提交方式,本质上就是调用 JDBC 的 setAutoCommit()来实现事务控制。
我们运行之前所写的测试代码中的任意一个方法:
这是我们的
Connection
的整个变化过程,通过分析我们能够发现之前的 CUD 操作过程中,我们都要手动进行事务的提交,原因是setAutoCommit()
方法,在执行时它的值被设置为 false 了,所以我们在 CUD 操作中,必须通过sqlSession.commit()
方法来执行提交操作。
那我们怎么设置自动提交呢?
Mybatis 自动提交事务的设置
通过上面的研究和分析,现在我们一起思考,为什么 CUD 过程中必须使用
sqlSession.commit()
提交事务?主要原因就是在连接池中取出的连接,都会将调用connection.setAutoCommit(false)
方法,这样我们就必须使用sqlSession.commit()
方法,相当于使用了 JDBC 中的connection.commit()
方法实现事务提交。
修改完之后再次运行代码:
我们发现,此时事务就设置为自动提交了,同样可以实现CUD操作时记录的保存。虽然这也是一种方式,但是不常用,
因为每次执行一个对数据库的CRUD操作,才可以用这种方式,当在一个方法里边多次跟数据库交互,如果这时候你让每个连接处于独立的自动提交中,那这个事务肯定是控制不住的
。
就编程而言,设置为自动提交方式为 false 再根据情况决定是否进行提交,这种方式更常用。因为我们可以根据业务情况来决定提交是否进行提交。
Mybatis 的动态 SQL 语句
MyBatis 的强大特性之一便是它的动态 SQL。如果你有使用 JDBC 或其它类似框架的经验,你就能体会到根据不同条件拼接 SQL 语句的痛苦。例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL 这一特性可以彻底摆脱这种痛苦。
if 标签的使用
- 首先定义接口方法:
/**
* 根据传入的参数条件查询
* @param user 查询的条件,有可能有用户名,性别,地址等;有可能都有,也有可能都没有
* @return
*/
List<User> findUserByCondition(User user);
- 接着配置映射文件:
<!-- 根据条件查询 -->
<select id="findUserByCondition" parameterType="user" resultMap="userMap">
select *from user where 1 = 1
<if test="userName != null and userName != ''">
and username = #{userName}
</if>
<if test="userSex != null and userSex != ''">
and sex = #{userSex}
</if>
<!--大家还可以接着往下写-->
</select>
- 这里加上
WHERE 1 =1
是防止所有条件都为空时拼接 SQL 语句出错。因为不加上1 = 1
这个恒等条件的话,如果后面查询条件都没拼接成功,那么 SQL 语句最后会带有一个WHERE
关键字而没有条件,不符合 SQL 语法<if> </if>
标签中test
属性是必须的,表示判断的条件。其中有几点需要注意:
- 如果
test
有多个条件,那么必须使用and
进行连接,而不能使用 Java 中的&&
运算符。test
中的参数名称必须与实体类的属性保持一致,也就是和 #{参数符号} 保持一致。- 如果判断条件为字符串,那么除了判断是否为
null
外,最好也判断一下是否为空字符串,''
,防止 SQL语句将其作为条件查询。
- 测试代码及运行结果如下:
/**
* 测试多条件查询
*/
@Test
public void testFindUserByCondition(){
User u = new User();
u.setUserName("老王");
u.setUserSex("男");
List<User> users = userDao.findUserByCondition(u);
for (User user : users){
System.out.println(user);
}
}
where 标签的使用
为了简化上面where 1=1
的条件拼装,我们可以采用<where>
标签来简化开发。
<!-- 根据条件查询(where标签) -->
<select id="findUserByCondition" parameterType="user" resultMap="userMap">
select *from user
<where>
<if test="userName != null and userName != ''">
and username = #{userName}
</if>
<if test="userSex != null and userSex != ''">
and sex = #{userSex}
</if>
</where>
</select>
- 可以发现,相比之前的 SQL 语句,我们少写了
WHERE 1 = 1
,而是使用<where></where>
标签来代替它。 -<where></where>
标签只会在至少有一个子元素的条件返回 SQL 子句的情况下才去插入WHERE
子句。而且,若语句的开头为AND 或 OR
,<where></where>
标签也会将它们去除。- 简单来说,就是该标签可以动态添加
WHERE
关键字,并且剔除掉 SQL 语句中多余的AND 或者 OR
。
运行之前的方法,结果是一样的
foreach 标签的使用
- 假如我们现在有一个新的需求,就是根据一个 id 集合,来查询出 id 在该集合中的所有用户,那么又该怎么实现呢?
- 如果使用普通 SQL 语句的话,那么查询语句应该这样写:
SELECT * FROM user WHERE id IN(41,42,43);
- 因此,如果想使用动态 SQL 来完成的话,那么我们就应该考虑如何拼接上
id IN(41,42,43)
这一串内容,这时候,我们的<foreach></foreach>
标签就出场了。
- 首先修改
QueryVo
类,增加一个成员变量用于存放 id 集合,并增加其getter()/setter()
:
package com.domain;
import java.util.List;
/**
* 用于封装查询条件
* */
public class QueryVo {
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;
}
}
- 接下来添加接口方法,并配置映射文件
/**
* 根据QueryVo中提供的id集合。查询用户信息
* @param vo
* @return
*/
List<User> findUserInIds(QueryVo vo);
<!--根据QueryVo中提供的id集合。查询用户列表-->
<select id="findUserInIds" parameterType="QueryVo" resultMap="userMap">
select * from user
<where>
<if test="ids != null and ids.size() > 0">
<foreach collection="ids" open="and id in (" close=")" item="uid" separator=",">
#{uid}
</foreach>
</if>
</where>
</select>
<foreach></foreach>
标签用于遍历集合,每个属性的作用如下所示:collection
: 代表要遍历的集合或数组,这个属性是必须的。如果是遍历数组,那么该值只能为 arrayopen
: 代表语句的开始部份。close
: 代表语句的结束部份。item
: 代表遍历集合时的每个元素,相当于一个临时变量。separator
: 代表拼接每个元素之间的分隔符。- 你可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象传递给 foreach 作为集合参数。当使用可迭代对象或者数组时,index 是当前迭代的次数,item 的值是本次迭代获取的元素。当使用 Map 对象(或者
Map.Entry 对象的集合)时,index 是键,item 是值
。- 注意,SQL 语句中的参数符号
#{uid}
应该与item="uid"
保持一致,也就是说,item
属性如果把临时变量声明为id
的话,那么使用时就必须写成#{id}
。
- 测试代码和运行结果
/**
* 测试foreach标签查询
*/
@Test
public void testFindUserInIds(){
QueryVo vo = new QueryVo();
List<Integer> list = new ArrayList<Integer>();
list.add(41);
list.add(42);
list.add(43);
list.add(45);
vo.setIds(list);
List<User> users = userDao.findUserInIds(vo);
for (User user : users){
System.out.println(user);
}
}
定义 SQL 片段
在上面的例子中,我们在每条 SQL 中都用到了 SELECT * FROM user
,因此,我们可以把该语句定义为 SQL 片段,以供复用,减少工作量。
- 首先在映射文件中定义代码片段
<!-- 抽取重复的语句代码片段 -->
<sql id="defaultUser">
select * from user
</sql>
- 接着就可以在需要的地方引用该片段了,使用时用 include 引用即可,最终达到 sql 重用的目的。
<!--根据QueryVo中提供的id集合。查询用户列表-->
<select id="findUserInIds" parameterType="QueryVo" resultMap="userMap">
<include refid="defaultUser"></include>
<!--下面这行代码就可以不写了-->
<!--select * from user;-->
<where>
<if test="ids != null and ids.size() > 0">
<foreach collection="ids" open="and id in (" close=")" item="uid" separator=",">
#{uid}
</foreach>
</if>
</where>
</select>
为了避免 Mybatis 在动态拼接 SQL 语句的时候发生错误,建议在编写 SQL 语句时不要添加分号
;