mybatis基础详解

Mybatis基础详解

主键返回(mybatis的自增主键或者非自增主键)

批量查询

动态传参

查询缓存(一级缓存、二级缓存)

延迟加载(侵入式延迟加载、深度延迟加载)

关联查询(一对一、一对映射)

逆向工程

PageHelper分页插件

注解开发

介绍篇

认识自己

开发人员 OR 研发人员?

主要是使用成熟的框架去开发应用功能,还是使用JavaEE、JVM、并发编程、NIO/Netty等知识点实现 编写自定义框架或者解决高并发场景下的非功能性需求,比如如何提高并发能力等?

如何进行接下来的学习呢?

他山之石,可以攻玉!!学习人家的框架,写出自己的框架。

认识框架

什么是框架

摘自百度:

可以说,一个框架是一个可复用的设计构件,它规定了应用的体系结构,阐明了整个设计、协作 构件之间的依赖关系、责任分配和控制流程,表现为一组抽象类以及其实例之间协作的方法,它 为构件复用提供了上下文(Context)关系。因此构件库的大规模重用也需要框架。

为什么使用框架

  • 因为软件系统发展到今天已经很复杂了,特别是服务器端软件,涉及到的知识,内容,问题太多。 在某些方面使用别人成熟的框架,就相当于让别人帮你完成一些基础工作,你只需要集中精力完成 系统的业务逻辑设计。

  • 而且框架一般是成熟,稳健的,它可以处理系统很多细节问题,比如,事务处理,安全性,数据流 控制等问题。

  • 还有框架一般都经过很多人使用,所以结构很好,所以扩展性也很好,而且它是不断升级的,你可 以直接享受别人升级代码带来的好处。

软件开发的三层结构

我们用三层结构主要是使项目结构更清楚,分工更明确,有利于后期的维护和升级. 三层结构包含:表现层,业务层,持久层

image-20200203233833282

认识设计模式

设计模式概述

  • 设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。

  • 设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件 开发人员经过相当长的一段时间的试验和错误总结出来的。

  • 设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模 式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。

  • 设计模式不是一种方法和技术,而是一种思想。

  • 设计模式和具体的语言无关,学习设计模式就是要建立面向对象的思想,尽可能的面向接口编程, 低耦合,高内聚,使设计的程序可复用。

  • 学习设计模式能够促进对面向对象思想的理解,反之亦然。它们相辅相成。

设计模式的类型

总体来说,设计模式分为三类23种:

image-20200203234106800

认识MyBatis

mybatis参考网址:http://www.mybatis.org/mybatis-3/zh/index.html

Github源码地址:https://github.com/mybatis/mybatis-3

Mybatis是什么

MyBatis 是一款优秀的持久层框架,它支持定制化SQL、存储过程以及高级映射。MyBatis 避免了几乎 所有的 JDBC代码和手动设置参数以及获取结果集,它可以使用简单的XML或注解来配置和映射SQL信 息,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。

Mybatis的由来

  • MyBatis 本是apache的一个开源项目iBatis。

  • 2010年这个项目由apache software foundation 迁移到了google code,并且改名为MyBatis 。

  • 2013年11月迁移到Github。

ORM是什么

对象-关系映射(OBJECT/RELATIONALMAPPING,简称ORM),是随着面向对象的软件开发方法发展 而产生的。用来把对象模型表示的对象映射到基于SQL 的关系模型数据库结构中去。这样,我们在具体 的操作实体对象的时候,就不需要再去和复杂的 SQL 语句打交道,只需简单的操作实体对象的属性和 方法 。ORM 技术是在对象和关系之间提供了一条桥梁,前台的对象型数据和数据库中的关系型的数据 通过这个桥梁来相互转化。

ORM框架和MyBatis的区别

image-20200203235723649

入门篇

编码流程

  1. 编写全局配置文件:SqlMapConfig.xml

  2. 映射文件:xxxMapper.xml

  3. 编写dao代码:xxxDao接口、xxxDaoImpl实现类

  4. POJO类

  5. 单元测试类

项目搭建

创建maven工程:mybatis-demo

POM文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependencies>
<!-- mybatis依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.0/version>
</dependency>
<!-- mysql依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.35</version>
</dependency>
<!-- 单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>

SqlMapConfig.xml

不需要有过多的关注,在实际应用中可能见不到,在和Spring整合之后,Spring提供了一个类,里面拥有了改配置的所有功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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 resource="db.properties"></properties>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="${db.driver}" />
<property name="url" value="${db.url}" />
<property name="username" value="${db.username}" />
<property name="password" value="${db.password}" />
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="UserMapper.xml" />
</mappers>
</configuration>

PO类

1
2
3
4
5
6
7
8
public class User {
private int id;
private String username;
private Date birthday;
private String sex;
private String address;
// getter\setter方法
}

UserMapper.xml

1
2
3
4
5
6
<?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="test">
</mapper>

需求实现

映射文件

1
2
3
4
5
6
7
8
9
10
<!-- 根据id获取用户信息 -->
<select id="findUserById" parameterType="int"
resultType="com.kkb.mybatis.po.User">
select * from user where id = #{id}
</select>
<!-- 根据名称模糊查询用户列表 -->
<select id="findUserByUsername" parameterType="java.lang.String"
resultType="com.kkb.mybatis.po.User">
select * from user where username like '%${value}%'
</select>

配置说明:

  1. parameterType:定义输入参数的Java类型

    注意事项

    • 如果parameterType为简单类型(基本类型+String 类型),#{}中的参数名称可以任意
    • 如果parameterType为POJO类型,#{}中的参数名称必须和POJO中的属性名称一致
    • 如果resultType为POJO类型,SELECT中的列名和POJO中的属性名称一致
  2. resultType:定义结果映射类型,Java里的属性名称和要查询的列的名称要一致才能封装成功

  3. #{}:相当于JDBC中的?占位符

  4. #{id}表示使用preparedstatement设置占位符号并将输入变量id传到sql。

  5. ${value}:取出参数名为value的值。将${value}占位符替换。
    注意:如果是取简单数据类型的参数,括号中的参数名称必须为value

  6. resultType参数是必须要有的,但是parameterType可以省略

dao接口

1
2
3
4
public interface UserDao {
public User findUserById(int id) throws Exception;
public List<User> findUsersByName(String name) throws Exception;
}

实现类

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
public class UserDaoImpl implements UserDao {
//注入SqlSessionFactory
public UserDaoImpl(SqlSessionFactory sqlSessionFactory){
this. sqlSessionFactory = sqlSessionFactory;
}
private SqlSessionFactory sqlSessionFactory;
@Override
public User findUserById(int id) throws Exception {
SqlSession session = sqlSessionFactory.openSession();
User user = null;
try {
//通过sqlsession调用selectOne方法获取一条结果集
//参数1:指定定义的statement的id,参数2:指定向statement中传递的参数
//namespace类似java中的包名,方便管理sql
//命名空间.id的方式可以唯一定义sql语句
user = session.selectOne("test.findUserById", id);
System.out.println(user);
} finally{
session.close();
}
return user;
}
@Override
public List<User> findUsersByName(String name) throws Exception {
SqlSession session = sqlSessionFactory.openSession();
List<User> users = null;
try {
users = session.selectList("test.findUsersByName", name);
System.out.println(users);
} finally{
session.close();
}
return users;
}
}

生命周期(作用范围)

sqlsession:方法级别

sqlsessionFactory:全局范围(应用级别)

sqlsessionFactoryBuilder:方法级别

sqlsessionFactory工厂不管怎么创建,都不应该在Dao接口层进行调用 ,应该在谁调用Dao接口的类中注入工厂即可,注入之后可以调用并使用sqlsession,sqlsession是阅读源码的入口

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MybatisTest {
private SqlSessionFactory sqlSessionFactory;
@Before
public void init() throws Exception {
SqlSessionFactoryBuilder sessionFactoryBuilder = new
SqlSessionFactoryBuilder();
InputStream inputStream =
Resources.getResourceAsStream("SqlMapConfig.xml");
sqlSessionFactory = sessionFactoryBuilder.build(inputStream);
}
@Test
public void testFindUserById() {
UserDao userDao = new UserDaoImpl(sqlSessionFactory);
User user = userDao.findUserById(22);
System.out.println(user);
}
@Test
public void testFindUsersByName() {
UserDao userDao = new UserDaoImpl(sqlSessionFactory);
List<User> users = userDao.findUsersByName("老郭");
System.out.println(users);
}
}

基础应用篇

mapper代理开发方式

此处使用的是JDK的动态代理方式,延迟加载使用的cglib动态代理方式

代理理解

代理分为静态代理和动态代理。此处先不说静态代理,因为Mybatis中使用的代理方式是动态代理

动态代理分为两种方式:

  • 基于JDK的动态代理–针对有接口的类进行动态代理
  • 基于CGLIB的动态代理–通过子类继承父类的方式去进行代理

XML方式

开发方式

只需要开发Mapper接口(dao接口)和Mapper映射文件,不需要编写实现类

开发规范

Mapper接口开发方式需要遵循以下规范:

1、 Mapper接口的类路径与Mapper.xml文件中的namespace相同

2、 Mapper接口方法名称和Mapper.xml中定义的每个statement的id相同

3、 Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql 的parameterType的类型相同

4、 Mapper接口方法的返回值类型和mapper.xml中定义的每个sql的resultType的类型相同

mapper映射文件

1
2
3
4
5
6
7
8
9
10
11
<?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="com.kkb.mybatis.mapper.UserMapper">
<!-- 根据id获取用户信息 -->
<select id="findUserById" parameterType="int"
resultType="com.kkb.mybatis.po.User">
select * from user where id = #{id}
</select>
</mapper>

mapper接口

1
2
3
4
5
6
7
/**
* 用户管理mapper
*/
public interface UserMapper {
//根据用户id查询用户信息
public User findUserById(int id) throws Exception;
}

全局配置文件中加载映射文件

