Mybatis高级学习笔记

学习来源:黑马程序员

学习时间:2021年3月9日,2023年6月24日

1 什么是框架

1.1 框架概念

它是我们软件开发中的一套解决方案,不同的框架解决的是不同的问题。框架封装了很多细节,是开发者可以使用极简的方式实现功能。大大提高开发效率。

1.2 三层架构

  • 表现层:用于展示数据。

  • 业务层:用于处理业务需求。

  • 持久层:用于和数据库交互。

1.3 持久层技术解决方案

  • JDBC技术:Connection,PreparedStatement,ResultSet
  • Spring的JdbcTemplate:Spring对Jdbc的简单封装
  • Apache的DBUtils:也是对jdbc的简单封装

以上都不是框架,jdbc是规范,下面两者都只是工具类。

2 Mybatis框架概述

mybatis是一个优秀的基于Java的持久层框架,它内部封装了jdbc,使开发者只需要关注sql语句本身,而不需要花费精力去处理加载驱动,创建连接,创建statement等繁杂的过程。

mybatis通过xml或注解的方式将要执行的各种statement配置起来,并通过java对象和statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射为java对象并返回。它使用了ORM思想,实现了结果集的封装。

ORM-Object Relational Mapping,对象关系映射。就是把数据库表和实体类的属性对应起来,让我们可以操作实体类就可以实现操作数据库表。

3 Mybatis入门

3.1 环境搭建

3.1.1 步骤

导入坐标

1
2
3
4
5
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>

创建实体类和dao的接口

user.java代码

1
2
3
4
5
6
7
8
9
public class User implements Serializable {

private Integer id;
private String username;
private Date birthday;
private String sex;
private String address;
//省略了get,set,toString方法
}

IUserDao.java代码

1
2
3
4
5
6
7
8
9
10
/**
* 用户持久层接口
*/
public interface IUserDao {
/**
* 查询所有操作
* @return
*/
List<User> findAll();
}

创建mybatis的主配置文件

SqlMapConfig.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
27
<?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">
<!--mybatis的主配置文件-->
<configuration>
<!--配置环境-->
<environments default="mysql">
<!--配置mysql的环境-->
<environment id="mysql">
<!--配置事务的类型-->
<transactionManager type="JDBC"></transactionManager>
<!--配置数据源,也叫连接池-->
<dataSource type="POOLED">
<!--配置连接数据库的基本信息-->
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test"/>
<property name="username" value="root"/>
<property name="password" value="12345678"/>
</dataSource>
</environment>
</environments>
<!--指定映射配置文件的位置,映射配置文件指的是每个dao独立的配置文件-->
<mappers>
<mapper resource="com/hongyi/dao/IUserDao.xml"></mapper>
</mappers>
</configuration>

创建映射配置文件

IUserDao.xml

1
2
3
4
5
6
7
8
9
10
11
12
<?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">
<!--resultType是指sql执行完后结果封装到哪里去-->
<!--id写dao的接口方法名称-->
<mapper namespace="com.hongyi.dao.IUserDao">
<select id="findAll" resultType="com.hongyi.domain.User">
<!--配置查询所有-->
select * from user
</select>
</mapper>

3.1.2 注意事项

  • 创建IUserDao.xml和IUserDao.java时,在mybatis中,它把持久层的操作接口名称和映射文件也叫作Mapper。所以,IUserDao和IUserMapper是一样的。
  • 在IDEA中创建目录的时候,它和包是不一样的。包在创建时:com.hongyi.dao时三级结构,而目录在创建时是一级目录。要一级一级地创建。
  • mybatis的映射配置文件位置必须与dao接口的包结构相同
  • 映射配置文件的mapper标签中的namespace属性的取值,必须是dao接口的全限定类名
  • 映射配置文件的操作配置,id属性的取值,必须是dao接口的方法名。

当我们遵从了第三四五点之后,在开发中就无须再写dao的实现类,剩下的就由mybatis来实现。

3.1.3 项目结构图

image-20211215194641619

3.2 入门案例

3.2.1 步骤

  • 读取配置文件
  • 创建SqlSessionFactory工厂
  • 创建SqlSession
  • 创建Dao接口的代理对象
  • 执行dao中的方法
  • 释放资源

注意:不要忘记在映射配置中告知mybatis要封装到哪个实体类中,配置的方法是指定实体类的全限定类名。

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
public class MybatisTest {
/**
* 入门案例
* @param args
*/
public static void main(String[] args) throws IOException {
// 1.读取配置文件
InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");
// 2.创建SqlSessionFactory工厂
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(in);
// 3.使用工厂生产一个SqlSession对象
SqlSession session = factory.openSession();
// 4.使用SqlSession创建dao接口的代理对象
IUserDao userDao = session.getMapper(IUserDao.class);
// 5.使用代理对象执行方法
List<User> users = userDao.findAll();
for (User user : users){
System.out.println(user);
}
// 6.释放资源
session.close();
in.close();
}
}

3.2.2 注解开发

IUserDao.xml移除,在dao接口的方法上使用@Select注解,并指定sql语句,同时需要在SqlMapConfig.xml中的mapper配置时,使用class属性指定dao接口的全限定类名

1
2
@Select("select * from user")
List<User> findAll();
1
2
3
4
<!--如果使用注解来配置的话,此处应该使用class属性指定备注接的dao全限定类名-->
<mappers>
<mapper class="com.hongyi.dao.IUserDao"></mapper>
</mappers>

我们在实际开发中,都是越简便越好,所以都是采用不写dao实现类的方式,不管使用xml还是注解配置。但是mybatis是支持写dao实现类的。

3.2.3 设计模式分析

image-20211215194703418

创建工厂SqlSessionFactory使用了建造者模式

1
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);

生产SqlSession使用了工厂模式

1
SqlSession sqlSession = sqlSessionFactory.openSession(true);

创建userDaoIUserDao接口的实现类对象)使用了动态代理

1
IUserDao userDao = session.getMapper(IUserDao.class);

4 自定义Mybatis

4.1 查询所有的分析

mybatis在使用代理dao的方式实现增删改查时做了什么事呢?

  • 创建代理对象
  • 在代理对象中调用selectList

  • (1)连接数据库的信息,有了它们就能创建Connection对象
1
2
3
4
5
<!--配置连接数据库的基本信息-->
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test?characterEncoding=utf8"/>
<property name="username" value="root"/>
<property name="password" value="12345678"/>
  • (2)有了它们就有了映射配置信息
