文章目录
Mybatis第二阶端
第一章 Mabatis第二阶端知识点
1、mybatis中的连接池以及事务控制
mybatis中连接池使用及分析
mybatis事务控制的分析
2、mybatis基于XML配置的动态SQL语句使用
mappers配置文件中的几个标签:
<if>
<where>
<foreach>
<sql>
3、mybatis中的多表操作 掌握应用(联合查询)
一对多,多对一
多对多
今日学习目标:
1:掌握SqlMapConfig.xml配置文件(第2章)
2:理解Mybatis连接池与事务操作(第3章)
3:掌握Mybatis动态SQL(第4章)
4:掌握Mybatis多表关联查询(第5章5.3)
5:掌握Mybatis多对多关系(第5章5.4)
第二章 SqlMapConfig.xml配置文件
在SqlMapConfigx.xml中可以配置很多属性(本章一个个讲解),如下:
SqlMapConfig.xml中配置的内容和顺序如下:
properties(属性)
settings(全局配置参数)
typeAliases (类型别名)
typeHandlers(类型处理器)
objectFactory(对象工厂)
plugins(插件)
environments(环境集合属性对象)
environment(环境子属性对象)
transactionManager(事务管理)
dataSource(数据源)
mappers(映射器)
2.1 properties(属性)
SqlMapConfig.xml可以引用java属性文件中的配置信息如下:
在classpath下定义jdbcConfig.properties文件, (即resources文件夹下)
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/itcastmybatis
jdbc.username=root
jdbc.password=root
在sqlMapConfig.xml中配置:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 配置properties
可以在标签内部配置连接数据库的信息。(可见我的博客中Mybatis(一)的讲解)
也可以通过属性引用外部配置文件信息
-->
<!--加载外部配置文件信息-->
<properties resource="jdbc.properties"/>
<!--也可以这么写-->
<!--<properties url="file:///D:/ideaProjects/mybatis/mybatis_day02_crud/src/main/resources/jdbcConfig.properties">
</properties>-->
<!--配置mybatis环境-->
<environments default="mysql">
<environment id="mysql">
<!--配置事务管理-->
<transactionManager type="JDBC"/>
<!--配置数据源-->
<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>
</environment>
</environments>
<!--配置映射文件信息-->
<mappers>
<mapper resource="cn/iyhome/dao/IUerDao.xml"/>
<!--如果是使用注解的话,此处应该是用class属性指定被注解的dao全限定类名-->
<!--<mapper class="cn.iyhome.dao.IUserDao"/>-->
</mappers>
</configuration>
Properties属性讲解
标签的属性讲解:
-
resource属性: (常用)
用于指定配置文件的位置,是按照类路径的写法来写,并且必须存在于类路径下。 -
url属性:
是要求按照Url的写法来写地址URL:Uniform Resource Locator 统一资源定位符。它是可以唯一定位一个资源的位置。 它的写法: http://localhost:8080/mybatisserver/demo1Servlet 协议 主机 端口 URI URI:Uniform Resource Identifier 统一资源标识符。它是在应用中可以唯一标识一个资源的。
所以 xml中加载jdbc.properties也可以这么写
<properties url="file:///D:/ideaProjects/mybatis/mybatis_day02_crud/src/main/resources/jdbcConfig.properties">
[题外话]HTTP 协议中 URI 和 URL 有什么区别?
ps:转自知乎daixinye的解释
2.2 typeAliases(类型别名)
mybatis内置别名映射
mybatis支持别名: 别名 | 映射的类型 |
---|---|
_byte | byte |
_long | long |
_short | short |
_int | int |
_integer | int |
_double | double |
_float | float |
_boolean | boolean |
string | String |
byte | Byte |
long | Long |
short | Short |
int | Integer |
integer | Integer |
double | Double |
float | Float |
boolean | Boolean |
date | Date |
decimal | BigDecimal |
bigdecimal | BigDecimal |
map | Map |
用户自定义别名
需求:IUserDao.xml中方法的返回值类型,全限定类名过于繁琐,是否可以简化
方案:使用别名,替换全限定类名
(1) 在SqlMapConfig.xml中配置
<!--在configuration标签下配置-->
<!--使用typeAliases配置别名,它只能配置domain中类的别名-->
<!--配置别名-->
<typeAliases>
<!--使用typeAlias给指定的实体类注册别名,配置的别名是不区分大小写的-->
<!--配置一个类-->
<!--<typeAlias type="cn.iyhome.domain.User" alias="user"/>-->
<!--但是当类比较多的时候,一个类一个类配置显得十分麻烦-->
<!--pageage用于指定要配置别名的包,该包下的实体类都会注册别名,并且类名就是别名,同样不区分大小写-->
<!--批量配置-->
<package name="cn.iyhome.domain"/>
</typeAliases>
(2)在配置了SqlMapConfig.xml后,在IUserDao.xml中就可以把权限定类名替换为别名
<!--可以看到下面3个方法的返回值都是用了别名,且不区分大小写-->
<!--查找所有-->
<select id="findAll" resultType="USER">
select * from user
</select>
<!--修改用户-->
<update id="updateUser" parameterType="UsER">
update user set sex=#{sex} where id=#{id}
</update>
<!--查找用户byQueryVo-->
<select id="findUserByQueryVO" parameterType="cn.iyhome.domain.QueryVo" resultType="uSER">
select * from user where username=#{user.username}
</select>
2.3 mappers(映射器)
问题现象:在SqlMapConfig.xml中,配置映射文件时,如果我们使用是是xml配置的方法,使用的是resource属性,如果是使用注解的方式的话(在第一阶段,入门案例有一点涉及,在第三阶段会详细讲解),使用的事是class属性.当Dao层的类特别多的时候,使用这两种方式配置起来,工作量特别大.
解决方案:使用package统一处理
<!--配置映射文件信息-->
<mappers>
<!--指定xml配置文件映射-->
<!--<mapper resource="cn/iyhome/dao/IUserDao.xml"/>-->
<!--指定使用注解,此处应该是用class属性指定被注解的dao全限定类名-->
<!--<mapper class="cn.iyhome.dao.IUserDao"/>-->
<!--两种方式通用的写法-->
<!--package标签用于指定dao接口所在的包,当指定了之后,就不需要再写mapper->resource或者mapper->class 了-->
<package name="cn.iyhome.dao"/>
</mappers>
注意:
使用<mapper class>和<package name>必须满足以下条件:
- 此种方法要求mapper接口名称和mapper映射文件名称相同,且放在同一个目录中
快捷的检查方式:
第三章 Mybatis 连接池与事务深入(了解)
3.1 Mybatis的连接池技术
我们在实际开发中都会使用连接池。因为它可以减少我们获取连接所消耗的时间。
在Mybatis中有连接池技术,但是它采用的是自己的连接池技术。在Mybatis的SqlMapConfig.xml配置文件中,通过<dataSource type=”pooled”>来实现Mybatis中连接池的配置。
什么是连接池?
连接池的特点:
1:连接池就是用于存储连接的一个容器
2:连接池其实就是一个集合对象,该集合必须是线程安全的,不能两个线程拿到同一个连接。
3:连接池必须实现队列的特性,先进先出
3.1.1 Mybatis连接池的分类
在Mybatis中我们将它的数据源dataSource分为以下几类:
可以看出Mybatis将它自己的数据源分为三类:
- UNPOOLED 不使用连接池的数据源,采用传统的获取连接的方式,虽然也实现Javax.sql.DataSource接口,但是并没有使用池的思想。
- POOLED 使用连接池的数据源,采用传统的javax.sql.DataSource规范中的连接池,mybatis中有针对规范的实现。
- JNDI 使用JNDI实现的数据源(不了解),采用服务器提供的JNDI技术实现,来获取DataSource对象,不同的服务器所能拿到DataSource是不一样。
注意:如果不是web或者maven的war工程,是不能使用的。
tomcat服务器,采用连接池就是dbcp连接池。
具体结构如下:
相应地,MyBatis内部分别定义了实现了java.sql.DataSource接口的UnpooledDataSource,PooledDataSource类来表示UNPOOLED、POOLED类型的数据源。
在这三种数据源中,我们一般采用的是POOLED数据源(很多时候我们所说的数据源就是为了更好的管理数据库连接,也就是我们所说的连接池技术)。
3.1.2 Mybatis中数据源的配置
同样还是以上述demo为案例,执行测试类中的调用findAll()的方法.此时数据源使用的是POOLED
查询结果:
此截图可以看到,他是从连接池中获取的连接,在使用完毕后归还给连接池.
底层代码跟踪
1.Ctrl+N :查找类:PooledDataSource.java
2.定位到方法 getConnection()
tips: Alt+7 可以再左下侧展示出所有方法和变量
3.跟踪进入popConnection()方法
图解处理流程
问:为什么MyBatis连接池要设计为有一个空闲连接列表和一个活动连接列表?
1:从连接池的设计看,这两个连接列表都是必需的,加起来等于是连接池能用的最大连接数。
2:当有新的连接请求时,有从空闲连接列表中选择一个可用的连接,如果这个连接可以正常执行,转移到活动连接列表。
3:新建连接比较耗时,所以一开始就新建好一堆连接,这些连接没有被使用的时候就在空闲列表里。
4:当要使用的时候,就从空闲列表里拿一个,放到活动列表里。
ps: 数据源使用的是UNPOOLED时,底层实现原理和我们之前接触的一样.使用反射加载驱动,然后使用DriverManager获取连接,再进行一系列的操作,此处不再阐述
总结:最后我们可以发现,真正连接打开的时间点,只是在我们执行SQL语句时,才会进行。其实这样做我们也可以进一步发现,数据库连接是我们最为宝贵的资源,只有在要用到的时候,才去获取并打开连接,当我们用完了就再立即将数据库连接归还到连接池中。 所以我们使用连接池的技术会节省连接资源。
3.1.3 Mybatis的事务控制
3.1.3.1 JDBC中事务的回顾
一:什么是事务?
二:事务的四大特性ACID?
三:不考虑隔离性会产生的3个问题?
脏读
(针对未提交数据)如果一个事务中对数据进行了更新,但事务还没有提交,另一个事务可以“看到”该事务没有提交的更新结果,这样造成的问题就是,如果第一个事务回滚,那么,第二个事务在此之前所“看到”的数据就是一笔脏数据。
不可重复读
(针对其他提交前后,读取数据本身的对比)不可重复读取是指同一个事务在整个事务过程中对同一笔数据进行读取,每次读取结果都不同。如果事务1在事务2的更新操作之前读取一次数据,在事务2的更新操作之后再读取同一笔数据一次,两次结果是不同的
幻读
(针对其他提交前后,读取数据条数的对比) 幻读是指同样一笔查询在整个事务过程中多次执行后,查询所得的结果集是不一样的。幻读针对的是多笔记录。如果事务1在事务2的新增操作之前读取一次数据,在事务2的新增操作之后再读取同一笔数据,取得的结果集是不同的,幻读发生。
不可重复读和幻读比较:
两者有些相似,但是前者针对的是update或delete,后者针对的insert。
四:解决办法:四种隔离级别?
1、Serializable (串行化):最严格的级别,事务串行执行,资源消耗最大,避免了“脏读”和“不可重复读”,“幻读”的情况;
2、REPEATABLE READ(重复读):保证了一个事务不会修改已经由另一个事务读取但未提交(回滚)的数据。避免了“脏读”和“不可重复读”的情况,但不能避免“幻读”,但是带来了更多的性能损失。(mysql的默认隔离级别)
3、READ COMMITTED (读已提交):大多数主流数据库的默认事务等级,保证了一个事务不会读到另一个并行事务已修改但未提交的数据,避免了“脏读”,但不能避免“幻读”和“不可重复读”。该级别适用于大多数系统。(oracle的默认隔离级别)
4、Read Uncommitted(读未提交):事务中的修改,即使没有提交,其他事务也可以看得到,会导致“脏读”、“幻读”和“不可重复读取”。
3.1.3.2 Mybatis中的事务提交方式
Mybatis中事务的提交方式,本质上就是调用JDBC的setAutoCommit()来实现事务控制。
源码分析
- ctrl+鼠标点击 openSession(),跳转到接口SqlSessionFactory
- 点击SqlSessionFactory,按ctrl+alt+b ,选择DefaultSqlSessionFactory
- 定位到方法openSession()
再来查看api,autoCommit:为true表示启动自动提交事务,false表示禁用自动提交事务
问题
看完上述源码可能有的同学不明白我们要讲的点在哪.我们再来观察我们的测试代码
还记得我们在Mybatis第一阶段单表CURD时做插入操作时,控制台没有报错,但是查看数据库表数据又没有插入成功的案例么.原因在于没有提交事务.所以我们再添加一行代码
sqlSession.commit();
此时我们再想上述的源码分析中, autoCommit:false
,你是不是就已经明白了些什么.因为自动提交事务的开关默认是关着的.所以我们如果只要打开这个开关后,SqlSession.commit()
就不要再写了.
总结
如果设置此时事务就设置为自动提交了,同样可以实现CUD操作时记录的保存。虽然这也是一种方式,但就编程而言,设置为自动提交方式为false再根据情况决定是否进行提交,这种方式更常用。因为我们可以根据业务情况来决定业务是否进行提交。如果设置自动提交,如果当前操作有误,事务很难设置回滚了。 只有设置为手动提交,我们才能更好的控制事务。
第四章 Mybatis映射文件的SQL深入
Mybatis的映射文件中,前面我们的SQL都是比较简单的,有些时候业务逻辑复杂时,我们的SQL是动态变化的,此时在前面的学习中我们的SQL就不能满足要求了。
4.1 动态SQL之 if 标签
我们根据实体类的不同取值,使用不同的SQL语句来进行查询。比如在id如果不为空时可以根据id查询,如果username不同空时还要加入用户名作为条件。这种情况在我们的多条件组合查询中经常会碰到。
需求:增加一个方法,根据实体类的不同取值,使用不同的SQL语句来进行查询
1.在IUserDao.java中添加方法
/**
* 根据条件查询
*/
List<User> findUserByCondition(User user);
2.在IUserDao.xml中添加配置信息
<!--根据条件查询-->
<select id="findUserByCondition" parameterType="user" resultType="user">
select * from user where 1=1
<!--if标签的test属性中写的是对象的属性名,如果是包装类的对象要使用OGNL表达式的写法-->
<if test="username!=null">
and username like #{username}
</if>
<if test="sex!=null">
and sex=#{sex}
</if>
</select>
注意:<if>标签的test属性中写的是对象的属性名,如果是包装类的对象要使用OGNL表达式的写法。
另外要注意where 1=1 的作用,如果两个if条件都为假,那么sql语句只有where关键字没有条件,语法错误.
3.在测试类中测试方法
@Test
public void finUserByCondition() {
//使用SqlSession构建Dao的代理对象
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
//
User user = new User();
user.setUsername("%王%");
user.setSex("男");
//执行Dao的findUserByCondition方法
List<User> users = userDao.findUserByCondition(user);
//遍历
for (User userInfo : users) {
System.out.println(userInfo);
}
}
结果
如果把user.setUsername("%王%");
注释,只保留user.setSex("男");
,查询结果为:
4.2 动态SQL之 where 标签
有了这个标签后就可以省略where 1=1
<!--where标签的使用-->
<select id="findByCondition" parameterType="user" resultMap="userMap">
select * from user
<where>
<if test="userName != null">
and username = #{userName}
</if>
<if test="userSex != null">
and sex = #{Sex}
</if>
</where>
</select>
<where />可以自动处理第一个and
4.3 动态SQL标签之 foreach 标签
需求
传入多个id查询用户信息,用下边两个sql实现:
SELECT * FROM USERS WHERE username LIKE '%王%' AND (id =41 OR id =43 OR id=45)
SELECT * FROM USERS WHERE username LIKE '%王%' AND id IN (41,43,45)
这样我们在进行范围查询时,就要将一个集合中的值,作为参数动态添加进来。
这样我们将如何进行参数的传递?
1.为QueryVo类添加成员变量,变量类型为List,存储Id
public class QueryVo {
private User user;
//存储用户id
private List<Integer> ids;
public List<Integer> getIds() {
return ids;
}
public void setIds(List<Integer> ids) {
this.ids = ids;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}
2.编写映射文件IUseDao.xml
<!--根据id范围查找-->
<select id="findUserByIds" parameterType="QueryVo" resultType="user">
select * from user
<where>
<if test="ids != null and ids.size()>0">
<foreach collection="ids" open="and id in (" item="id" close=")" separator=",">
#{id}
</foreach>
</if>
</where>
</select>
sql语句:select * from user where id in(?,?,?)
参数说明:
<foreach>标签用于遍历集合,它的属性:
collection:代表要遍历的集合元素,注意编写时不要写#{}
open:代表语句的开始部分
close:代表结束部分
item:代表遍历集合的每个元素,生成的变量名
sperator:代表分隔符
3.编写测试类
@Test
public void findUserByIds() {
//使用SqlSession构建Dao的代理对象
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
List<Integer> list = new ArrayList<Integer>();
Collections.addAll(list, 41,43,45,100);
QueryVo qv = new QueryVo();
qv.setIds(list);
List<User> users = userDao.findUserByIds(qv);
for (User user : users) {
System.out.println(user);
}
}
查看结果:
4.4 Mybatis中简化编写的SQL片段
Sql中可将重复的sql提取出来,使用时用include引用即可,最终达到sql重用的目的。
我们先到UserDao.xml文件中使用<sql>
标签,定义出公共部分,如下:
<sql id="defaultUser">
select * from user
</sql>
使用<include refid="defaultUser"></include>
<!--foreach标签的使用-->
<select id="findInIds" parameterType="queryVo" resultMap="userMap">
<!--select * from user-->
<!--使用include引入公共代码块-->
<include refid="defaultUser"></include>
<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>
第5章 Mybatis 的多表关联查询
5.1 回顾
表之间的关系有几种:
一对一,一对多,多对一,多对多
在多对一中:特例:
如果拿出每一个订单,他都只能属于一个用户。
所以Mybatis就把多对一看成了一对一。
5.2 用户和账号
本次案例主要以最为简单的用户和账户的模型来分析Mybatis 多表关系。用户为User 表,账户为Account 表。一个用户(User)可以有多个账户(Account)。具体关系如下:
维护数据库示例数据
-- user表在之前的案例中已经创建过,不再创建
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account` (
`ID` INT(11) NOT NULL COMMENT '编号',
`UID` INT(11) DEFAULT NULL COMMENT '用户编号',
`MONEY` DOUBLE DEFAULT NULL COMMENT '金额',
PRIMARY KEY (`ID`),
KEY `FK_Reference_8` (`UID`),
CONSTRAINT `FK_Reference_8` FOREIGN KEY (`UID`) REFERENCES `user` (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
INSERT INTO `account`(`ID`,`UID`,`MONEY`) VALUES (1,46,1000),(2,45,1000),(3,46,2000);
5.3 Mybatis维护一对多关系
5.3.1 查询所有账户
第一步:创建工程
第二步:编写java文件、资源文件、测试文件、pom.xml
在上述案例中,再添加以下文件
cn.iyhome.domain.Account.java
cn.iyhome.dao.IAccountDao.java
cn/iyhome/dao/IAccountDao.xml
Account.java
public class Account {
private int id;
private int uid;
private float money;
//getter
//setter
@Override
public String toString() {
//...略
}
}
IAccountDao.java
public interface IAccountDao {
/**
* 查询所有用户
* @return
*/
List<Account> findAll();
/**
* 根据id查询用户信息
* @param id
* @return
*/
Account findById(Integer id);
}
IAccountDao.xml
<?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">
<mapper namespace="cn.iyhome.dao.IAccountDao">
<!--查找所有-->
<select id="findAll" resultType="account">
select * from account
</select>
<!--根据id查找账户信息-->
<select id="findById" parameterType="int" resultType="account">
select * from account where id = #{id}
</select>
</mapper>
未完待续…