1
2
3
4
<!-- 加载映射文件 -->
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
</mappers>

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class UserMapperTest{
private SqlSessionFactory sqlSessionFactory;
@Before
public void setUp() throws Exception {
//mybatis配置文件
String resource = "SqlMapConfig.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
//使用SqlSessionFactoryBuilder创建sessionFactory
sqlSessionFactory = new
SqlSessionFactoryBuilder().build(inputStream);
}
@Test
public void testFindUserById() throws Exception {
//获取session
SqlSession session = sqlSessionFactory.openSession();
//获取mapper接口的代理对象
UserMapper userMapper = session.getMapper(UserMapper.class);
//调用代理对象方法
User user = userMapper.findUserById(1);
System.out.println(user);
//关闭session
session.close();
}
}

注解方式

开发方式

只需要编写mapper接口文件接口

mapper接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface AnnotationUserMapper {
// 查询
@Select("SELECT * FROM user WHERE id = #{id}")
public User findUserById(int id);
// 模糊查询用户列表
@Select("SELECT * FROM user WHERE username LIKE '%${value}%'")
public List<User> findUserList(String username);
// 添加并实现主键返回
@Insert("INSERT INTO user (username,birthday,sex,address) VALUES (#
{username},#{birthday},#{sex},#{address})")
@SelectKey(statement = "SELECT LAST_INSERT_ID()", keyProperty = "id",
resultType = int.class, before = false)
public void insertUser(User user);
}

测试代码

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
public class AnnotationUserMapperTest {
private SqlSessionFactory sqlSessionFactory;
/**
* @Before注解的方法会在@Test注解的方法之前执行
*
* @throws Exception
*/
@Before
public void init() throws Exception {
// 指定全局配置文件路径
String resource = "SqlMapConfig.xml";
// 加载资源文件(全局配置文件和映射文件)
InputStream inputStream = Resources.getResourceAsStream(resource);
// 还有构建者模式,去创建SqlSessionFactory对象
sqlSessionFactory = new
SqlSessionFactoryBuilder().build(inputStream);
}
@Test
public void testFindUserById() {
SqlSession sqlSession = sqlSessionFactory.openSession();
AnnotationUserMapper userMapper =
sqlSession.getMapper(AnnotationUserMapper.class);
User user = userMapper.findUserById(1);
System.out.println(user);
}
@Test
public void testFindUserList() {
SqlSession sqlSession = sqlSessionFactory.openSession();
AnnotationUserMapper userMapper =
sqlSession.getMapper(AnnotationUserMapper.class);
List<User> list = userMapper.findUserList("老郭");
System.out.println(list);
}
@Test
public void testInsertUser() {
SqlSession sqlSession = sqlSessionFactory.openSession();
AnnotationUserMapper userMapper =
sqlSession.getMapper(AnnotationUserMapper.class);
User user = new User();
user.setUsername("开课吧-2");
user.setSex("1");
user.setAddress("致真大厦");
userMapper.insertUser(user);
System.out.println(user.getId());
}
}

全局配置文件

配置内容

SqlMapConfig.xml中配置的内容和顺序如下:必须按照如下的顺序进行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
properties(属性)

settings(全局配置参数)

typeAliases(类型别名)

typeHandlers(类型处理器)--Java类型--JDBC类型--->数据库类型转换

objectFactory(对象工厂)

plugins(插件)--可以在Mybatis执行SQL语句的流程中,横叉一脚去实现一些功能增强,比如
PageHelper分页插件,就是第三方实现的一个插件

environments(环境集合属性对象)

environment(环境子属性对象)

transactionManager(事务管理)

dataSource(数据源)

mappers(映射器)

properties标签

SqlMapConfig.xml可以引用java属性文件中的配置信息。

1、在classpath下定义db.properties文件

1
2
3
4
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/ssm?characterEncoding=utf-8
jdbc.username=root
jdbc.password=root

2、在SqlMapConfig.xml文件中,引用db.properties中的属性,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
<properties resource="db.properties"/>
<environments default="development">
<environment id="development">
<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>

properties标签除了可以使用resource属性,引用properties文件中的属性。还可以在properties标签内定义property子标签来定义属性和属性值,具体如下:

1
2
3
<properties>
<property name="driver" value="com.mysql.jdbc.Driver"/>
</properties>

注意: MyBatis 将按照下面的顺序来加载属性:

  • 读取properties 元素体内定义的属性。

  • 读取properties 元素中resource或 url 加载的属性,它会覆盖已读取的同名属性。

typeAlias标签

别名的作用:就是为了简化映射文件中parameterType和ResultType中的POJO类型名称编写

默认支持别名

image-20200204131759687

自定义别名

在SqlMapConfig.xml中进行如下配置:

1
2
3
4
5
6
<typeAliases>
<!-- 单个别名定义 -->
<typeAlias alias="user" type="com.kkb.mybatis.po.User"/>
<!-- 批量别名定义,扫描整个包下的类,别名为类名(首字母大写或小写都可以) -->
<package name="com.kkb.mybatis.po"/>
</typeAliases>

mappers标签

<mapper resource=””/>

使用相对于类路径的资源

1
<mapper resource="sqlmap/User.xml" />

<mapper url=””>

使用绝对路径加载资源

1
<mapper url="file://d:/sqlmap/User.xml" />

<mapper class=””/>

使用mapper接口类路径,加载映射文件

1
<mapper class="com.kkb.mybatis.mapper.UserMapper"/>

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

<package name=””/>

注册指定包下的所有mapper接口,来加载映射文件

1
<package name="com.kkb.mybatis.mapper"/>

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

输入映射和输出映射

parameterType(输入类型)

parameterType属性可以映射的输入参数Java类型有:简单类型、POJO类型、Map类型、List类型 (数组)

  • Map类型和POJO类型的用法类似

  • List类型在动态SQL部分进行讲解。

传递简单类型

需求

根据用户名称模糊查询用户列表

映射文件

image-20200204134001455

Mapper接口

image-20200204134043780

如果返回的结果是一个集合,只需要关注集合内存储的类型,集合内存储是什么类型,resresultType就是什么类型

${value}:取出参数名为value的值。将${value}占位符替换

注意:如果是取简单数据类型的参数,括号中的参数名称必须为value,如果是取POJO类型或者Map类型的参数,则与#{}用法相同

#{}和${}区别

区别1

1
2
#{} :相当于JDBC SQL语句中的占位符? (PreparedStatement)
${} : 相当于JDBC SQL语句中的连接符合 + (Statement)

区别2

1
2
#{} :进行输入映射的时候,会对参数进行类型解析(如果是String类型,那么SQL语句会自动加上’’)
${} :进行输入映射的时候,将参数原样输出到SQL语句中

区别3

1
2
3
4
#{} :如果进行简单类型(String、Date、8种基本类型的包装类)的输入映射时,#{}中参数名称
可以任意
${} : 如果进行简单类型(String、Date、8种基本类型的包装类)的输入映射时,${}中参数名称
必须是value

区别4

1
${} :存在SQL注入问题 ,如果使用 OR 1=1 关键字则将会忽略查询条件

UserMapper.xml中的内容

1
2
3
4
<!-- 根据名称模糊查询用户列表 -->
<select id="findUserByUsername" resultType="com.kkb.mybatis.phase01.po.User">
select * from user where username ='${value}'or 1=1
</select>

userDao接口中的内容

1
2
3
4
5
6
public interface UserDao {

User findUserById(int id);

List<User> findUserByUsername(String username);
}

UserDaoImpl实现类中的内容

1
2
3
4
5
6
7
public List<User> findUserByUsername(String username) {
// sqlsessionFactory工厂类去创建sqlsession会话
SqlSession sqlSession = sqlSessionFactory.openSession();
List<User> selectList = sqlSession.selectList("test.findUserByUsername", username);

return selectList;
}

像这里,查询条件中加入or 1=1,则接口和实现类中的返回值类型就必须是集合类型,这说明使用OR 1=1关键字则将会忽略查询条件,从而转成查询所有的数据

传递POJO对象

需求

添加用户,并返回主键

映射文件

image-20200204142312215

添加selectKey标签实现主键返回

image-20200204143015225Mapper接口

image-20200204143317434

传递pojo包装对象

OGNL

#{}:是通过反射获取数据的—StaticSqlSource

${}:是通过OGNL表达式会随着对象的嵌套而相应的发生层级变化 –DynamicSqlSource

1
2
3
4
5
6
7
8
对象导航图语言
|---User(参数值对象)
|--username--张三
|--birthday
|--sex--男
|--dept -- Department
|--name
|--no

OGNL表达式去获取Department对象的name属性:dept.name

需求

通过包装POJO传递参数,完成用户查询

QueryVO

定义包装对象QueryVO

1
2
3
public class QueryVO {
private User user;
}

SQL语句

1
SELECT * FROM user where username like '%小明%'

Mapper文件

1
2
3
4
5
6
<!-- 使用包装类型查询用户
使用ognl从对象中取属性值,如果是包装对象可以使用.操作符来取内容部的属性
-->
<select id="findUserList" parameterType="queryVo" resultType="user">
SELECT * FROM user where username like '%${user.username}%'
</select>

Mapper接口

1
2
3
4
5
6
7
/**
* 用户管理mapper
*/
public interface UserMapper {
//综合查询用户列表
public List<User> findUserList(QueryVo queryVo)throws Exception;
}

测试方法

在UserMapperTest测试类中,添加以下测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void testFindUserList() throws Exception {
SqlSession sqlSession = sqlSessionFactory.openSession();
//获得mapper的代理对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//创建QueryVo对象
QueryVo queryVo = new QueryVo();
//创建user对象
User user = new User();
user.setUsername("小明");
queryVo.setUser(user);
//根据queryvo查询用户
List<User> list = userMapper.findUserList(queryVo);
System.out.println(list);
sqlSession.close();
}

resultType(输出类型)

resultType属性可以映射的java类型有:简单类型、POJO类型、Map类型

不过Map类型和POJO类型的使用情况类似,所以只需讲解POJO类型即可

使用要求

使用resultType进行输出映射时,要求sql语句中查询的列名和要映射的pojo的属性名一致

映射简单类型

案例需求

查询用户记录总数

Mapper映射文件

1
2
3
4
<!-- 获取用户列表总数 -->
<select id="findUserCount" resultType="int">
select count(1) from user
</select>

Mapper接口

1
2
//查询用户总数
public int findUserCount() throws Exception;

测试代码

在UserMapperTest测试类中,添加以下测试代码:

1
2
3
4
5
6
7
8
9
@Test
public void testFindUserCount() throws Exception {
SqlSession sqlSession = sessionFactory.openSession();
//获得mapper的代理对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
int count = userMapper.findUserCount();
System.out.println(count);
sqlSession.close();
}

注意:输出简单类型必须查询出来的结果集只有一列

映射pojo对象

注意:不管是单个POJO还是POJO集合,在使用resultType完成映射时,用法一样。

参考入门程序之根据用户ID查询用户信息和根据名称模糊查询用户列表的案例

resultMap

使用要求

如果sql查询列名和pojo的属性名可以不一致,通过resultMap将列名和属性名作一个对应关系,最终将查询结果映射到指定的pojo对象中

注意:resultType底层也是通过resultMap完成映射的

需求

将以下sql的查询结果进行映射:

1
SELECT id id_,username username_,birthday birthday_ FROM user

Mapper接口

1
2
// resultMap入门
public List<User> findUserListResultMap() throws Exception;

Mapper映射文件

由于sql查询列名和User类属性名不一致,所以不能使用resultType进行结构映射。

需要定义一个resultMap将sql查询列名和User类的属性名对应起来,完成结果映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- 定义resultMap:将查询的列名和映射的pojo的属性名做一个对应关系 -->
<!--
type:指定查询结果要映射的pojo的类型
id:指定resultMap的唯一标示
-->
<resultMap type="user" id="userListResultMap">
<!--
id标签:映射查询结果的唯一列(主键列)
column:查询sql的列名
property:映射结果的属性名
-->
<id column="id_" property="id"/>
<!-- result标签:映射查询结果的普通列 -->
<result column="username_" property="username"/>
<result column="birthday_" property="birthday"/>
</resultMap>
<!-- resultMap入门 -->
<select id="findUserListResultMap" resultMap="userListResultMap">
SELECT id id_,username username_,birthday birthday_ FROM user
</select>
1
2
3
4
5
6
7
8
<id>:表示查询结果集的唯一标识,非常重要。如果是多个字段为复合唯一约束则定义多个
Property:表示User类的属性
Column:表示sql查询出来的字段名
Column和property放在一块儿表示将sql查询出来的字段映射到指定的pojo类属性上
<result>:普通结果,即pojo的属性
Property:表示User类的属性
Column:表示sql查询出来的字段名
Column和property放在一块儿表示将sql查询出来的字段映射到指定的pojo类属性上

高级应用篇

关联查询

商品订单数据类型

image-20200204152202159

注意:因为一个订单信息只会是一个人下的订单,所以从查询订单信息出发,关联查询用户信息为一对一查询。如果从用户信息出发,查询用户下的订单信息则为一对多查询,因为一个用户可以下多个订单。

一对一查询

需求

查询所有订单信息,关联查询下单用户信息

SQL语句

1
2
3
4
5
6
7
SELECT
orders.*,
user.username,
user.address
FROM
orders LEFT JOIN user
ON orders.user_id = user.id

主信息:订单信息

从信息:用户信息

方法一:resultType

学生们自己实现

方法二:resultMap

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

创建扩展po类

创建OrdersExt类(该类用于结果集封装),加入User属性,user属性中用于存储关联查询的用户信 息,因为订单关联查询用户是一对一关系,所以这里使用单个User对象存储关联查询的用户信息

1
2
3
4
public class OrdersExt extends Orders {
private User user;// 用户对象
// get/set。。。。
}

Mapper映射文件

在UserMapper.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
28
29
30
31
32
<!-- 查询订单关联用户信息使用resultmap -->
<resultMap type="OrdersExt" id="ordersAndUserRstMap">
<id column="id" property="id"/>
<result column="user_id" property="userId"/>
<result column="number" property="number"/>
<result column="createtime" property="createtime"/>
<result column="note" property="note"/>
<!-- 一对一关联映射 -->
<!--
property:Orders对象的user属性
javaType:user属性对应的类型
-->
<association property="user" javaType="com.kkb.mybatis.po.User">
<!-- column:user表的主键对应的列 property:user对象中id属性-->
<id column="user_id" property="id"/>
<result column="username" property="username"/>
<result column="address" property="address"/>
</association>
</resultMap>
<select id="findOrdersAndUserRstMap" resultMap="ordersAndUserRstMap">
SELECT
o.id,
o.user_id,
o.number,
o.createtime,
o.note,
u.username,
u.address
FROM
orders o
JOIN `user` u ON u.id = o.user_id
</select>

association:表示进行一对一关联查询映射

property:表示关联查询的结果存储在com.kkb.mybatis.po.Orders的user属性中

javaType:表示关联查询的映射结果类型

Mapper接口

在UserMapper接口中,添加以下接口方法:

1
public List<OrdersExt> findOrdersAndUserRstMap() throws Exception;

测试代码

在UserMapperTest测试类中,添加测试代码:

1
2
3
4
5
6
7
8
9
10
11
public void testfindOrdersAndUserRstMap()throws Exception{
//获取session
SqlSession session = sqlSessionFactory.openSession();
//获限mapper接口实例
UserMapper userMapper = session.getMapper(UserMapper.class);
//查询订单信息
List<OrdersExt> list = userMapper.findOrdersAndUserRstMap();
System.out.println(list);
//关闭session
session.close();
}

小结

使用resultMap进行结果映射时,具体是使用association完成关联查询的映射,将关联查询信息映射到pojo对象中

一对多查询

需求

查询所有用户信息及用户关联的订单信息

SQL语句

1
2
3
4
5
6
7
8
9
SELECT
u.*,
o.id oid,
o.number,
o.createtime,
o.note
FROM
`user` u
LEFT JOIN orders o ON u.id = o.user_id

主信息:用户信息

从信息:订单信息

分析

在一对多关联查询时,只能使用resultMap进行结果映射

1、一对多关联查询时,sql查询结果有多条,而映射对象是一个

2、resultType完成结果映射的方式的一条记录映射一个对象

3、resultMap完成结果映射的方式是以[主信息]为主对象,[从信息]映射为集合或者对象,然后封装到主对象中

修改po类

在User类中加入List orders属性

image-20200204154945786

Mapper映射文件

在UserMapper.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
<resultMap type="user" id="userAndOrderRstMap">
<!-- 用户信息映射 -->
<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="orders" ofType="orders">
<id property="id" column="oid"/>
<result property="userId" column="id"/>
<result property="number" column="number"/>
<result property="createtime" column="createtime"/>
<result property="note" column="note"/>
</collection>
</resultMap>
<select id="findUserAndOrderRstMap" resultMap="userAndOrderRstMap">
SELECT
u.*,
o.id oid,
o.number,
o.createtime,
o.note
FROM
`user` u
LEFT JOIN orders o ON u.id = o.user_id
</select>

Collection标签:定义了一对多关联的结果映射

property=”orders”:关联查询的结果集存储在User对象的上哪个属性

ofType=”orders”:指定关联查询的结果集中的对象类型即List中的对象类型。此处可以使用别名,也可以使用全限定名

Mapper接口

1
2
// resultMap入门
public List<User> findUserAndOrdersRstMap() throws Exception;

测试代码

1
2
3
4
5
6
7
8
9
10
@Test
public void testFindUserAndOrdersRstMap() {
SqlSession session = sqlSessionFactory.openSession();
UserMapper userMapper = session.getMapper(UserMapper.class);
List<User> result = userMapper.findUserAndOrdersRstMap();
for (User user : result) {
System.out.println(user);
}
session.close();
}

延迟加载

什么是延迟加载

  • MyBatis中的延迟加载,也称为懒加载,是指在进行关联查询时,按照设置延迟规则推迟对关联对 象的select查询。延迟加载可以有效的减少数据库压力

  • Mybatis的延迟加载,需要通过resultMap标签中的association和collection子标签才能演示成 功

  • Mybatis的延迟加载,也被称为是嵌套查询,对应的还有嵌套结果的概念,可以参考一对多关联的 案例

  • 注意:MyBatis的延迟加载只是对关联对象的查询有延迟设置,对于主加载对象都是直接执行查 询语句的

延迟加载的分类

MyBatis根据对关联对象查询的select语句的执行时机,分为三种类型:直接加载、侵入式加载与深度延迟加载

  • 直接加载: 执行完对主加载对象的select语句,马上执行对关联对象的select查询

  • 侵入式延迟:执行对主加载对象的查询时,不会执行对关联对象的查询。但当要访问主加载对象的某个属性(该属性不是关联对象的属性)时,就会马上执行关联对象的select查询

  • 深度延迟:执行对主加载对象的查询时,不会执行对关联对象的查询。访问主加载对象的详情时也不会执行关联对象的select查询。只有当真正访问关联对象的详情时,才会执行对关联对象的 select查询

延迟加载策略需要在Mybatis的全局配置文件中,通过<settings>标签进行设置

案例准备

查询订单信息及它的下单用户信息

直接加载

通过对全局参数:lazyLoadingEnabled进行设置,默认就是false

1
2
3
4
<settings>
<!-- 延迟加载总开关 -->
<setting name="lazyLoadingEnabled" value="false"/>
</settings>

侵入式延迟加载

1
2
3
4
5
6
<settings>
<!-- 延迟加载总开关 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 侵入式延迟加载开关 -->
<setting name="aggressiveLazyLoading" value="true"/>
</settings>

深度延迟加载

1
2
3
4
5
6
<settings>
<!-- 延迟加载总开关 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 侵入式延迟加载开关 -->
<setting name="aggressiveLazyLoading" value="false"/>
</settings>

N+1问题

深度延迟加载的使用会提升性能

如果延迟加载的表数据太多,此时会产生N+1问题,主信息加载一次算1次,而从信息是会根据主 信息传递过来的条件,去查询从表多次

Mybatis缓存

缓存介绍

现在流行分布式缓存,Mybatis自身的缓存已经没有太大用处

Mybatis提供查询缓存,如果缓存中有数据就不用从数据库中获取,用于减轻数据压力,提高系统性能

Mybatis的查询缓存总共有两级,我们称之为一级缓存和二级缓存,如图:

image-20200204173259866

  • 一级缓存是SqlSession级别的缓存。在操作数据库时需要构造 sqlSession对象,在对象中有一个 数据结构(HashMap)用于存储缓存数据。不同的sqlSession之间的缓存数据区域(HashMap) 是互相不影响的

  • 二级缓存是Mapper(namespace)级别的缓存。多个SqlSession去操作同一个Mapper的sql语 句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的

一级缓存

Mybatis默认开启了一级缓存

原理

image-20200204173938178

说明

第一次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,如果没有,从数据库查询用户信息,将查询到的用户信息存储到一级缓存中

如果中间sqlSession去执行commit操作(执行插入、更新、删除),清空SqlSession中的一级缓 存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读

第二次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,缓存中有,直接从缓存中获取用户信息

测试1

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testOneLevelCache() {
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 第一次查询ID为1的用户,去缓存找,找不到就去查找数据库
User user1 = mapper.findUserById(1);
System.out.println(user1);
// 第二次查询ID为1的用户
User user2 = mapper.findUserById(1);
System.out.println(user2);
sqlSession.close();
}

测试2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testOneLevelCache() {
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 第一次查询ID为1的用户,去缓存找,找不到就去查找数据库
User user1 = mapper.findUserById(1);
System.out.println(user1);
User user = new User();
user.setUsername("隔壁老詹1");
user.setAddress("洛杉矶湖人");
//执行增删改操作,清空缓存
mapper.insertUser(user);
// 第二次查询ID为1的用户
User user2 = mapper.findUserById(1);
System.out.println(user2);
sqlSession.close();
}

具体应用

正式开发,是将mybatis和spring进行整合开发,事务控制在service中

一个service方法中包括 很多mapper方法调用:

1
2
3
4
5
6
service{
//开始执行时,开启事务,创建SqlSession对象
//第一次调用mapper的方法findUserById(1)
//第二次调用mapper的方法findUserById(1),从一级缓存中取数据
//方法结束,sqlSession关闭
}

如果是执行两次service调用查询相同 的用户信息,是不走一级缓存的,因为mapper方法结束, sqlSession就关闭,一级缓存就清空

二级缓存

原理

二级缓存是mapper(namespace)级别的

下图是多个sqlSession请求UserMapper的二级缓存图解

image-20200204174332502

说明

  • 第一次调用mapper下的SQL去查询用户信息。查询到的信息会存到该mapper对应的二级缓存区域内。

  • 第二次调用相同namespace下的mapper映射文件中相同的SQL去查询用户信息。会去对应的二级 缓存内取结果

  • 如果调用相同namespace下的mapper映射文件中的增删改SQL,并执行了commit操作。此时会 清空该namespace下的二级缓存

开启二级缓存

Mybatis默认是没有开启二级缓存,开启步骤如下:

在核心配置文件SqlMapConfig.xml中加入以下内容(开启二级缓存总开关):

1
2
3
4
<!-- 开启二级缓存总开关 -->
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>

在UserMapper映射文件中,加入以下内容,开启二级缓存:

1
2
<!-- 开启本mapper下的namespace的二级缓存,默认使用的是mybatis提供的PerpetualCache -->
<cache></cache>

实现序列化

由于二级缓存的数据不一定都是存储到内存中,它的存储介质多种多样,比如说存储到文件系统中,所 以需要给缓存的对象执行序列化。

如果该类存在父类,那么父类也要实现序列化。

image-20200204174620205

测试1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void testTwoLevelCache() {
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
SqlSession sqlSession3 = sqlSessionFactory.openSession();
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class);
// 第一次查询ID为1的用户,去缓存找,找不到就去查找数据库
User user1 = mapper1.findUserById(1);
System.out.println(user1);
// 关闭SqlSession1
sqlSession1.close();
// 第二次查询ID为1的用户
User user2 = mapper2.findUserById(1);
System.out.println(user2);
// 关闭SqlSession2
sqlSession2.close();
}