1
2
3
<mappers>
<mapper class="com.hongyi.dao.IUserDao"></mapper>
</mappers>
  • (3)有了它就有了执行的Sql语句,就可以获取PreparedStatement。此配置中还有封装的实体类的全限定类名
1
2
3
4
5
<mapper namespace="com.hongyi.dao.IUserDao">
<select id="findAll" resultType="com.hongyi.domain.User">
select * from user
</select>
</mapper>

1+2+3:读取配置文件:用到的技术就是解析xml的技术。此种用的是dom4j解析xml技术。

selectList方法执行的伪代码过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1.根据配置文件的信息创建Connection对象
注册驱动,获取连接
// 2.获取预处理对象PreparedStatement
此时需要Sql语句
con.preparedStatement(sql); // 从配置文件3中的sql语句中获取
// 3.执行查询
ResuluSet resultSet = preparedStatement.executeQuery();
// 4.遍历结果集,用于封装
List<E> list = new ArrayList();
while(resultSet.next()){
// 通过映射配置文件中的resultType的全限定类名反射创建
E element = (E)Class.forName(全限定类名).newInstance();
// 我们的实体类属性和表中的列名是一致的。于是我们就可以把表中的列名看成是实体类的属性名称。就可以使用反射的方式来根据名称获取属性,并把值赋进去。即使用反射来进行封装。
进行封装,把resultSet的内容都添加到element中;
把element加入到list中;
list.add(element);
}
// 5.返回list
return list;

要想让selectList方法执行,我们需要给方法提供两个信息:

  • 连接信息
  • 映射信息

对于映射信息的封装

它包含了两部分:执行的sql语句;封装结果的实体类全限定类名。把这两个信息组合起来定义成一个对象

该对象(假设名为Mapper)与键为String类型的全限定类名+方法名(例如com.hongyi.dao.IUserDao.findAll)绑定,形成一个Map。

image-20211215194713279

4.2 创建代理对象的分析

1
2
// 4.使用SqlSession创建dao接口的代理对象
IUserDao userDao = session.getMapper(IUserDao.class);

过程分析:

1
2
3
4
5
6
7
// 根据dao接口的字节码创建dao的代理对象
public <T> T getMapper(Class<T> daoInterfaceClass){
// 如何代理:它就是增强的方法,我们需要自己来提供
// 此处是一个InvocationHanlder的接口,需要写一个该接口的实现类
// 在实现类中调用selectList方法,即上一节中的所有功能
Proxy.newProxyInstance(类加载器,代理对象要实现的接口字节码数组,如何代理);
}

4.3 自定义mybatis的编码

4.3.1 初始化框架搭建

自定义mybatis需要的四个类

  • class Resources
  • class SqlSessionFactoryBuilder
  • interface SqlSessionFactory
  • interface SqlSession

Resources.java

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 使用类加载器读取配置文件的类
*/
public class Resources {
/**
* 根据传入的参数获取一个字节输入流
* @param filePath
* @return
*/
public static InputStream getResourceAsStream(String filePath){
return Resources.class.getClassLoader().getResourceAsStream(filePath);
}
}

SqlSessionFactoryBuilder.java

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 用于创建一个SqlSessionFactory对象
*/
public class SqlSessionFactoryBuilder {
/**
* 根据参数的字节输入流来构建一个SqlSessionFactory工厂
* @param config
* @return
*/
public SqlSessionFactory build(InputStream config){
return null;
}
}

SqlSessionFactory.java

1
2
3
4
5
6
7
public interface SqlSessionFactory {
/**
* 用于打开一个新的SqlSession对象
* @return
*/
SqlSession openSession();
}

SqlSession.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 自定义mybatis中和数据库交互的核心类
*/
public interface SqlSession {
/**
* 根据参数创建一个代理对象
* @param daoInterfaceClass dao接口的运行时类对象
* @param <T> 泛型
* @return 代理对象
*/
<T> T getMapper(Class<T> daoInterfaceClass);

/**
* 释放资源
*/
void close();
}

<T> T的含义:<T> T 表示返回值T的类型是泛型

4.3.2 解析xml的工具类

