水滴石穿-深入理解MyBatis

本文最后更新于:May 13, 2023 pm

积土成山,风雨兴焉;积水成渊,蛟龙生焉;积善成德,而神明自得,圣心备焉。故不积跬步,无以至千里,不积小流无以成江海。齐骥一跃,不能十步,驽马十驾,功不在舍。面对悬崖峭壁,一百年也看不出一条裂缝来,但用斧凿,能进一寸进一寸,能进一尺进一尺,不断积累,飞跃必来,突破随之。

目录

优缺点

优点

  • 灵活度高。可以自定义编写SQL。
  • 内置缓存机制。通过缓存机制,可以大大减少与数据库的交互次数,提高性能。
  • 简化代码。与传统JDBC代码相比,只需要接口方法的声明和mapper.xml配置。
  • 良好的兼容性。几乎支持市场上所有主流的数据库。
  • 支持与Spring整合。

缺点

  • 需要开发者具有良好的SQL功底。
  • 移植性差。SQL依赖数据库,不同的数据库有不同的SQL语句。

ORM

ORM(Object Relational Mapping),即对象关系映射。是一种程序设计技术。主要思想是通过指定对象和关系型数据库之间的映射关系,可以使程序开发时可以使用面向对象思想操作关系型数据库。

MyBatis中是否必须有接口

MyBatis中并不是必须要求有接口。MyBatis提供了三种引入的方式:

只编写mapper.xml文件、编写接口和mapper.xml文件、编写接口并使用注解。对应的在主配置文件中相应的配置为:

1
2
3
4
5
<mappers>
<mapper resource="com/tothefor/dao/StudentDao.xml"/> <!-- 1 -->
<package name="com.tothefor.dao"/> <!-- 2 -->
<mapper class="com.tothefor.MyStudentDao" /> <!-- 3 -->
</mappers>

只编写mapper文件

不需要写接口实现。

StudentDao.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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="NoEntity">

<select id="DouQueryById" parameterType="int" resultType="com.tothefor.entity.Student">
select * from Student where id = #{id}
</select>


</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
<?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="mysql.properties" />
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${mysql.driver}"/>
<property name="url" value="${mysql.url}"/>
<property name="username" value="${mysql.username}"/>
<property name="password" value="${mysql.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/tothefor/dao/StudentDao.xml"/>
</mappers>
</configuration>

测试

见下一个示例。但需要注意修改namespace。

编写接口和Mapper文件

接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.tothefor.dao;

import com.tothefor.entity.Student;

/**
* @Author DragonOne
* @Date 2021/12/26 23:41
* @墨水记忆 www.tothefor.com
*/
public interface StudentDao {
public Student queryById(String id);
}

对应mapper文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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.tothefor.dao.StudentDao">

<select id="DouQueryById" parameterType="int" resultType="com.tothefor.entity.Student">
select * from Student where id = #{id}
</select>


</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
<?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="mysql.properties" />
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${mysql.driver}"/>
<property name="url" value="${mysql.url}"/>
<property name="username" value="${mysql.username}"/>
<property name="password" value="${mysql.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<package name="com.tothefor.dao"/>
</mappers>
</configuration>

测试

1
2
3
4
5
6
7
8
9
@Test
public void testDouQueryById() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("myBatis.xml");
SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession session = build.openSession();
Student student = session.selectOne("com.tothefor.dao.StudentDao.DouQueryById", 2);
System.out.println(student);
session.close();
}

接口和注解

接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.tothefor;

import com.tothefor.entity.Student;
import org.apache.ibatis.annotations.Select;

import java.util.List;

/**
* @Author DragonOne
* @Date 2022/6/5 15:21
* @墨水记忆 www.tothefor.com
*/
public interface MyStudentDao {
@Select("select * from Student")
public List<Student> all();
}

主配置文件:

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
<?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="mysql.properties" />
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${mysql.driver}"/>
<property name="url" value="${mysql.url}"/>
<property name="username" value="${mysql.username}"/>
<property name="password" value="${mysql.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper class="com.tothefor.MyStudentDao" />
</mappers>
</configuration>

测试

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testNo() throws IOException {
InputStream resourceAsStream = Resources.getResourceAsStream("myBatis.xml");
SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);
SqlSession session = build.openSession();
MyStudentDao mapper = session.getMapper(MyStudentDao.class);
List<Student> all = mapper.all();
for(Student it: all){
System.out.println(it);
}
session.close();
}

延伸

为什么在项目中需要提供一个接口?

  • 主要原因就是:在其他层可以通过接口快速注入接口对象,从而方便调用方法。

MyBatis是如何实现接口和mapper文件绑定的

MyBatis是通过动态代理(JDK动态代理)来实现绑定的。当获取到接口对象的时候,会产生一个接口的动态代理对象,然后通过接口代理对象调用里面的方法时就是通过动态代理去new真实的需要使用的SQL语句。

应用步骤:

  • 在主配置文件中进行指定要扫描的包。包中提供了同名的接口文件和mapper文件。
1
2
3
<mappers>
<package name="com.tothefor.dao"/>
</mappers>
  • 在mapper文件中的namespace指定接口的全限定名称。
1
<mapper namespace="com.tothefor.dao.StudentDao"></mapper>
  • 在编写的标签的id属性时,设置为接口中方法的名称。
1
2
3
<select id="queryById" resultType="com.tothefor.entity.Student">
select * from Student where id = #{id}
</select>

MyBatis接口中是否支持方法的重载