Cache Hit Radio : 缓存命中率

第一次缓存中没有记录,则命中率0.0

第二次缓存中有记录,则命中率0.5(访问两次,有一次命中)

测试2

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
@Test
public void testTwoLevelCache() {
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
SqlSession sqlSession3 = sqlSessionFactory.openSession();
UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
UserMapper mapper3 = sqlSession3.getMapper(UserMapper.class);
// 第一次查询ID为1的用户,去缓存找,找不到就去查找数据库
User user1 = mapper1.findUserById(1);
System.out.println(user1);
// 关闭SqlSession1
sqlSession1.close();
//修改查询出来的user1对象,作为插入语句的参数
user1.setUsername("隔壁老詹1");
user1.setAddress("洛杉矶湖人");
mapper3.insertUser(user1);
// 提交事务
sqlSession3.commit();
// 关闭SqlSession3
sqlSession3.close();
// 第二次查询ID为1的用户
User user2 = mapper2.findUserById(1);
System.out.println(user2);
// 关闭SqlSession2
sqlSession2.close();
}

禁用二级缓存

默认二级缓存的粒度是Mapper级别的,但是如果在同一个Mapper文件中某个查询不想使用二级缓存的 话,就需要对缓存的控制粒度更细。

在select标签中设置useCache=false,可以禁用当前select语句的二级缓存,即每次查询都是去数据库中查询,默认情况下是true,即该statement使用二级缓存。