工具类XMLConfigBuilder.java代码如下

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
/**
* @author 黑马程序员
* @Company http://www.ithiema.com
* 用于解析配置文件
*/
public class XMLConfigBuilder {

/**
* 解析主配置文件,把里面的内容填充到DefaultSqlSession所需要的地方
* 使用的技术:
* dom4j+xpath
*/
public static Configuration loadConfiguration(InputStream config){
try{
//定义封装连接信息的配置对象(mybatis的配置对象)
Configuration cfg = new Configuration();

//1.获取SAXReader对象
SAXReader reader = new SAXReader();
//2.根据字节输入流获取Document对象
Document document = reader.read(config);
//3.获取根节点
Element root = document.getRootElement();
//4.使用xpath中选择指定节点的方式,获取所有property节点
List<Element> propertyElements = root.selectNodes("//property");
//5.遍历节点
for(Element propertyElement : propertyElements){
//判断节点是连接数据库的哪部分信息
//取出name属性的值
String name = propertyElement.attributeValue("name");
if("driver".equals(name)){
//表示驱动
//获取property标签value属性的值
String driver = propertyElement.attributeValue("value");
cfg.setDriver(driver);
}
if("url".equals(name)){
//表示连接字符串
//获取property标签value属性的值
String url = propertyElement.attributeValue("value");
cfg.setUrl(url);
}
if("username".equals(name)){
//表示用户名
//获取property标签value属性的值
String username = propertyElement.attributeValue("value");
cfg.setUsername(username);
}
if("password".equals(name)){
//表示密码
//获取property标签value属性的值
String password = propertyElement.attributeValue("value");
cfg.setPassword(password);
}
}
//取出mappers中的所有mapper标签,判断他们使用了resource还是class属性
List<Element> mapperElements = root.selectNodes("//mappers/mapper");
//遍历集合
for(Element mapperElement : mapperElements){
//判断mapperElement使用的是哪个属性
Attribute attribute = mapperElement.attribute("resource");
if(attribute != null){
System.out.println("使用的是XML");
//表示有resource属性,用的是XML
//取出属性的值
String mapperPath = attribute.getValue();//获取属性的值"com/itheima/dao/IUserDao.xml"
//把映射配置文件的内容获取出来,封装成一个map
Map<String, Mapper> mappers = loadMapperConfiguration(mapperPath);
//给configuration中的mappers赋值
cfg.setMappers(mappers);
}else{
// System.out.println("使用的是注解");
// //表示没有resource属性,用的是注解
// //获取class属性的值
// String daoClassPath = mapperElement.attributeValue("class");
// //根据daoClassPath获取封装的必要信息
// Map<String,Mapper> mappers = loadMapperAnnotation(daoClassPath);
// //给configuration中的mappers赋值
// cfg.setMappers(mappers);
}
}
//返回Configuration
return cfg;
}catch(Exception e){
throw new RuntimeException(e);
}finally{
try {
config.close();
}catch(Exception e){
e.printStackTrace();
}
}

}

/**
* 根据传入的参数,解析XML,并且封装到Map中
* @param mapperPath 映射配置文件的位置
* @return map中包含了获取的唯一标识(key是由dao的全限定类名和方法名组成)
* 以及执行所需的必要信息(value是一个Mapper对象,里面存放的是执行的SQL语句和要封装的实体类全限定类名)
*/
private static Map<String,Mapper> loadMapperConfiguration(String mapperPath)throws IOException {
InputStream in = null;
try{
//定义返回值对象
Map<String,Mapper> mappers = new HashMap<String,Mapper>();
//1.根据路径获取字节输入流
in = Resources.getResourceAsStream(mapperPath);
//2.根据字节输入流获取Document对象
SAXReader reader = new SAXReader();
Document document = reader.read(in);
//3.获取根节点
Element root = document.getRootElement();
//4.获取根节点的namespace属性取值
String namespace = root.attributeValue("namespace");//是组成map中key的部分
//5.获取所有的select节点
List<Element> selectElements = root.selectNodes("//select");
//6.遍历select节点集合
for(Element selectElement : selectElements){
//取出id属性的值 组成map中key的部分
String id = selectElement.attributeValue("id");
//取出resultType属性的值 组成map中value的部分
String resultType = selectElement.attributeValue("resultType");
//取出文本内容 组成map中value的部分
String queryString = selectElement.getText();
//创建Key
String key = namespace+"."+id;
//创建Value
Mapper mapper = new Mapper();
mapper.setQueryString(queryString);
mapper.setResultType(resultType);
//把key和value存入mappers中
mappers.put(key,mapper);
}
return mappers;
}catch(Exception e){
throw new RuntimeException(e);
}finally{
in.close();
}
}

/**
* 根据传入的参数,得到dao中所有被select注解标注的方法。
* 根据方法名称和类名,以及方法上注解value属性的值,组成Mapper的必要信息
* @param daoClassPath
* @return
*/
// private static Map<String,Mapper> loadMapperAnnotation(String daoClassPath)throws Exception{
// //定义返回值对象
// Map<String,Mapper> mappers = new HashMap<String, Mapper>();
//
// //1.得到dao接口的字节码对象
// Class daoClass = Class.forName(daoClassPath);
// //2.得到dao接口中的方法数组
// Method[] methods = daoClass.getMethods();
// //3.遍历Method数组
// for(Method method : methods){
// //取出每一个方法,判断是否有select注解
// boolean isAnnotated = method.isAnnotationPresent(Select.class);
// if(isAnnotated){
// //创建Mapper对象
// Mapper mapper = new Mapper();
// //取出注解的value属性值
// Select selectAnno = method.getAnnotation(Select.class);
// String queryString = selectAnno.value();
// mapper.setQueryString(queryString);
// //获取当前方法的返回值,还要求必须带有泛型信息
// Type type = method.getGenericReturnType();//List<User>
// //判断type是不是参数化的类型
// if(type instanceof ParameterizedType){
// //强转
// ParameterizedType ptype = (ParameterizedType)type;
// //得到参数化类型中的实际类型参数
// Type[] types = ptype.getActualTypeArguments();
// //取出第一个
// Class domainClass = (Class)types[0];
// //获取domainClass的类名
// String resultType = domainClass.getName();
// //给Mapper赋值
// mapper.setResultType(resultType);
// }
// //组装key的信息
// //获取方法的名称
// String methodName = method.getName();
// String className = method.getDeclaringClass().getName();
// String key = className+"."+methodName;
// //给map赋值
// mappers.put(key,mapper);
// }
// }
// return mappers;
// }
}

4.3.3 SqlSessionFactory和SqlSession的实现类

  • Configuration.java代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 自定义mybatis的配置类
*/
public class Configuration {
private String driver;
private String url;
private String username;
private String password;

private Map<String,Mapper> mappers = new HashMap<>();

public Map<String, Mapper> getMappers() {
return mappers;
}

public void setMappers(Map<String, Mapper> mappers) {
this.mappers.putAll(mappers); // 此处需要使用追加的方法
}
// 省略了属性的set和get方法
}
  • Mapper.java代码
1
2
3
4
5
6
7
8
/**
* 用于封装执行的sql语句和结果类型的全限定类名
*/
@Data
public class Mapper {
private String queryString;// sql
private String resultType;// 实体类的全限定类名
}
  • 接口SqlSessionFactory的实现类DefaultSqlSessionFactory
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class DefaultSqlSessionFactory implements SqlSessionFactory {

private Configuration cfg;

public DefaultSqlSessionFactory(Configuration cfg) {
this.cfg = cfg;
}

/**
* 用于创建一个新的操作数据库对象
* @return
*/
@Override
public SqlSession openSession() {
return new DefaultSqlSession(cfg);
}
}
  • 接口SqlSession的实现类DefaultSqlSession
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
27
public class DefaultSqlSession implements SqlSession {

private Configuration cfg;

public DefaultSqlSession(Configuration cfg) {
this.cfg = cfg;
}

/**
* 创建代理对象
* @param daoInterfaceClass dao接口的运行时类对象
* @param <T> 泛型
* @return 代理对象
*/
@Override
public <T> T getMapper(Class<T> daoInterfaceClass) {
return null;
}

/**
* 释放资源
*/
@Override
public void close() {

}
}

4.3.4 基于XML的查询所有操作

代码整体框架:

image-20230624204219032