在Java类中是支持方法的重载,但是在MyBatis中是不允许方法重载的。

原因

  • 在上一个问题中我们也知道了mapper文件中的namespace是接口的全限定名称,并且标签的id属性的值是方法的名称。而且在mapper文件中的每个标签(select、update、delete、insert)最终存储时都是以namespace+id作为key进行存储的。
  • 所以,如果接口中存在两个同名方法(重载),那么在同一个mapper文件下就会提供两个同名的id。

namespace

  • namespace属性必须存在,不能为空。如果没有配置namespace或取值为空时会抛出BuilderException。
  • 多个mapper文件中允许出现同名的namespace,但是一般都是用接口的全限定名称。且一个接口对应一个mapper文件,所以一般是不会出现重名的。

MyBatis的mapper文件中支持哪些标签

共24个标签。

最基本的:select、update、delete、insert

  • 子标签:if、choose、trim、where、set、foreach、bind(动态SQL的7个标签)、selectKey(新增时回填主键)、include(引用SQL)

结果集映射:resultMap

  • 子标签:id、result、association、collection、constructor、discriminator。

参数映射:parameterMap

二级缓存配置:cache

二级缓存引用:cache-ref

SQL片段:sql

mapper文件获取方法参数

详见《MyBatis学习-(三)参数 》

结果映射的方式

分为三种方式:自动映射、别名映射、手动映射。

自动映射

这种要求实体类属性和数据库字段名称相同。

1
2
3
<select id="selectAll" resultType="Stu">
select id,name,age,phone from stu;
</select>

id,name,age,phone为实体类Stu的属性名称。

别名映射

通过设置别名,本质和自动映射类似。

1
2
3
<select id="selectAll" resultType="Stu">
select t_id id,t_name name from stu;
</select>

其中,t_id、t_name为数据库字段名称;id、name为实体类属性名称。

手动映射

通过resultMap实现映射。

1
2
3
4
5
6
7
<resultMap id="stuMap" type="Stu"> <!--Stu为实体类名称-->
<id column="t_id" property="id" /> <!--t_id为数据库字段,id为实体类属性名称。下同-->
<result column="t_name" property="name" />
</resultMap>
<select id="selectAll" resultType="stuMap"> <!--stuMap为resultMap的id-->
select t_id,t_name from stu;
</select>

MyBatis实现关联对象查询

MyBatis支持两种方式实现关联对象查询:N+1查询、多表联合查询。

  • N+1查询支持延迟加载,多表查询不支持。
  • 如果关联的是一个对象,使用resultMap中的association进行查询。
  • 如果关联的是一个集合,使用resultMap中的Collection进行查询。

MyBatis实现延迟加载

MyBatis中的延迟加载只能出现在N+1查询方式,默认是没有开启延迟加载的。有两种配置方式:在主配置文件中配置两个属性,从而开启延迟加载;在association或者Collection中的fetchType控制,lazy表示延迟加载,eager表示立即加载。当fetchType和主配置文件同时配置时,fetchType生效。

主配置文件:

1
2
3
4
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>

MyBatis的缓存机制

MyBatis提供了两种缓存,一级缓存和二级缓存。通过缓存可以减少对数据库的访问,提高程序执行性能。

  • 一级缓存又称为SqlSession缓存,默认开启。有效范围是同一个SqlSession对象,每次缓存同一个方法中相同的SQL语句。在缓存时把SQL当成Key,结果当成Value进行缓存。关闭后释放缓存中的内容。
  • 二级缓存又称为SqlSessionFactory缓存,有效范围同一个SqlSessionFactory对象,默认关闭。可以在对应映射文件中添加cache标签启用二级缓存。
  • 当SqlSession提交或关闭时,才把一级缓存的内容放到二级缓存中。因为项目中SqlSessionFactory是不关闭的,所以二级缓存内容默认一直存在。二级缓存中放经常被查询、但很少被修改的内容。还需要注意的是,对象需要序列化(实现Serializable接口)。
  • 每次查询时先判断二级缓存,如果没有,再判断一级缓存,如果也没有,则访问数据库。

MyBatis中执行器的类型

MyBatis的执行器都实现了Executor接口,作用是SQL执行的流程。共分为三个类型:SimpleExecutorReuseExecutorBatchExecutor

  • SimpleExecutor是默认的执行器类型,每次执行SQL都需要预编译、设置参数、执行。
  • ReuseExecutor只预编译一次,复用Statement对象,然后设置参数,并执行。
  • BatchExecutor先预编译,批量执行时设置参数,最后统一提交给数据库执行。
  • 可以通过factory.openSession()方法参数设置执行器类型。通过枚举类型ExecutorType进行设置。

MyBatis的四大核心接口

四大核心接口:Executor(执行器)、ParameterHandler(参数处理器)、StatementHandler、ResultSetHandler(结果集处理器)。

  • Executor:负责SQL执行过程的总体控制。
  • ParameterHandler:负责SQL语句的参数设置。
  • StatementHandler:负责与JDBC代码的交互,包含prepare预处理、query查询、update增删改操作。
  • ResultSetHandler:负责把查询结果映射为Java对象。

MyBatis的执行流程

MyBatis使用的设计模式

代理模式(Mapper)、适配器模式(log)、工厂模式(SqlSession)、装饰器模式(CachingExecutor)、建造者模式(SqlSessionFactoryBuilder)、策略模式(openSession可以控制ExecutorType)、模板模式(BaseExecutor)、责任链模式(Interceptor)。