1
2
3
4
<select id="findUserById" parameterType="int"
resultType="com.kkb.mybatis.po.User" useCache="true">
SELECT * FROM user WHERE id = #{id}
</select>
刷新二级缓存

通过flushCache属性,可以控制select、insert、update、delete标签是否属性二级缓存

默认设置

  • 默认情况下如果是select语句,那么flushCache是false

  • 如果是insert、update、delete语句,那么flushCache是true

默认配置解读

如果查询语句设置成true,那么每次查询都是去数据库查询,即意味着该查询的二级缓存失效

如果增删改语句设置成false,即使用二级缓存,那么如果在数据库中修改了数据,而缓存数据还是 原来的,这个时候就会出现脏读

flushCache设置如下:

1
2
3
4
5
<select id="findUserById" parameterType="int"
resultType="com.kkb.mybatis.po.User" useCache="true"
flushCache="true">
SELECT * FROM user WHERE id = #{id}
</select>
应用场景

使用场景

对于访问响应速度要求高,但是实时性不高的查询,可以采用二级缓存技术

注意事项

在使用二级缓存的时候,要设置一下刷新间隔(cache标签中有一个flashInterval属性)来定时 刷新二级缓存,这个刷新间隔根据具体需求来设置,比如设置30分钟、60分钟等,单位为毫秒

局限性

Mybatis二级缓存对细粒度的数据级别的缓存实现不好

场景

对商品信息进行缓存,由于商品信息查询访问量大,但是要求用户每次查询都是最新的商品信息, 此时如果使用二级缓存,就无法实现当一个商品发生变化只刷新该商品的缓存信息而不刷新其他商 品缓存信息,因为二级缓存是mapper级别的,当一个商品的信息发送更新,所有的商品信息缓存 数据都会清空

解决方法

此类问题,需要在业务层根据需要对数据有针对性的缓存

比如可以对经常变化的数据操作单独放到另一个namespace的mapper中

动态SQL

动态SQL的思想:就是使用不同的动态SQL标签去完成字符串的拼接处理、循环判断

解决的问题是:

1、 在映射文件中,会编写很多有重叠部分的SQL语句,比如SELECT语句和WHERE语句等这些重叠语句,该如何处理

2、SQL语句中的where条件有多个,但是页面只传递过来一个条件参数,此时会发生问题

if标签

综合查询的案例中,查询条件是由页面传入,页面中的查询条件可能输入用户名称,也可能不输入用户名称

1
2
3
4
5
6
7
8
<select id="findUserList" parameterType="queryVo" resultType="user">
SELECT * FROM user where 1=1
<if test="user != null">
<if test="user.username != null and user.username != ''">
AND username like '%${user.username}%'
</if>
</if>
</select>

注意:要做『不等于空』字符串校验

where标签

上边的sql中的1=1,虽然可以保证sql语句的完整性,但是存在性能问题。Mybatis提供where标签解决该问题

代码修改如下

1
2
3
4
5
6
7
8
9
10
11
<select id="findUserList" parameterType="queryVo" resultType="user">
SELECT * FROM user
<!-- where标签会处理它后面的第一个and -->
<where>
<if test="user != null">
<if test="user.username != null and user.username != ''">
AND username like '%${user.username}%'
</if>
</if>
</where>
</select>

sql片段

在映射文件中可使用sql标签将重复的sql提取出来,然后使用include标签引用即可,最终达到sql重用的目的,具体实现如下

  • 原映射文件中的代码
1
2
3
4
5
6
7
8
9
10
11
<select id="findUserList" parameterType="queryVo" resultType="user">
SELECT * FROM user
<!-- where标签会处理它后面的第一个and -->
<where>
<if test="user != null">
<if test="user.username != null and user.username !=''">
AND username like '%${user.username}%'
</if>
</if>
</where>
</select>
  • 将where条件抽取出来
1
2
3
4
5
6
7
<sql id="query_user_where">
<if test="user != null">
<if test="user.username != null and user.username != ''">
AND username like '%${user.username}%'
</if>
</if>
</sql>
  • 使用include引用
1
2
3
4
5
6
7
8
9
<!-- 使用包装类型查询用户 使用ognl从对象中取属性值,如果是包装对象可以使用.操作符来
取内容部的属性 -->
<select id="findUserList" parameterType="queryVo" resultType="user">
SELECT * FROM user
<!-- where标签会处理它后面的第一个and -->
<where>
<include refid="query_user_where"></include>
</where>
</select>

注意

如果引用其它mapper.xml的sql片段,则在引用时需要加上namespace,如下:

1
<include refid="namespace.sql片段”/>

foreach

需求

综合查询时,传入多个id查询用户信息,用下边两个sql实现

1
2
SELECT * FROM USER WHERE username LIKE '%老郭%' AND (id =1 OR id =10 OR id=16)
SELECT * FROM USER WHERE username LIKE '%老郭%' AND id IN (1,10,16)

POJO

在pojo中定义list属性ids存储多个用户id,并添加getter/setter方法

1
2
3
4
5
6
public class QueryVo{

private User user;

private List<Integer> ids;
}