① DefaultSqlSession
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class DefaultSqlSession implements SqlSession {

private Configuration cfg;
private Connection conn; // mysql连接对象

public DefaultSqlSession(Configuration cfg) {
this.cfg = cfg;
// 通过数据源来获取一个连接对象
conn = DataSourceUtil.getConnection(cfg);
}

/**
* 动态创建代理对象,重点
* @param daoInterfaceClass dao接口的运行时类对象(被代理对象)
* @param <T> 泛型
* @return 代理对象
*/
@Override
public <T> T getMapper(Class<T> daoInterfaceClass) {
return (T) Proxy.newProxyInstance(
daoInterfaceClass.getClassLoader(),
new Class[]{daoInterfaceClass},
// 方法增强
new MapperProxy(cfg.getMappers(), conn));
}

/**
* 释放资源
*/
@Override
public void close() {
if(conn != null) {
try {
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
② DataSourceUtil

通过数据源(连接池)获取一个sql连接对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class DataSourceUtil {
/**
* 通过数据源获取一个sql连接
* @param cfg
* @return
*/
public static Connection getConnection(Configuration cfg) {
try {
// jdbc标准写法
Class.forName(cfg.getDriver());
return DriverManager.getConnection(cfg.getUrl(), cfg.getUsername(), cfg.getPassword());
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
③ MapperProxy

MapperProxy是一个实现接口InvocationHandler的类,它必须实现invoke方法(即调用被代理对象的同名方法),以完成代理的具体操作。

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
27
28
29
30
31
32
33
34
35
36
public class MapperProxy implements InvocationHandler {

private Map<String, Mapper> mappers;
private Connection conn;

public MapperProxy(Map<String, Mapper> mappers, Connection conn) {
this.mappers = mappers;
this.conn = conn;
}

/**
* 对方法进行增强——即调用selectList方法
* @param proxy 代理类对象,基本不用
* @param method 要调用的代理类对象的方法,和被代理类对象的方法相同
* @param args 方法的参数
* @return selectList方法的返回值
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1.方法名 findAll
String methodName = method.getName();
// 2.方法所在类的名称 com.hongyi.dao.IUserDao
String className = method.getDeclaringClass().getName();
// 3.组合成key
String key = className + "." + methodName;
// 4.获取mappers中的Mapper对象
Mapper mapper = mappers.get(key);
// 5.判断是否有mapper
if (mapper == null) {
throw new IllegalArgumentException("传入的参数有误");
}
// 6.调用工具类查询所有
return new Executor().selectList(mapper, conn);
}
}
④ Executor

执行mapper对应的方法,并返回封装后的数据。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class Executor {

public <E> List<E> selectList(Mapper mapper, Connection conn) {
PreparedStatement pstm = null;
ResultSet rs = null;
try {
//1.取出mapper中的数据
String queryString = mapper.getQueryString(); //select * from user
String resultType = mapper.getResultType(); // com.hongyi.domain.User
Class domainClass = Class.forName(resultType);
//2.获取PreparedStatement对象
pstm = conn.prepareStatement(queryString);
//3.执行SQL语句,获取结果集
rs = pstm.executeQuery();
//4.封装结果集
List<E> list = new ArrayList<E>();//定义返回值
while(rs.next()) {
//实例化要封装的实体类对象
E obj = (E) domainClass.newInstance();

//取出结果集的元信息:ResultSetMetaData
ResultSetMetaData rsmd = rs.getMetaData();
//取出总列数
int columnCount = rsmd.getColumnCount();
//遍历总列数
for (int i = 1; i <= columnCount; i++) {
//获取每列的名称,列名的序号是从1开始的
String columnName = rsmd.getColumnName(i);
//根据得到列名,获取每列的值
Object columnValue = rs.getObject(columnName);
//给obj赋值:使用Java内省机制(借助PropertyDescriptor实现属性的封装)
PropertyDescriptor pd = new PropertyDescriptor(columnName,domainClass);//要求:实体类的属性和数据库表的列名保持一致
//获取它的写入方法
Method writeMethod = pd.getWriteMethod();
//把获取的列的值,给对象赋值
writeMethod.invoke(obj,columnValue);
}
//把赋好值的对象加入到集合中
list.add(obj);
}
return list;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
release(pstm,rs);
}
}


private void release(PreparedStatement pstm,ResultSet rs){
if(rs != null){
try {
rs.close();
}catch(Exception e){
e.printStackTrace();
}
}

if(pstm != null){
try {
pstm.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
使用的是XML
{com.hongyi.dao.IUserDao.findAll=Mapper(queryString=
SELECT * FROM user
, resultType=com.hongyi.domain.User)}
User(id=1, username=Action, birthday=2006-02-15 04:46:27.0)
User(id=2, username=Animation, birthday=2006-02-15 04:46:27.0)
User(id=3, username=Children, birthday=2006-02-15 04:46:27.0)
User(id=4, username=Classics, birthday=2006-02-15 04:46:27.0)
User(id=5, username=Comedy, birthday=2006-02-15 04:46:27.0)
User(id=6, username=Documentary, birthday=2006-02-15 04:46:27.0)
User(id=7, username=Drama, birthday=2006-02-15 04:46:27.0)
User(id=8, username=Family, birthday=2006-02-15 04:46:27.0)
User(id=9, username=Foreign, birthday=2006-02-15 04:46:27.0)
User(id=10, username=Games, birthday=2006-02-15 04:46:27.0)
User(id=11, username=Horror, birthday=2006-02-15 04:46:27.0)
User(id=12, username=Music, birthday=2006-02-15 04:46:27.0)
User(id=13, username=New, birthday=2006-02-15 04:46:27.0)
User(id=14, username=Sci-Fi, birthday=2006-02-15 04:46:27.0)
User(id=15, username=Sports, birthday=2006-02-15 04:46:27.0)
User(id=16, username=Travel, birthday=2006-02-15 04:46:27.0)

Process finished with exit code 0

4.3.5 基于注解的查询所有操作

① 配置文件

resource修改为class,并将属性修改为全限定类型:

1
2
3
<mappers>
<mapper class="com.hongyi.dao.IUserDao"/>
</mappers>
② IUserDao

在方法上添加注解:

1
2
3
4
5
6
7
8
public interface IUserDao {
/**
* 查询所有操作
* @return
*/
@Select("select * from user")
List<User> findAll();
}
③ Select

mybatis下新建包Annotation,并新建注解Select

1
2
3
4
5
6
7
8
9
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Select {
/**
* 配置sql语句
* @return
*/
String value();
}
④ XMLConfigBuilder

放开所有注解即可。

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
使用的是注解
User(id=1, username=Action, birthday=2006-02-15 04:46:27.0)
User(id=2, username=Animation, birthday=2006-02-15 04:46:27.0)
User(id=3, username=Children, birthday=2006-02-15 04:46:27.0)
User(id=4, username=Classics, birthday=2006-02-15 04:46:27.0)
User(id=5, username=Comedy, birthday=2006-02-15 04:46:27.0)
User(id=6, username=Documentary, birthday=2006-02-15 04:46:27.0)
User(id=7, username=Drama, birthday=2006-02-15 04:46:27.0)
User(id=8, username=Family, birthday=2006-02-15 04:46:27.0)
User(id=9, username=Foreign, birthday=2006-02-15 04:46:27.0)
User(id=10, username=Games, birthday=2006-02-15 04:46:27.0)
User(id=11, username=Horror, birthday=2006-02-15 04:46:27.0)
User(id=12, username=Music, birthday=2006-02-15 04:46:27.0)
User(id=13, username=New, birthday=2006-02-15 04:46:27.0)
User(id=14, username=Sci-Fi, birthday=2006-02-15 04:46:27.0)
User(id=15, username=Sports, birthday=2006-02-15 04:46:27.0)
User(id=16, username=Travel, birthday=2006-02-15 04:46:27.0)

5 Mybatis框架实现CRUD操作

5.1 自定义Mybatis的流程分析

image-20211215194752276

5.2 CRUD操作

5.2.1 保存操作

IUserDao.xml

1
2
3
4
<!--parameterType:values的数据类型-->
<insert id="saveUser" parameterType="com.hongyi.domain.User">
insert into user(username,address,sex,birthday) values (#{username},#{address},#{sex},#{birthday});
</insert>

IUserDao.java

1
2
3
4
5
/**
* 保存用户
* @param user
*/
void saveUser(User user);

改进后的mybatis测试类:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
* 测试mybatis的crud操作
*/
public class MybatisTest {
private InputStream in;
private SqlSession session;
private IUserDao userDao;

@Before // 用于在测试方法执行之前执行
public void init() throws IOException {
// 1.读取配置文件
in = Resources.getResourceAsStream("SqlMapConfig.xml");
// 2.创建SqlSessionFactory工厂
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(in);
// 3.使用工厂生产一个SqlSession对象
session = factory.openSession();
// 4.使用SqlSession创建dao接口的代理对象
userDao = session.getMapper(IUserDao.class);
}

@After // 用于在测试方法执行之后执行
public void destroy() throws IOException {
// 提交事务
session.commit();
// 6.释放资源
session.close();
in.close();
}

/**
* 查询所有
*/
@Test
public void testFindAll() throws IOException {
// 5.使用代理对象执行方法
List<User> users = userDao.findAll();
for (User user : users){
System.out.println(user);
}

}

/**
* 测试保存操作
*/
@Test
public void testSave() throws IOException {
User user = new User();
user.setUsername("曾弘毅");
user.setAddress("中国");
user.setSex("男");
user.setBirthday(new Date());
// 5.使用代理对象执行保存方法
userDao.saveUser(user);
}
}

5.2.2 更新和删除操作

IUserDao.xml

1
2
3
4
5
6
7
8
9
10
<!--更新用户操作-->
<update id="updateUser" parameterType="com.hongyi.domain.User">
update user set username=#{username},address=#{address},sex=#{sex},birthday=#{birthday} where id=#{id};
</update>

<!--删除用户操作-->
<!--当只有一个占位符时,其名称可以任意写,例如写成id=#{userId}-->
<delete id="deleteUser" parameterType="java.lang.Integer">
delete from user where id=#{id}
</delete>

IUserDao.java

1
2
3
4
5
6
7
8
9
10
/**
* 更新用户
* @param user
*/
void updateUser(User user);

/**
* 删除用户
*/
void deleteUser(Integer id);

MybatisTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 测试更新操作
*/
@Test
public void testUpdate() throws IOException {
User user = new User();
user.setId(53);
user.setUsername("孙文");
user.setAddress("台湾");
user.setSex("男");
user.setBirthday(new Date());
// 5.使用代理对象执行保存方法
userDao.updateUser(user);
}

/**
* 测试删除操作
*/
@Test
public void testDelete() throws IOException {
// 5.使用代理对象执行保存方法
userDao.deleteUser(53);
}

5.2.3 查询和模糊查询

IUserDao.java

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 根据id查询用户
* @param id
* @return
*/
User findById(Integer id);

/**
* 根据姓名模糊查询用户
* @param username
* @return
*/
List<User> findByName(String username);

IUserDao.xml

1
2
3
4
5
6
7
8
9
<!--根据id查询用户操作-->
<select id="findById" parameterType="INT" resultType="com.hongyi.domain.User">
select * from user where id=#{id};
</select>

<!--根据姓名模糊查询用户操作-->
<select id="findByName" parameterType="java.lang.String" resultType="com.hongyi.domain.User">
select * from user where username like #{username};
</select>

MybatisTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 测试查询操作
*/
@Test
public void testFindOne() throws IOException {
// 5.使用代理对象执行保存方法
User user = userDao.findById(52);
System.out.println(user);
}

/**
* 测试模糊查询操作
*/
@Test
public void testFindByName() throws IOException {
// 5.使用代理对象执行保存方法
// 注意百分号用户模糊查询
List<User> users = userDao.findByName("%王%");
for(User user : users){
System.out.println(user);
}
}

5.2.4 返回一行一列和占位符分析

IUserDao.java

1
2
3
4
5
/**
* 查询总用户数
* @return
*/
int findTotal();

IUserDao.xml

1
2
3
4
<!--获取用户的总记录条数-->
<select id="findTotal" resultType="int">
select count(id) from user;
</select>

MybatisTest.java

1
2
3
4
5
6
7
8
/**
* 测试集合函数操作:查询总记录条数
*/
@Test
public void testFindTotal() throws IOException {
int count = userDao.findTotal();
System.out.println(count);
}

占位符的小细节

image-20211215194815114

5.2.5 获取保存数据后的id

新增用户后,同时还要返回当前新增用户的 id 值,因为 id 是由数据库的自动增长来实现的,所以就相

当于我们要在新增后将自动增长auto_increment的值返回。

IUserDao.xml

1
2
3
4
5
6
7
8
9
10
11
<!--保存用户操作-->
<!--parameterType:values的数据类型-->
<insert id="saveUser" parameterType="com.hongyi.domain.User">
<!--配置插入操作后,获取插入数据的id-->
<!--keyProperty对应实体类的属性,keyColumn对应数据库表的字段名-->
<!--resultType代表sql执行返回后的数据封装类型(结果集类型),order表示该条sql执行在之前还是之后-->
<selectKey keyProperty="id" keyColumn="id" resultType="int" order="AFTER">
select last_insert_id();
</selectKey>
insert into user(username,address,sex,birthday) values (#{username},#{address},#{sex},#{birthday});
</insert>

MybatisTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 测试保存操作
*/
@Test
public void testSave() throws IOException {
User user = new User();
user.setUsername("曾弘毅");
user.setAddress("中国");
user.setSex("男");
user.setBirthday(new Date());
System.out.println("保存操作之前:"+user);
userDao.saveUser(user);
System.out.println("保存操作之后:"+user);
}

测试结果:

image-20211215194830165

5.3 Mybatis参数深入

5.3.1 parameterType参数配置

SQL 语句传参,使用标签的 parameterType 属性来设定。该属性的取值可以是基本类型,引用类型(例如:String 类型),还可以是实体类类型(POJO 类)。同时也可以使用实体类的包装类,即传递pojo包装对象。

OGNL表达式

OGNL:Object Graphic Navigation Language,它是通过对象的取值方法来获取数据,在写法上把get给省略了。例如我们在获取用户的名称:

  • 类中的写法:user.getUsername();

  • OGNL写法:user.username

mybatis中为什么能直接写username,而不用user.呢?因为在parameterType中已经提供了属性所属的类,此时不需要再写对象名。例如:

1
2
3
4
<!--更新用户操作-->
<update id="updateUser" parameterType="com.hongyi.domain.User">
update user set username=#{username},address=#{address},sex=#{sex},birthday=#{birthday} where id=#{id};
</update>

例如这里的#{username},#{address}等属性,ognl表达式会认为是com.hongyi.domain.User类的属性字段。

传递pojo包装对象

QueryVo.java

1
2
3
4
5
6
7
8
9
10
11
public class QueryVo {
private User user;

public User getUser(){
return user;
}

public void setUser(User user){
this.user = user;
}
}

IUserDao.java

1
2
3
4
5
6
/**
* 根据QueryVo查询中的条件查询用户
* @param vo
* @return
*/
List<User> findUserByVo(QueryVo vo);

IUserDao.xml

1
2
3
4
5
<!--根据vo模糊查询用户操作-->
<select id="findUserByVo" parameterType="com.hongyi.domain.QueryVo" resultType="com.hongyi.domain.User">
<!--注意是user.username,即ognl写法-->
select * from user where username like #{user.username};
</select>

MybatisTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 测试使用QueryVo(User类的包装类)作为查询条件的模糊查询操作
*/
@Test
public void testFindByVo() throws IOException {
QueryVo queryVo = new QueryVo();
User user = new User();
user.setUsername("%王%");// 加上百分号进行模糊查询
queryVo.setUser(user);
List<User> users = userDao.findUserByVo(queryVo);
for (User u : users){
System.out.println(u);
}
}

5.3.2 输出结果封装

resultType 属性可以指定结果集的类型,它支持基本类型和实体类类型。需要注意的是,它和 parameterType 一样,如果注册过类型别名的,可以直接使用别名。没有注册过的必须使用全限定类名。例如:我们的实体类此时必须是全限定类名。同时,当是实体类名称时,还有一个要求,实体类中的属性名称必须和查询语句中的列名保持一致,否则无法实现封装。

解决实体类属性和数据库列名不一致的两种方法

实际开发中,Java的命名规范通常采用驼峰法,而数据库字段的名称通常采用下划线的方式,因此会造成两者的不一致。例如:

User.java类的属性:

1
2
3
4
5
private Integer userId;
private String userName;
private String userAddress;
private String userSex;
private Date userBirthday;

数据库中表的字段名:

image-20211215194839343

  • 方法一:在sql语句中取别名
1
2
3
4
5
<!--查询所有操作-->
<select id="findAll" resultType="com.hongyi.domain.User">
<!--关键字as可以省略-->
select id as userId,username as userName,address as userAddress,sex as userSex,birthday as userBirthday from user;
</select>
  • 方法二:在映射配置文件IUserDao.xml中配置resultMap标签
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!--配置查询结果的列名和实体类属性名的对应关系-->
<!--id:唯一id,任意-->
<!--type:查询的实体类-->
<resultMap id="userMap" type="com.hongyi.domain.User">
<!--主键字段的对应-->
<!--property:实体类的属性,column:数据库列名-->
<id property="userId" column="id"></id>
<!--非主键字段的对应-->
<result property="userName" column="username"></result>
<result property="userSex" column="sex"></result>
<result property="userAddress" column="address"></result>
<result property="userBirthday" column="birthday"></result>
</resultMap>

<!--查询所有操作-->
<!--resultMap代表要采用的resultMap对应关系的id-->
<select id="findAll" resultMap="userMap">
select * from user;
</select>

6 配置文件

6.1 配置内容和顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-properties(属性)
--property
-settings(全局配置参数)
--setting
-typeAliases(类型别名)
--typeAliase
--package
-typeHandlers(类型处理器)
-objectFactory(对象工厂)
-plugins(插件)
-environments(环境集合属性对象)
--environment(环境子属性对象)
---transactionManager(事务管理)
---dataSource(数据源)
-mappers(映射器)
--mapper
--package

6.2 properties

在使用 properties 标签配置时,我们可以采用两种方式指定属性配置。

  • 方法1:直接在配置文件写
1
2
3
4
5
6
<properties>
<property name="driver" value="xxxx"/>
<property name="url" value="xxxx"/>
<property name="username" value="xxxx"/>
<property name="password" value="xxxx"/>
</properties>

使用${}来引用属性:

1
2
3
4
5
6
<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>
  • 方法2:新建jdbc.properties文件,然后使用标签引用
1
<properties resource="jdbc.properties"/>
1
2
3
4
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test?characterEncoding=utf8
jdbc.username=root
jdbc.password=12345678

6.3 typeAliases

如果没有设置别名,则在映射文件中,必须使用全限定类名:

1
2
3
<select id="getUserById" resultType="com.hongyi.pojo.User">
select * from t_user where id = 1
</select>

设置别名有两种方法:

  • 单独设置:使用typeAlias子标签
1
2
3
4
<typeAliases>
<!-- alias:别名 type:全限定类名 -->
<typeAlias alias="User" type="com.hongyi.pojo.User"></typeAlias>
</typeAliases>

此时可以使用别名:

1
2
3
<select id="getUserById" resultType="User">
select * from t_user where id = 1
</select>
  • 批量设置:使用package子标签
1
2
3
4
<typeAliases>
<!-- 批量别名定义,扫描整个包下的类,别名为类名(首字母大写或小写都可以) -->
<package name="com.hongyi.pojo"/>
</typeAliases>
1
2
3
4
5
6
7
8
9
<select id="getUserById" resultType="User">
select * from t_user where id = 1
</select>

<!--或者-->

<select id="getUserById" resultType="user">
select * from t_user where id = 1
</select>

6.4 mappers

有3种方法来配置xml映射文件。

  • 使用相对于类路径的资源
1
<mapper resource="com/hongyi/dao/IUserDao.xml" />
  • 使用 mapper 接口类路径
1
<mapper class="com.hongyi.dao.UserDao"/>

注意:此种方法要求 mapper 接口名称和 mapper 映射文件名称相同,且放在同一级目录中。

  • 注册指定包下的所有 mapper 接口
1
<package name="com.hongyi.mybatis.mapper"/>

注意:此种方法要求 mapper 接口名称和 mapper 映射文件名称相同,且放在同一级目录中。

7 Mybatis连接池

7.1 连接池分类

Mybatis 将它自己的数据源分为三类:

  • UNPOOLED:不使用连接池的数据源
  • POOLED:使用连接池的数据源
  • JNDI:使用 JNDI 实现的数据源

相应地,MyBatis 内部分别定义了实现了java.sql.DataSource 接口的 UnpooledDataSourcePooledDataSource 类来表示 UNPOOLED、POOLED 类型的数据源。

在这三种数据源中,我们一般采用的是 POOLED 数据源(很多时候我们所说的数据源就是为了更好的管理数据库连接,也就是我们所说的连接池技术)。

image-20230625120230486

7.2 连接池配置

我们的数据源配置就是在 SqlMapConfig.xml 文件中,具体配置如下:

1
2
3
4
5
6
7
8
<!--配置数据源,也叫连接池-->
<dataSource type="POOLED">
<!--配置连接数据库的基本信息-->
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost/sakila"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>

7.3 Unpooled配置分析

接口DataSource中有一个重要方法getConnection,即获取连接:

1
2
3
4
5
6
public interface DataSource  extends CommonDataSource, Wrapper {

Connection getConnection() throws SQLException;
//...

}
1
2
3
public Connection getConnection() throws SQLException {
return this.doGetConnection(this.username, this.password);
}

调用了doGetConnection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private Connection doGetConnection(String username, String password) throws SQLException {
Properties props = new Properties();
if (this.driverProperties != null) {
props.putAll(this.driverProperties);
}
// 用户名
if (username != null) {
props.setProperty("user", username);
}
// 密码
if (password != null) {
props.setProperty("password", password);
}
// 调用doGetConnection,传入Properties props
return this.doGetConnection(props);
}

调用doGetConnection(props)

1
2
3
4
5
6
7
8
9
10
private Connection doGetConnection(Properties properties) throws SQLException {
// 注册驱动
this.initializeDriver();
// 获取连接,jdbc套路
Connection connection = DriverManager.getConnection(this.url, properties);
// 配置连接,例如超时时间,事务的自动提交和隔离级别
this.configureConnection(connection);
// 返回连接
return connection;
}

initializeDriver注册驱动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private synchronized void initializeDriver() throws SQLException {
if (!registeredDrivers.containsKey(this.driver)) {
try {
Class driverType;
if (this.driverClassLoader != null) {
// 注册驱动,jdbc套路
driverType = Class.forName(this.driver, true, this.driverClassLoader);
} else {
driverType = Resources.classForName(this.driver);
}
// ...
} catch (Exception var3) {
throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + var3);
}
}
}

7.4 Pooled配置分析

PooledDataSource类同样实现了DataSource接口和对应方法。

成员变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class PooledDataSource implements DataSource {
private static final Log log = LogFactory.getLog(PooledDataSource.class);
// 连接池状态
private final PoolState state = new PoolState(this);
// UnpooledDataSource对象
private final UnpooledDataSource dataSource;
// 最大活动连接数
protected int poolMaximumActiveConnections = 10;
// 最大空闲连接数
protected int poolMaximumIdleConnections = 5;
protected int poolMaximumCheckoutTime = 20000;
protected int poolTimeToWait = 20000;
protected int poolMaximumLocalBadConnectionTolerance = 3;
protected String poolPingQuery = "NO PING QUERY SET";
protected boolean poolPingEnabled;
protected int poolPingConnectionsNotUsedFor;
private int expectedConnectionTypeCode;
// ...
}

空参构造器:

1
2
3
public PooledDataSource() {
this.dataSource = new UnpooledDataSource();
}

PoolState类记录了连接池的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class PoolState {
protected PooledDataSource dataSource;
// 空闲连接
protected final List<PooledConnection> idleConnections = new ArrayList();
// 活动连接
protected final List<PooledConnection> activeConnections = new ArrayList();
protected long requestCount = 0L;
protected long accumulatedRequestTime = 0L;
protected long accumulatedCheckoutTime = 0L;
protected long claimedOverdueConnectionCount = 0L;
protected long accumulatedCheckoutTimeOfOverdueConnections = 0L;
protected long accumulatedWaitTime = 0L;
protected long hadToWaitCount = 0L;
protected long badConnectionCount = 0L;
// ...
}

PooledDataSource类中getConnection实现:

1
2
3
4
5
6
7
public Connection getConnection() throws SQLException {
return this.popConnection(this.dataSource.getUsername(), this.dataSource.getPassword()).getProxyConnection();
}

public Connection getConnection(String username, String password) throws SQLException {
return this.popConnection(username, password).getProxyConnection();
}

调用了popConnection方法,即从连接池中取出(pop)一个连接,并且是同步地取。

大致流程:

image-20230625145023068

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
private PooledConnection popConnection(String username, String password) throws SQLException {
boolean countedWait = false;
PooledConnection conn = null;
long t = System.currentTimeMillis();
int localBadConnectionCount = 0;

while(conn == null) {
// 加锁,防止出现并发问题:即两个或多个线程拿到同一个连接
synchronized(this.state) {
PoolState var10000;
// 空闲连接不为空,则取出一个空闲连接
if (!this.state.idleConnections.isEmpty()) {
// 取出一个连接
conn = (PooledConnection)this.state.idleConnections.remove(0);
if (log.isDebugEnabled()) {
log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
}
}
// 前提:空闲连接为空
// 活动连接数小于设定的最大活动连接数,则新建一个连接
else if (this.state.activeConnections.size() < this.poolMaximumActiveConnections) {
conn = new PooledConnection(this.dataSource.getConnection(), this);
if (log.isDebugEnabled()) {
log.debug("Created connection " + conn.getRealHashCode() + ".");
}
}
// 取出最老的连接
else {
PooledConnection oldestActiveConnection = (PooledConnection)this.state.activeConnections.get(0);
long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
if (longestCheckoutTime > (long)this.poolMaximumCheckoutTime) {
++this.state.claimedOverdueConnectionCount;
var10000 = this.state;
var10000.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
var10000 = this.state;
var10000.accumulatedCheckoutTime += longestCheckoutTime;
this.state.activeConnections.remove(oldestActiveConnection);
if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
try {
oldestActiveConnection.getRealConnection().rollback();
} catch (SQLException var15) {
log.debug("Bad connection. Could not roll back");
}
}

conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
oldestActiveConnection.invalidate();
if (log.isDebugEnabled()) {
log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
}
} else {
try {
if (!countedWait) {
++this.state.hadToWaitCount;
countedWait = true;
}

if (log.isDebugEnabled()) {
log.debug("Waiting as long as " + this.poolTimeToWait + " milliseconds for connection.");
}

long wt = System.currentTimeMillis();
this.state.wait((long)this.poolTimeToWait);
var10000 = this.state;
var10000.accumulatedWaitTime += System.currentTimeMillis() - wt;
} catch (InterruptedException var16) {
break;
}
}
}

if (conn != null) {
if (conn.isValid()) {
if (!conn.getRealConnection().getAutoCommit()) {
conn.getRealConnection().rollback();
}

conn.setConnectionTypeCode(this.assembleConnectionTypeCode(this.dataSource.getUrl(), username, password));
conn.setCheckoutTimestamp(System.currentTimeMillis());
conn.setLastUsedTimestamp(System.currentTimeMillis());
this.state.activeConnections.add(conn);
++this.state.requestCount;
var10000 = this.state;
var10000.accumulatedRequestTime += System.currentTimeMillis() - t;
} else {
if (log.isDebugEnabled()) {
log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
}

++this.state.badConnectionCount;
++localBadConnectionCount;
conn = null;
if (localBadConnectionCount > this.poolMaximumIdleConnections + this.poolMaximumLocalBadConnectionTolerance) {
if (log.isDebugEnabled()) {
log.debug("PooledDataSource: Could not get a good connection to the database.");
}

throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
}
}
}
}
}

if (conn == null) {
if (log.isDebugEnabled()) {
log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
}

throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection.");
} else {
return conn;
}
}

8 Mybatis事务

8.1 JDBC中的事务

在 JDBC 中我们可以通过手动方式将事务的提交改为手动方式,通过 setAutoCommit()方法就可以调整。

image-20230625145336185

8.2 事务提交方式

Mybatis事务的提交本质上使用了JDBC中的connection.commit()

Mybatis中事务的提交方式,本质上调用 JDBC 的 setAutoCommit()来实现事务控制。

代码示例

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
27
28
29
30
31
32
@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();
}

执行结果:

image-20230625145912250

这是我们的 Connection 的整个变化过程,通过分析我们能够发现之前的 CRUD 操作过程中,我们都要手动进行事务的提交,原因是 setAutoCommit() 方法,在执行时它的值被设置为 false 了,所以我们在 CRUD 操作中,必须通过 sqlSession.commit() 方法来执行提交操作。

8.3 自动提交事务

openSession有一个重载的构造函数,传入布尔值来决定是否开启自动提交,默认为false

1
2
3
4
public interface SqlSessionFactory {
SqlSession openSession(boolean var1);
// ...
}

实现类:

1
2
3
4
5
6
public class DefaultSqlSessionFactory implements SqlSessionFactory {
public SqlSession openSession() {
return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false); // 默认为false
}
// ...
}

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
@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);
}

执行结果:

image-20230625150414169

9 Mybatis注解开发

9.1 简介

Mybatis支持注解开发,但是需要明确的是,Mybatis仅仅是把 映射配置文件(IxxxDao.xml) 使用注解代替了,而Mybatis的全局配置文件仍然是xml

常用注解:

  • @Select:用于配置查询语句。相当于select标签
  • @Insert:用于配置插入语句。相当于insert标签
  • @SelectKey:用于插入数据时,获取主键值
  • @Update
  • @Delete
  • @Results:相当于resultMap标签
  • @Result:相当于resultMap里的id或者result标签
  • @Many:相当于resultMap里的collection
  • @One:相当于resultMap里的association

配置文件:

1
2
3
<mappers>
<package name="com.hongyi.dao"/>
</mappers>

9.2 CRUD

  • IUserDao
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public interface IUserDao {

/**
* 查询所有用户
* @return
*/
@Select("select * from user")
List<User> findAll();

/**
* 保存用户
* @param user
*/
@Insert("insert into user(username,address,sex,birthday)values(#{username},#{address},#{sex},#{birthday})")
void saveUser(User user);

/**
* 更新用户
* @param user
*/
@Update("update user set username=#{username},sex=#{sex},birthday=#{birthday},address=#{address} where id=#{id}")
void updateUser(User user);

/**
* 删除用户
* @param userId
*/
@Delete("delete from user where id=#{id} ")
void deleteUser(Integer userId);

/**
* 根据id查询用户
* @param userId
* @return
*/
@Select("select * from user where id=#{id} ")
User findById(Integer userId);

/**
* 根据用户名称模糊查询
* @param username
* @return
*/
// @Select("select * from user where username like #{username} ")
@Select("select * from user where username like '%${value}%' ")
List<User> findUserByName(String username);

/**
* 查询总用户数量
* @return
*/
@Select("select count(*) from user ")
int findTotalUser();
}
  • 测试方法
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public class AnnotationCRUDTest {
private InputStream in;
private SqlSessionFactory factory;
private SqlSession session;
private IUserDao userDao;

@Before
public void init() throws Exception{
in = Resources.getResourceAsStream("SqlMapConfig.xml");
factory = new SqlSessionFactoryBuilder().build(in);
session = factory.openSession();
userDao = session.getMapper(IUserDao.class);
}

// 手动提交
@After
public void destroy() throws Exception{
session.commit();
session.close();
in.close();
}

@Test
public void testSave(){
User user = new User();
user.setUsername("mybatis annotation");
user.setAddress("北京市昌平区");
userDao.saveUser(user);
}

@Test
public void testUpdate(){
User user = new User();
user.setId(57);
user.setUsername("mybatis annotation update");
user.setAddress("北京市海淀区");
user.setSex("男");
user.setBirthday(new Date());
userDao.updateUser(user);
}


@Test
public void testDelete(){
userDao.deleteUser(51);
}

@Test
public void testFindOne(){
User user = userDao.findById(57);
System.out.println(user);
}


@Test
public void testFindByName(){
// List<User> users = userDao.findUserByName("%mybatis%");
List<User> users = userDao.findUserByName("mybatis");
for(User user : users){
System.out.println(user);
}
}

@Test
public void testFindTotal(){
int total = userDao.findTotalUser();
System.out.println(total);
}
}

9.3 多表查询