Mapper映射文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<sql id="query_user_where">
<if test="user != null">
<if test="user.username != null and user.username != ''">
AND username like '%${user.username}%'
</if>
</if>
<if test="ids != null and ids.size() > 0">
<!-- collection:指定输入的集合参数的参数名称 -->
<!-- item:声明集合参数中的元素变量名 -->
<!-- open:集合遍历时,需要拼接到遍历sql语句的前面 -->
<!-- close:集合遍历时,需要拼接到遍历sql语句的后面 -->
<!-- separator:集合遍历时,需要拼接到遍历sql语句之间的分隔符号 -->
<foreach collection="ids" item="id" open=" AND id IN ( "
close=" ) " separator=",">
#{id}
</foreach>
</if>
</sql>

测试代码

在UserMapperTest测试代码中,修改testFindUserList方法,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void testFindUserList() throws Exception {
SqlSession sqlSession = sqlSessionFactory.openSession();
// 获得mapper的代理对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 创建QueryVo对象
QueryVo queryVo = new QueryVo();
// 创建user对象
User user = new User();
user.setUsername("老郭");
queryVo.setUser(user);
List<Integer> ids = new ArrayList<Integer>();
ids.add(1);// 查询id为1的用户
ids.add(10); // 查询id为10的用户
queryVo.setIds(ids);
// 根据queryvo查询用户
List<User> list = userMapper.findUserList(queryVo);
System.out.println(list);
sqlSession.close();
}

注意事项

如果parameterType不是POJO类型,而是List或者Array的话,那么foreach语句中,collection属性值需要固定写死为list或者array

Mybatis逆向工程

逆向工程介绍

使用官方网站的Mapper自动生成工具mybatis-generator-core-1.3.2来针对单表生成po类 (Example)Mapper接口和mapper映射文件

实现

Mybatis逆向工程的实现前提是数据库中已经设计并创建好所有的表

pom.xml文件

最主要的是mybatis-generator-maven-plugin插件

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
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.kkb</groupId>
<artifactId>kkb-mybatis</artifactId>
<version>0.0.1-SNAPSHOT</version>

<dependencies>
<!-- mybatis依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.0</version>
</dependency>
<!-- mysql依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.35</version>
</dependency>

<!-- 分页插件 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.6</version>
</dependency>

<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>


<!-- 单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.2</version>
<configuration>
<verbose>true</verbose>
<overwrite>true</overwrite>
</configuration>
</plugin>
</plugins>
</build>
</project>

datasource.properties文件

在maven项目下的src/main/resources 目录下创建generatorConfig.xml和generator.properties配置文件

image-20200507140141388

1
2
3
4
5
jdbc.driverLocation=D:\\maven_repository\\mysql\\mysql-connector-java\\5.1.47\\mysql-connector-java-5.1.47.jar
jdbc.driverClass=com.mysql.jdbc.Driver
jdbc.connectionURL=jdbc:mysql:///sptest
jdbc.userId=root
jdbc.password=root

jdbc.driverLocation中的路径是mysql-connector-java-5.1.47.jar文件所在的位置

generatorConfig.xml

在generatorConfig.xml中配置Mapper生成的详细信息,注意修改以下几点

  1. 修改要生成的数据库表

  2. pojo文件所在包路径

  3. 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
68
69
70
71
72
73
74
75
76
77
78
79
80
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
<!--导入属性配置-->
<properties resource="datasource.properties"></properties>

<!--指定特定数据库的jdbc驱动jar包的位置-->
<classPathEntry location="${jdbc.driverLocation}"/>

<context id="default" targetRuntime="MyBatis3">

<!-- optional,旨在创建class时,对注释进行控制 -->
<commentGenerator>
<property name="suppressDate" value="true"/>
<property name="suppressAllComments" value="true"/>
</commentGenerator>

<!--jdbc的数据库连接 -->
<jdbcConnection
driverClass="${jdbc.driverClass}"
connectionURL="${jdbc.connectionURL}"
userId="${jdbc.userId}"
password="${jdbc.password}">
</jdbcConnection>


<!-- 非必需,类型处理器,在数据库类型和java类型之间的转换控制-->
<javaTypeResolver>
<property name="forceBigDecimals" value="false"/>
</javaTypeResolver>


<!-- Model模型生成器,用来生成含有主键key的类,记录类以及查询Example类
targetPackage 指定生成的model生成所在的包名
targetProject 指定在该项目下所在的路径
-->
<javaModelGenerator targetPackage="com.ssm.test.model"
targetProject="src/main/java">

<!-- 是否允许子包,即targetPackage.schemaName.tableName -->
<property name="enableSubPackages" value="false"/>
<!-- 是否对model添加 构造函数 -->
<property name="constructorBased" value="true"/>
<!-- 是否对类CHAR类型的列的数据进行trim操作 -->
<property name="trimStrings" value="true"/>
<!-- 建立的Model对象是否不可改变 即生成的Model对象不会有 setter方法,只有构造方法 -->
<property name="immutable" value="false"/>
</javaModelGenerator>

<!--Mapper映射文件生成所在的目录 为每一个数据库的表生成对应的SqlMap文件 -->
<sqlMapGenerator targetPackage="com.ssm.test.mapper"
targetProject="src/main/java">
<property name="enableSubPackages" value="false"/>
</sqlMapGenerator>

<!-- 客户端代码,生成易于使用的针对Model对象和XML配置文件 的代码
type="ANNOTATEDMAPPER",生成Java Model 和基于注解的Mapper对象
type="MIXEDMAPPER",生成基于注解的Java Model 和相应的Mapper对象
type="XMLMAPPER",生成SQLMap XML文件和独立的Mapper接口
-->
<javaClientGenerator targetPackage="com.ssm.test.dao"
targetProject="src/main/java" type="XMLMAPPER">
<property name="enableSubPackages" value="true"/>
</javaClientGenerator>

<!--tableName:指定了表名
domainObjectName:指定了实体类的名称
-->
<table tableName="user" domainObjectName="user"
enableCountByExample="false" enableUpdateByExample="false"
enableDeleteByExample="false" enableSelectByExample="false"
selectByExampleQueryId="false">
</table>


</context>
</generatorConfiguration>

生成文件

点击这里生成相应文件

image-20200507140351458

注意事项

每次执行逆向工程代码之前,先删除原来已经生成的mapper xml文件再进行生成

  • mapper.xml文件的内容不是被覆盖而是进行内容追加,会导致mybatis解析失败
  • po类及mapper.java文件的内容是直接覆盖没有此问题

Mysql-connector-java与Java、Mysql版本对应的关系

image-20200507133646736

PageHelper分页插件

PageHelper分页插件介绍

https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/en/HowToUse.md

  • 如果你也在用Mybatis,建议尝试该分页插件,这个一定是最方便使用的分页插件

  • 目前几乎支持所有的关系型数据库

  • 最新版本是5.1.6

使用方法

添加依赖

1
2
3
4
5
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.6</version>
</dependency>

配置PageHelper

Mybatis全局配置文件

1
2
3
4
5
6
7
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- config params as the following -->
<!--指定方言-->
<property name="helperDialect" value="mysql"/>
</plugin>
</plugins>

spring配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<bean id="sqlSessionFactory"
class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- other configuration -->
<property name="plugins">
<array>
<bean class="com.github.pagehelper.PageInterceptor">
<property name="properties">
<!-- config params as the following -->
<value>
helperDialect=mysql
</value>
</property>
</bean>
</array>
</property>
</bean>

项目中使用PageHelper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//获取第1页,10条内容,默认查询总数count
PageHelper.startPage(1, 10);
List<Country> list = countryMapper.selectAll();
//用PageInfo对结果进行包装
PageInfo page = new PageInfo(list);
//测试PageInfo全部属性
//PageInfo包含了非常全面的分页属性
assertEquals(1, page.getPageNum());
assertEquals(10, page.getPageSize());
assertEquals(1, page.getStartRow());
assertEquals(10, page.getEndRow());
assertEquals(183, page.getTotal());
assertEquals(19, page.getPages());
assertEquals(1, page.getFirstPage());
assertEquals(8, page.getLastPage());
assertEquals(true, page.isFirstPage());
assertEquals(false, page.isLastPage());
assertEquals(false, page.isHasPreviousPage());
assertEquals(true, page.isHasNextPage());

Assert.assertEquals作用

junit.framework包下的Assert提供了多个断言方法,主用于比较测试传递进去的两个参数

Assert.assertEquals()方法及其重载方法的比较规则

  1. 如果两者一致,程序继续往下运行
  2. 如果两者不一致,中断测试方法,抛出异常信息AssertionFailedError

查看源码,以Assert.assertEquals(int expected, int actual)为例

1
2
3
4
5
6
/**
* Asserts that two ints are equal. 断言两个int是相等的
*/
static public void assertEquals(int expected, int actual) {
assertEquals(null, expected, actual);
}

可以看到里面调用了assertEquals(String message, int expected, int actual)方法

1
2
3
4
5
6
7
8
/**
* Asserts that two ints are equal. If they are not
* an AssertionFailedError is thrown with the given message.
* 如果不抛出带有 message 的异常(AssertionFailedError)信息, 则表明两者相等
*/
static public void assertEquals(String message, int expected, int actual) {
assertEquals(message, Integer.valueOf(expected), Integer.valueOf(actual));
}

可以看到, 这里把int类型封箱成为Integer类型

注释说会抛异常,但这里没有,没关系,我们接着看里面调用

assertEquals(String message, Object expected, Object actual)方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Asserts that two objects are equal. If they are not
* an AssertionFailedError is thrown with the given message.
* 如果不抛出带有 message 的异常(AssertionFailedError)信息, 则表明两者相等(这里比较的是Object对象)
*/
static public void assertEquals(String message, Object expected, Object actual) {
if (expected == null && actual == null) {
return;
}
if (expected != null && expected.equals(actual)) {
return;
}
failNotEquals(message, expected, actual);
}

两个if语句,判断了两者相等的情况,引用(地址)相等或者内容相等

如果这两种if情况都不命中,那么表明1参和2参实际是不相等,所以代码会往下执行

failNotEquals(String message, Object expected, Object actual)方法,并在此方法中抛出异常, 接下来就比较简单了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static public void failNotEquals(String message, Object expected, Object actual) {
fail(format(message, expected, actual));
}

public static String format(String message, Object expected, Object actual) {
String formatted = "";
if (message != null && message.length() > 0) {
formatted = message + " ";
}
return formatted + "expected:<" + expected + "> but was:<" + actual + ">";
}
/**
* Fails a test with the given message.
*/
static public void fail(String message) {
if (message == null) {
throw new AssertionFailedError();
}
throw new AssertionFailedError(message);
}

以上可以看出, 最终是由fail(String message)这个方法抛出异常信息

Assert.assertEquals()使用方法

1
2
3
4
5
6
Assert.assertEquals(true, arry.contains("hello"));
Assert.assertEquals(39991L, aa.getLong("key3", 0L));
Assert.assertEquals(true, bb.getBoolean("key4", false));
Assert.assertEquals(5.3f, cc.getFloat("key5", 0.f));
Assert.assertEquals(99, dd.getInt("key6", 1));
Assert.assertEquals("如果打印本信息, 证明参数不相等", 10L, 10);

按照源码分析, 我们可以把一个预期结果作为1参传递进去. 2参传递我们需要测试的方法. 然后执行. 相等, 代码继续往下执行, 不相等, 中断执行, 抛出异常信息

注意

Assert.assertSame(Object expected, Object actual)方法

查看源码,其比较的是引用地址是否相等,并没有对内容进行比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Asserts that two objects refer to the same object. If they are not
* the same an AssertionFailedError is thrown.
*/
static public void assertSame(Object expected, Object actual) {
assertSame(null, expected, actual);
}
/**
* Asserts that two objects refer to the same object. If they are not
* an AssertionFailedError is thrown with the given message.
*/
static public void assertSame(String message, Object expected, Object actual) {
if (expected == actual) {
return;
}
failNotSame(message, expected, actual);
}

测试类

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
/**
* 测试逆向工程代码和PageHelper分页插件案例
*
*
*/
public class Test3 {
private SqlSessionFactory sqlSessionFactory;

@Before
public void init() throws Exception {
// 加载全局配置文件(同时把映射文件也加载了)
String resource = "phase03/SqlMapConfig.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// sqlsessionFactory需要通过sqlsessionFactoryBuilder读取全局配置文件信息之后
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}

//没有使用分页插件进行查询所有数据
@Test
public void test() {
// 创建UserMapper对象
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);

//通过UserExample传递对象
UserExample example = new UserExample();
//其中的参数不能写null,如果没有参数可以传递,也要new一个对象
//通过逆向工程得到list
List<User> list = mapper.selectByExample(example);
System.out.println(list);
}

//使用分页插件查询所有的数据,并进行分页
@Test
public void test2() {
// 创建UserMapper对象
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);

//直接在上面代码没有修改的情况下添加如下代码就行,这时数据就会进行分页操作
//下面的集合就显示分页完成之后的数据
//编写分页代码
PageHelper.startPage(2, 1);

UserExample example = new UserExample();
// 此处返回的list实现类不再是ArrayList,而是PageHelper提供的Page对象
// Page对象也实现了list接口
List<User> list = mapper.selectByExample(example );
System.out.println(list);

//通过PageInfo得到总页数
PageInfo<User> pageInfo = new PageInfo<User>(list);
System.out.println(pageInfo.getTotal());
}

}

注意事项

  1. 需要分页的查询语句,必须是处于PageHelper.startPage(2, 1);这条语句的后面。

  2. 如果查询语句是使用resultMap进行的嵌套结果映射,则无法使用PageHelper进行分页。

课后思考

#{}与${}的区别是什么?

  1. #{}与${}取值方式不同,${}使用的是OGNL

  2. #{}是预编译的方式,${}是动态拼接字符串的方式

  3. #{}与${}穿的参数是简单类型的话,它们取值的参数名称是有区别的,也就是#{}和${}中的名称

  4. #{}存在SQL注入问题

  5. 解析顺序不同,#{}和${}谁先解析

    SELECT * FROM user WHERE username like ‘%${name}%’ and age = #{age}

    SELECT * FROM user WHERE age = #{age} and username like ‘%${name}%’

  6. #{}对于参数的处理,是需要根据类型来处理,比如字符串类型,处理之后,会加上双引号。而${}不管类型,内容都是原样输出

既然${}存在SQL注入问题,那么什么场景下使用?

where name=${name}

from ${tablename} 不能使用#{}

order by ${columnName} 思考如果使用#{}会出现什么问题

group by ${columnName}

SELECT * FROM user WHERE username = #{name} and pwd = ${pwd}

SELECT * FROM user WHERE username = #{name} and pwd =’’ OR 1=1

既然mybatis的SqlSession要求只能传入一个参数,那么以下代码如何解释?

int updateByExample(@Param("record") User record,@Param("example") UserExample example);

封装厂Map对象,@Param注解中的值为key,@Param注解的参数为value

SqlSession.update(statementId,param)

预编译语句(Prepared Statements)介绍

以MySQL为例

背景

本文重点讲述MySQL中的预编译语句并从MySQL的Connector/J源码出发讲述其在Java语言中相关使用。

注意:文中的描述与结论基于MySQL 5.7.16以及Connect/J 5.1.42版本。

预编译语句是什么

通常我们的一条sql在db接收到最终执行完毕返回可以分为下面三个过程

  1. 词法和语义解析
  2. 优化sql语句,制定执行计划
  3. 执行并返回结果

我们把这种普通语句称作Immediate Statements。

但是很多情况,我们的一条sql语句可能会反复执行,或者每次执行的时候只有个别的值不同(比如query的where子句值不同,update的set子句值不同,insert的values值不同)。

如果每次都需要经过上面的词法语义解析、语句优化、制定执行计划等,则效率就明显不行了。

所谓预编译语句就是将这类语句中的值用占位符替代,可以视为将sql语句模板化或者说参数化,一般称这类语句叫Prepared Statements或者Parameterized Statements

预编译语句的优势在于归纳为:一次编译、多次运行,省去了解析优化等过程;此外预编译语句能防止sql注入。

当然就优化来说,很多时候最优的执行计划不是光靠知道sql语句的模板就能决定了,往往就是需要通过具体值来预估出成本代价。

MySQL的预编译功能

注意MySQL的老版本(4.1之前)是不支持服务端预编译的,但基于目前业界生产环境普遍情况,基本可以认为MySQL支持服务端预编译。

下面我们来看一下MySQL中预编译语句的使用。

首先我们有一张测试表t,结构如下所示:

1
2
3
4
5
6
7
8
9
mysql> show create table t\G
*************************** 1. row ***************************
      Table: t
Create Table: CREATE TABLE `t` (
 `a` int(11) DEFAULT NULL,
 `b` varchar(20) DEFAULT NULL,
 UNIQUE KEY `ab` (`a`,`b`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)

编译

我们接下来通过PREPARE stmt_name FROM preparable_stm 的语法来预编译一条sql语句

1
2
3
mysql> prepare ins from 'insert into t select ?,?';
Query OK, 0 rows affected (0.00 sec)
Statement prepared

执行

我们通过EXECUTE stmt_name [USING @var_name [, @var_name] …] 的语法来执行预编译语句

1
2
3
4
5
6
7
8
9
10
11
12
mysql> set @a=999,@b='hello';
Query OK, 0 rows affected (0.00 sec)
mysql> execute ins using @a,@b;
Query OK, 1 row affected (0.01 sec)
Records: 1 Duplicates: 0  Warnings: 0
mysql> select * from t;
+------+-------+
| a   | b     |
+------+-------+
|  999 | hello |
+------+-------+
1 row in set (0.00 sec)

可以看到,数据已经被成功插入表中。

MySQL中的预编译语句作用域是session级,但我们可以通过max_prepared_stmt_count变量来控制全局最大的存储的预编译语句。

1
2
3
4
5
mysql> set @@global.max_prepared_stmt_count=1;
Query OK, 0 rows affected (0.00 sec)
mysql> prepare sel from 'select * from t';
ERROR 1461 (42000): Can't create more than max_prepared_stmt_count statements
(current value: 1)

当预编译条数已经达到阈值时可以看到MySQL会报如上所示的错误。

释放

如果我们想要释放一条预编译语句,则可以使用{DEALLOCATE | DROP} PREPARE stmt_name 的语法进行操作

1
2
mysql> deallocate prepare ins;
Query OK, 0 rows affected (0.00 sec)

通过MySQL驱动进行预编译

以上介绍了直接在MySQL上通过sql命令进行预编译/缓存sql语句。接下来我们以MySQL Java驱动Connector/J(版本5.1.42)为例来介绍通过MySQL驱动进行预编译。

客户端预编译

首先,简要提一下JDBC中java.sql.PreparedStatement是java.sql.Statement的子接口,它主要提供了无参数执行方法如executeQuery和executeUpdate等,以及大量形如set{Type}(int, {Type})形式的方法用于设置参数。

image-20200507194120845

在Connector/J中,java.sql.connection的底层实现类为com.mysql.jdbc.JDBC4Connection,它的类层
次结构如下图所示

image-20200507194142316

下面是我编写如下测试类,程序中做的事情很简单,就是往test.t表中插入一条记录。

test.t表的结构在上述服务端预编译语句中已经有展示,此处不再赘述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
/**
* Test for PreparedStatement.
*
* @author Robin Wang
*/
public class PreparedStatementTest {
   public static void main(String[] args) throws Throwable {
       Class.forName("com.mysql.jdbc.Driver");
       String url = "jdbc:mysql://localhost/test";
       try (Connection con = DriverManager.getConnection(url, "root",
null)) {
           String sql = "insert into t select ?,?";
           PreparedStatement statement = con.prepareStatement(sql);
           statement.setInt(1, 123456);
           statement.setString(2, "abc");
           statement.executeUpdate();
statement.close();
      }
  }
}

执行main方法后,通过MySQL通用日志查看到相关log:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2017-07-04T16:39:17.608548Z        19 Connect   root@localhost on test using
SSL/TLS
2017-07-04T16:39:17.614299Z        19 Query     /* mysql-connector-java5.1.42
( Revision: 1f61b0b0270d9844b006572ba4e77f19c0f230d4 ) */SELECT
 @@session.auto_increment_increment AS auto_increment_increment,
@@character_set_client AS character_set_client, @@character_set_connection AS
character_set_connection, @@character_set_results AS character_set_results,
@@character_set_server AS character_set_server, @@init_connect AS
init_connect, @@interactive_timeout AS interactive_timeout, @@license AS
license, @@lower_case_table_names AS lower_case_table_names,
@@max_allowed_packet AS max_allowed_packet, @@net_buffer_length AS
net_buffer_length, @@net_write_timeout AS net_write_timeout,
@@query_cache_size AS query_cache_size, @@query_cache_type AS
query_cache_type, @@sql_mode AS sql_mode, @@system_time_zone AS
system_time_zone, @@time_zone AS time_zone, @@tx_isolation AS tx_isolation,
@@wait_timeout AS wait_timeout
2017-07-04T16:39:17.642476Z        19 Query     SET character_set_results =
NULL
2017-07-04T16:39:17.643212Z        19 Query     SET autocommit=1
2017-07-04T16:39:17.692708Z        19 Query     insert into t select
123456,'abc'
2017-07-04T16:39:17.724803Z        19 Quit

从MySQL驱动源码中我们可以看到程序中对prepareStatement方法的调用最终会走到如下所示的代码段中:

image-20200507194351361

上图截自 com.mysql.jdbc.ConnectionImpl#prepareStatement(java.lang.String, int, int)

这里有两个很重要的参数useServerPrepStmts以及emulateUnsupportedPstmts用于控制是否使用服务端预编译语句。

由于上述程序中我们没有启用服务端预编译,因此MySQL驱动在上面的prepareStatement方法中会进入使用客户端本地预编译的分支进入如下所示的clientPrepareStatement方法。

image-20200507194450006

上图截自 com.mysql.jdbc.ConnectionImpl#clientPrepareStatement(java.lang.String, int, int, boolean)

而我们上面的程序中也没有通过cachePrepStmts参数启用缓存,因此会通过
com.mysql.jdbc.JDBC42PreparedStatement的三参构造方法初始化出一个PreparedStatement对象。

image-20200507194511933

上图截自 com.mysql.jdbc.PreparedStatement#getInstance(com.mysql.jdbc.MySQLConnection,
java.lang.String, java.lang.String)

com.mysql.jdbc.JDBC42PreparedStatement的类继承关系图如下所示:

image-20200507194544361

以上介绍的是默认不开启服务预编译及缓存的情况。

通过服务端预编译的情况

接下来,将上述程序中的连接串改为jdbc:mysql://localhost/test?useServerPrepStmts=true,其余部分不作变化,清理表数据,重新执行上述程序,我们会在MySQL日志中看到如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2017-07-04T16:42:23.228297Z        22 Connect   root@localhost on test using
SSL/TLS
2017-07-04T16:42:23.233854Z        22 Query     /* mysql-connector-java5.1.42
( Revision: 1f61b0b0270d9844b006572ba4e77f19c0f230d4 ) */SELECT
 @@session.auto_increment_increment AS auto_increment_increment,
@@character_set_client AS character_set_client, @@character_set_connection AS
character_set_connection, @@character_set_results AS character_set_results,
@@character_set_server AS character_set_server, @@init_connect AS
init_connect, @@interactive_timeout AS interactive_timeout, @@license AS
license, @@lower_case_table_names AS lower_case_table_names,
@@max_allowed_packet AS max_allowed_packet, @@net_buffer_length AS
net_buffer_length, @@net_write_timeout AS net_write_timeout,
@@query_cache_size AS query_cache_size, @@query_cache_type AS
query_cache_type, @@sql_mode AS sql_mode, @@system_time_zone AS
system_time_zone, @@time_zone AS time_zone, @@tx_isolation AS tx_isolation,
@@wait_timeout AS wait_timeout
2017-07-04T16:42:23.261345Z        22 Query     SET character_set_results =
NULL
2017-07-04T16:42:23.262127Z        22 Query     SET autocommit=1
2017-07-04T16:42:23.286449Z        22 Prepare   insert into t select ?,?
2017-07-04T16:42:23.288361Z        22 Execute   insert into t select
123456,'abc'
2017-07-04T16:42:23.301597Z        22 Close stmt        
2017-07-04T16:42:23.302188Z        22 Quit

从上面的日志中,我们可以很清楚地看到Prepare, Execute, Close几个command,显然MySQL服务器为我们预编译了语句。

我们仅仅通过useServerPrepStmts开启了服务端预编译,由于未开启缓存,因此prepareStatement方法会向MySQL服务器请求对语句进行预编译。

image-20200507194736589

上图截自 com.mysql.jdbc.ConnectionImpl#prepareStatement(java.lang.String, int, int)

如果我们对代码稍作调整,在其中再向表中做对同一个sql模板语句进行prepare->set->execute->close操作,可以看到如下所示的日志,由于没有缓存后面即使对同一个模板的sql进行预编译,仍然会向MySQL服务器请求编译、执行、释放。

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
2017-07-05T16:04:45.801650Z    76 Connect   root@localhost on test using
SSL/TLS
2017-07-05T16:04:45.807448Z    76 Query /* mysql-connector-java-5.1.42 (
Revision: 1f61b0b0270d9844b006572ba4e77f19c0f230d4 ) */SELECT
 @@session.auto_increment_increment AS auto_increment_increment,
@@character_set_client AS character_set_client, @@character_set_connection
AS character_set_connection, @@character_set_results AS
character_set_results, @@character_set_server AS character_set_server,
@@init_connect AS init_connect, @@interactive_timeout AS
interactive_timeout, @@license AS license, @@lower_case_table_names AS
lower_case_table_names, @@max_allowed_packet AS max_allowed_packet,
@@net_buffer_length AS net_buffer_length, @@net_write_timeout AS
net_write_timeout, @@query_cache_size AS query_cache_size,
@@query_cache_type AS query_cache_type, @@sql_mode AS sql_mode,
@@system_time_zone AS system_time_zone, @@time_zone AS time_zone,
@@tx_isolation AS tx_isolation, @@wait_timeout AS wait_timeout
2017-07-05T16:04:45.834672Z    76 Query SET character_set_results = NULL
2017-07-05T16:04:45.835183Z    76 Query SET autocommit=1
2017-07-05T16:04:45.868532Z    76 Prepare   insert into t select ?,?
2017-07-05T16:04:45.869961Z    76 Execute   insert into t select
1234546,'ab33c'
2017-07-05T16:04:45.891609Z    76 Close stmt
2017-07-05T16:04:45.892015Z    76 Prepare   insert into t select ?,?
2017-07-05T16:04:45.892454Z    76 Execute   insert into t select
6541321,'de22f'
2017-07-05T16:04:45.904014Z    76 Close stmt
2017-07-05T16:04:45.904312Z    76 Quit

使用缓存的情况

在类似MyBatis等ORM框架中,往往会大量用到预编译语句。例如MyBatis中语句的statementType默认为PREPARED,因此通常语句查询时都会委托connection调用prepareStatement来获取一个
java.sql.PreparedStatement对象。

image-20200507194853194

上图截自 org.apache.ibatis.executor.statement.PreparedStatementHandler#instantiateStatement

如果不进行缓存,则MySQL服务端预编译也好,本地预编译也好,都会对同一种语句重复预编译。因此为了提升效率,往往我们需要启用缓存,通过设置连接中cachePrepStmts参数就可以控制是否启用缓存。此外通过prepStmtCacheSize参数可以控制缓存的条数,MySQL驱动默认是25,通常实践中都在250-500左右;通过prepStmtCacheSqlLimit可以控制长度多大的sql可以被缓存,MySQL驱动默认是256,通常实践中往往设置为2048这样。

服务端预编译+缓存

接下来,将测试程序中的连接url串改为jdbc:mysql://localhost/test?useServerPrepStmts=true&cachePrepStmts=true,并尝试向表中插入两条语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class PreparedStatementTest {
   public static void main(String[] args) throws Throwable {
       Class.forName("com.mysql.jdbc.Driver");
       String url = "jdbc:mysql://localhost/test?
useServerPrepStmts=true&cachePrepStmts=true";
       try (Connection con = DriverManager.getConnection(url, "root",
null)) {
           insert(con, 123, "abc");
           insert(con, 321, "def");
      }
  }
   private static void insert(Connection con, int arg1, String arg2)
throws SQLException {
       String sql = "insert into t select ?,?";
       try (PreparedStatement statement = con.prepareStatement(sql)) {
           statement.setInt(1, arg1);
           statement.setString(2, arg2);
           statement.executeUpdate();
      }
  }
}

观察到此时的MySQL日志如下所示,可以看到由于启用了缓存,在MySQL服务端只会预编译一次,之后每次由驱动从本地缓存中读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2017-07-05T14:11:08.967038Z        45 Query     /* mysql-connector-java5.1.42
( Revision: 1f61b0b0270d9844b006572ba4e77f19c0f230d4 ) */SELECT
 @@session.auto_increment_increment AS auto_increment_increment,
@@character_set_client AS character_set_client, @@character_set_connection AS
character_set_connection, @@character_set_results AS character_set_results,
@@character_set_server AS character_set_server, @@init_connect AS
init_connect, @@interactive_timeout AS interactive_timeout, @@license AS
license, @@lower_case_table_names AS lower_case_table_names,
@@max_allowed_packet AS max_allowed_packet, @@net_buffer_length AS
net_buffer_length, @@net_write_timeout AS net_write_timeout,
@@query_cache_size AS query_cache_size, @@query_cache_type AS
query_cache_type, @@sql_mode AS sql_mode, @@system_time_zone AS
system_time_zone, @@time_zone AS time_zone, @@tx_isolation AS tx_isolation,
@@wait_timeout AS wait_timeout
2017-07-05T14:11:09.014069Z        45 Query     SET character_set_results =
NULL
2017-07-05T14:11:09.016009Z        45 Query     SET autocommit=1
2017-07-05T14:11:09.060693Z        45 Prepare   insert into t select ?,?
2017-07-05T14:11:09.061870Z        45 Execute   insert into t select
123,'abc'
2017-07-05T14:11:09.086018Z        45 Execute   insert into t select
321,'def'
2017-07-05T14:11:09.107963Z        45 Quit

MySQL驱动里对于server预编译的情况维护了两个基于LinkedHashMap使用LRU策略的cache,分别是serverSideStatementCheckCache用于缓存sql语句是否可以由服务端来缓存以及serverSideStatementCache用于缓存服务端预编译sql语句,这两个缓存的大小由prepStmtCacheSize参数控制。

接下来,我们来看一下MySQL驱动是如何通过这样的缓存来实现预编译结果复用的。

image-20200507195311706

上图截自 com.mysql.jdbc.ConnectionImpl#prepareStatement(java.lang.String, int, int)

如上图所示,在启用服务端缓存的情况下,MySQL驱动会尝试从LRU缓存中读取预编译sql,如果命中缓存的话,则会置Statement对象的close状态为false,复用此对象;

而如果未命中缓存的话,则会根据sql长度是否小于prepStmtCacheSqlLimit参数的值来为设置是否需要缓存,可以理解为是打个缓存标记,并延迟到语句close时进行缓存。

而在Statement对象执行close方法时,MySQL驱动中的ServerPreparedStatement会根据isCached标记、是否可池化、是否已经关闭等来判断是否要把预编译语句放到缓存中以复用。

image-20200507195357854

上图截自 com.mysql.jdbc.ServerPreparedStatement#close

在连接初始化时,如果启用了useServerPrepStmts,则serverSideStatementCheckCache和serverSideStatementCache这两个LRU缓存也将随之初始化。

image-20200507195423718

上图截自 com.mysql.jdbc.ConnectionImpl#createPreparedStatementCaches

其中serverSideStatementCache对于被待移除元素有更进一步的处理:对于被缓存淘汰的预编译语句,给它缓存标记置为false,并且调用其close方法。

客户端预编译+缓存

接下来看看客户端本地预编译并且使用缓存的情况。

MySQL驱动源码中使用cachedPreparedStatementParams来缓存sql语句的ParseInfo,ParseInfo是com.mysql.jdbc.PreparedStatement的一个内部类,用于存储预编译语句的一些结构和状态基本信息。cachedPreparedStatementParams的类型是com.mysql.jdbc.CacheAdapter,这是MySQL驱动源码中的一个缓存适配器接口,在连接初始化的时候会通过parseInfoCacheFactory来初始化一个作用域为sql连接的缓存类(com.mysql.jdbc.PerConnectionLRUFactory)出来,其实就是对LRUCache和sql连接的一个封装组合。

image-20200507202100146

上图截自 com.mysql.jdbc.ConnectionImpl#clientPrepareStatement(java.lang.String, int, int, boolean)

在缓存未命中的情况下,驱动会本地prepare出来一个预编译语句,并且将parseInfo放入缓存中;而缓存命中的话,则会把缓存中的parseInfo带到四参构造方法中构造初始化。

性能测试

这里可以做一个简易的性能测试。

首先写个存储过程向表中初始化大约50万条数据,然后使用同一个连接做select查询(查询条件走索引)。

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
CREATE PROCEDURE init(cnt INT)
 BEGIN
   DECLARE i INT DEFAULT 1;
   TRUNCATE t;
   INSERT INTO t SELECT 1, 'stmt 1';
   WHILE i <= cnt DO
     BEGIN
       INSERT INTO t SELECT a+i, concat('stmt ',a+i) FROM t;
       SET i = i << 1;
     END;
   END WHILE;
 END;
mysql> call init(1<<18);
Query OK, 262144 rows affected (3.60 sec)
mysql> select count(0) from t;
+----------+
| count(0) |
+----------+
|   524288 |
+----------+
1 row in set (0.14 sec)
public static void main(String[] args) throws Throwable {
  Class.forName("com.mysql.jdbc.Driver");
  String url = "";
   long start = System.currentTimeMillis();
  try (Connection con = DriverManager.getConnection(url, "root", null)) {
       for (int i = 1; i <= (1<<19); i++) {
           query(con, i, "stmt " + i);
      }
  }
   long end = System.currentTimeMillis();
   System.out.println(end - start);
}
private static void query(Connection con, int arg1, String arg2) throws
SQLException {
  String sql = "select a,b from t where a=? and b=?";
  try (PreparedStatement statement = con.prepareStatement(sql)) {
      statement.setInt(1, arg1);
      statement.setString(2, arg2);
      statement.executeQuery();
  }
}

以下几种情况,经过3测试取平均值,情况如下

本地预编译:65769 ms

本地预编译+缓存:63637 ms

服务端预编译:100985 ms

服务端预编译+缓存:57299 ms

从中我们可以看出本地预编译加不加缓存其实差别不是太大,服务端预编译不加缓存性能明显会降低很多,但是服务端预编译加缓存的话性能还是会比本地好很多。

主要原因是服务端预编译不加缓存的话本身prepare也是有开销的,另外多了大量的round-trip。

总结

本文重点介绍了预编译语句的概念及其在MySQL中的使用,并以介绍了预编译语句在MySQL驱动源码中的一些实现细节。

在实际生产环境中,如MyBatis等ORM框架大量使用了预编译语句,最终底层调用都会走到MySQL驱动里,从驱动中了解相关实现细节有助于更好地理解预编译语句。

一些网上的文章称必须使用useServerPrepStmts才能开启预编译,这种说法是错误的。实际上JDBC规范里没有说过预编译语句这件事情由本地来做还是服务端来做。MySQL早期版本中由于不支持服务端预编译,因此当时主要是通过本地预编译。

经过实际测试,对于频繁使用的语句,使用服务端预编译+缓存效率还是能够得到可观的提升的。但是对于不频繁使用的语句,服务端预编译本身会增加额外的round-trip,因此在实际开发中可以视情况定夺使用本地预编译还是服务端预编译以及哪些sql语句不需要开启预编译等。

参考

MySQL官方手册预编译语句
mysql-5-prepared-statement-syntax
MySQL Connector/J源码

显式替换

${}的位置主要用在where、from后面或是order by(开发中经常会用到动态根据字段排序),这时候会显式的使用变量

1
2
3
select * from user where name = ${name}; --where
select * from user where name='auth' order by ${column}; --order by
select * from ${tableName} where name=#{name}; --from

这类的${} 会进行显式处理,即传入什么直接替换,例如

1
${column}=id和${column}='id‘

则会产生不同的效果。而此类语句有可能会产生sql的注入,经典的例子

1
select * from ${tableName} where name = #{name};

传入变量tableName =

1
" user;delete user;-- "

动态解析后的sql

1
select * from user;delete user;-- where name = ?;

会存在注入。如果

1
select * from #{tableName} where name = #{name};

传入tableName =

1
"user"

则产生无法编译的sql 错误

文章目录
  1. 1. Mybatis基础详解
    1. 1.1. 介绍篇
      1. 1.1.1. 认识自己
      2. 1.1.2. 认识框架
        1. 1.1.2.1. 什么是框架
        2. 1.1.2.2. 为什么使用框架
        3. 1.1.2.3. 软件开发的三层结构
        4. 1.1.2.4. 认识设计模式
        5. 1.1.2.5. 认识MyBatis
        6. 1.1.2.6. Mybatis是什么
        7. 1.1.2.7. Mybatis的由来
        8. 1.1.2.8. ORM是什么
        9. 1.1.2.9. ORM框架和MyBatis的区别
    2. 1.2. 入门篇
      1. 1.2.1. 编码流程
      2. 1.2.2. 项目搭建
      3. 1.2.3. 需求实现
    3. 1.3. 基础应用篇
      1. 1.3.1. mapper代理开发方式
        1. 1.3.1.1. 代理理解
        2. 1.3.1.2. XML方式
        3. 1.3.1.3. 注解方式
      2. 1.3.2. 全局配置文件
        1. 1.3.2.1. 配置内容
        2. 1.3.2.2. properties标签
        3. 1.3.2.3. typeAlias标签
        4. 1.3.2.4. mappers标签
      3. 1.3.3. 输入映射和输出映射
        1. 1.3.3.1. parameterType(输入类型)
        2. 1.3.3.2. 传递简单类型
        3. 1.3.3.3. #{}和${}区别
        4. 1.3.3.4. 传递POJO对象
        5. 1.3.3.5. 传递pojo包装对象
          1. 1.3.3.5.1. OGNL
        6. 1.3.3.6. resultType(输出类型)
          1. 1.3.3.6.1. 映射简单类型
          2. 1.3.3.6.2. 映射pojo对象
        7. 1.3.3.7. resultMap
    4. 1.4. 高级应用篇
      1. 1.4.1. 关联查询
        1. 1.4.1.1. 商品订单数据类型
      2. 1.4.2. 一对一查询
        1. 1.4.2.1. 方法一:resultType
        2. 1.4.2.2. 方法二:resultMap
      3. 1.4.3. 一对多查询
      4. 1.4.4. 延迟加载
      5. 1.4.5. Mybatis缓存
        1. 1.4.5.1. 缓存介绍
        2. 1.4.5.2. 一级缓存
        3. 1.4.5.3. 二级缓存
          1. 1.4.5.3.1. 刷新二级缓存
          2. 1.4.5.3.2. 应用场景
      6. 1.4.6. 动态SQL
    5. 1.5. Mybatis逆向工程
      1. 1.5.1. 逆向工程介绍
      2. 1.5.2. 实现
      3. 1.5.3. 注意事项
    6. 1.6. PageHelper分页插件
      1. 1.6.1. PageHelper分页插件介绍
      2. 1.6.2. 使用方法
        1. 1.6.2.1. 添加依赖
        2. 1.6.2.2. 配置PageHelper
        3. 1.6.2.3. 项目中使用PageHelper
        4. 1.6.2.4. 测试类
      3. 1.6.3. 课后思考
    7. 1.7. 预编译语句(Prepared Statements)介绍
      1. 1.7.1. 背景
      2. 1.7.2. 预编译语句是什么
      3. 1.7.3. MySQL的预编译功能
      4. 1.7.4. 通过MySQL驱动进行预编译
        1. 1.7.4.1. 客户端预编译
        2. 1.7.4.2. 通过服务端预编译的情况
        3. 1.7.4.3. 使用缓存的情况
      5. 1.7.5. 性能测试
      6. 1.7.6. 总结
      7. 1.7.7. 参考
      8. 1.7.8. 显式替换
|