MyBatis

4/10/2022 MyBatis

Mybatis1

# 简介

MyBatis 是一个开源、轻量级的数据持久化框架,是 JDBC 和 Hibernate 的替代方案。MyBatis 内部封装了 JDBC,简化了加载驱动、创建连接、创建 statement 等繁杂的过程,开发者只需要关注 SQL 语句本身。

Mybatis图标

数据持久化是将内存中的数据模型转换为存储模型,以及将存储模型转换为内存中数据模型的统称。例如,文件的存储、数据的读取以及对数据表的增删改查等都是数据持久化操作。

MyBatis 支持定制化 SQL、存储过程以及高级映射,可以在实体类和 SQL 语句之间建立映射关系,是一种半自动化的 ORM 实现。其封装性低于 Hibernate,但性能优秀、小巧、简单易学、应用广泛。

ORM(Object Relational Mapping,对象关系映射)是一种数据持久化技术,它在对象模型和关系型数据库之间建立起对应关系,并且提供了一种机制,通过 JavaBean 对象去操作数据库表中的数据。

MyBatis 前身为 IBatis,2002 年由 Clinton Begin 发布。2010 年从 Apache 迁移到 Google,并改名为 MyBatis,2013 年又迁移到了 Github。

MyBatis 的主要思想是将程序中的大量 SQL 语句剥离出来,使用 XML 文件或注解的方式实现 SQL 的灵活配置,将 SQL 语句与程序代码分离,在不修改程序代码的情况下,直接在配置文件中修改 SQL 语句。

MyBatis 与其它持久性框架最大的不同是,MyBatis 强调使用 SQL,而其它框架(例如 Hibernate)通常使用自定义查询语言,即 HQL(Hibernate查询语言)或 EJB QL(Enterprise JavaBeans查询语言)。

MyBatis 官方文档:https://mybatis.org/mybatis-3/zh/

# 优点

  • MyBatis 是免费且开源的。
  • 与 JDBC 相比,减少了 50% 以上的代码量。
  • MyBatis 是最简单的持久化框架,小巧并且简单易学。
  • 提供 XML 标签,支持编写动态 SQL 语句。
  • 提供映射标签,支持对象与数据库的 ORM 字段关系映射。
  • 支持存储过程。MyBatis 以存储过程的形式封装 SQL,可以将业务逻辑保留在数据库之外,增强应用程序的可移植性、更易于部署和测试。

# 缺点

  • 编写 SQL 语句工作量较大,对开发人员编写 SQL 语句的功底有一定要求。
  • SQL 语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。

# 拓展

Mybatis-Plus(简称 MP)是 Mybatis 的增强工具,在 Mybatis 的基础上只做增强不做改变,支持 Mybatis 所有原生的特性,为简化开发、提高效率而生。可以参考 MyBatis-Plus 官网 (opens new window)

# Hello MyBatis

# 安装MySQL

参考传送门 (opens new window)

在MySQL内创建数据库HelloMyBatis,根据如下SQL语句,创建website数据库表。

DROP TABLE IF EXISTS `website`;
CREATE TABLE `website` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) COLLATE utf8_unicode_ci NOT NULL,
  `url` varchar(30) COLLATE utf8_unicode_ci DEFAULT '',
  `age` tinyint(3) unsigned NOT NULL,
  `country` char(3) COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
  `createtime` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
1
2
3
4
5
6
7
8
9
10

# 创建Maven项目

参考传送门 (opens new window)

# 导入MyBatis依赖

使用MyBatis的Maven坐标,加载MyBatis依赖和MySQL驱动。

<dependencies>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.5</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.49</version>
    </dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
10
11
12

# 创建持久化对象

在 src 目录下创建一个名为top.snake8859.pojo的包,在该包中创建持久化类 WebSite。

package top.snake8859.pojo;

import java.util.Date;

public class WebSite {
    private int id;
    private String name;
    private String url;
    private int age;
    private String country;
    private Date createtime;

    public WebSite(){

    }

    public WebSite(int id, String name, String url, int age, String country, Date createtime) {
        this.id = id;
        this.name = name;
        this.url = url;
        this.age = age;
        this.country = country;
        this.createtime = createtime;
    }
	
    /*省略setter和getter方法*/
 
    @Override
    public String toString() {
        return "WebSite{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", url='" + url + '\'' +
                ", age=" + age +
                ", country='" + country + '\'' +
                ", createtime=" + createtime +
                '}';
    }
}
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

注意,在类中声明的属性与数据表 website 的字段一致。

# 创建映射文件

在 src 目录下创建 top.snake8859.mapper包,在该包下创建映射文件 WebsiteMapper.xml。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="top.snake8859.mapper.WebSiteMapper">

    <!--添加一个网站-->
    <insert id="addWebSite" parameterType="top.snake8859.pojo.WebSite">
        insert into website
        (name, url, age, country)
        value (#{name}, #{url}, #{age}, #{country})
    </insert>

    <!--查询所有网站信息-->
    <select id="selectAllWebSite" resultType="top.snake8859.pojo.WebSite">
        select * from website
    </select>

</mapper>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

上述代码中,<mapper>元素是配置文件的根元素,它包含了 namespace 属性,该属性值通常设置为“包名+SQL映射文件名”,用于指定唯一的命名空间。

子元素 <select><insert>中的信息用于执行查询、添加操作。在定义的 SQL 语句中,“#{}”表示一个占位符,相当于“?”,而“#{name}”表示该占位符待接收参数的名称为 name。

# 创建日志文件

MyBatis 默认使用 log4j 输出日志信息,如果开发者需要查看控制台输出的 SQL 语句,可以在 classpath 路径下配置其日志文件。在 mybatisDemo 的 src 目录下创建 log4j.properties 文件,其内容如下:

# Global logging configuration
log4j.rootLogger=ERROR,stdout
# MyBatis logging configuration...
log4j.logger.top.snake8859=DEBUG
# Console output...
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
1
2
3
4
5
6
7
8

在日志文件中配置了全局的日志配置、MyBatis 的日志配置和控制台输出,其中 MyBatis 的日志配置用于将 top.snake8859 包下所有类的日志记录级别设置为 DEBUG。

# 创建配置文件

MyBatis 核心配置文件主要用于配置数据库连接和 MyBatis 运行时所需的各种特性,包含了设置和影响 MyBatis 行为的属性。

在 src/main/resource 目录下创建 MyBatis 的核心配置文件 mybatis-config.xml,在该文件中配置了数据库环境和映射文件的位置,具体内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
    <settings>
        <setting name="logImpl" value="LOG4J" />
    </settings>
    <!--配置mybatis运行环境-->
    <environments default="development">
        <environment id="development">
            <!-- 使用JDBC的事务管理 -->
            <transactionManager type="JDBC"></transactionManager>
            <dataSource type="POOLED">
                <!--MySQL数据库驱动-->
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <!--连接数据库的URL-->
                <property name="url" value="jdbc:mysql://localhost:3306/hellomybatis?useSSL=false&amp;characterEncoding=utf8"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>
    <!--将mapper文件加入配置-->
    <mappers>
        <mapper resource="top/snake8859/mapper/WebSiteMapper.xml"></mapper>
    </mappers>
</configuration>
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

# 创建测试类

在 src/test/java 目录下创建 MyBatisTest 测试类。在测试类中首先使用输入流读取配置文件,然后根据配置信息构建 SqlSessionFactory 对象。

接下来通过 SqlSessionFactory 对象创建 SqlSession 对象,并使用 SqlSession 对象的方法执行数据库操作。 MyBatisTest 测试类的代码如下:

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import top.snake8859.pojo.WebSite;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

public class MyBatisTest {
    @Test
    public void Test1() throws IOException {
        //读取配置文件mybatis-config.xml
        InputStream config = Resources.getResourceAsStream("mybatis-config.xml");
        // 根据配置文件构建SqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(config);
        //通过SqlSessionFactory创建SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // SqlSession执行文件中定义的SQL,并返回映射结果
        // 添加网站
        WebSite website = new WebSite();
        website.setName("snake8859");
        website.setUrl("https://www.snake8859.top/");
        website.setAge(23);
        website.setCountry("CN");
        sqlSession.insert("top.snake8859.mapper.WebSiteMapper.addWebSite", website);

        //查询所有网站
        List<WebSite> listWeb = sqlSession.selectList("top.snake8859.mapper.WebSiteMapper.selectAllWebSite");
        for (WebSite webSite : listWeb) {
            System.out.println(webSite);
        }
        //提交事务
        sqlSession.commit();
        //关闭sqlSession
        sqlSession.close();
    }
}
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

运行结果如下。

DEBUG [main] - ==>  Preparing: insert into website (name, url, age, country) value (?, ?, ?, ?)
DEBUG [main] - ==> Parameters: Stack Overflow(String), https://www.stackoverflow.com/(String), 20(Integer), CN(String)
DEBUG [main] - <==    Updates: 1
DEBUG [main] - ==>  Preparing: select * from website
DEBUG [main] - ==> Parameters: 
DEBUG [main] - <==      Total: 7
WebSite{id=1, name='snake_8859', url='https://www.snake8859.top/', age=23, country='CN', createtime=null}
WebSite{id=2, name='snake_8859', url='https://www.snake8859.top/', age=23, country='CN', createtime=null}
WebSite{id=4, name='snake_8859', url='https://www.baidu.com/', age=18, country='CN', createtime=null}
WebSite{id=5, name='snake_8859', url='https://www.google.com/', age=22, country='CN', createtime=null}
WebSite{id=6, name='snake_8859', url='https://www.stackoverflow.com/', age=20, country='CN', createtime=null}
WebSite{id=7, name='snake_8859', url='https://www.stackoverflow.com/', age=20, country='CN', createtime=null}
WebSite{id=8, name='Stack Overflow', url='https://www.stackoverflow.com/', age=20, country='CN', createtime=null}
1
2
3
4
5
6
7
8
9
10
11
12
13

若报mybatis Could not find resource top/snake8859/mapper/WebSiteMapper.xml的错误,是由于maven打包的时候忽略WebSiteMapper.xml文件,需要添加maven静态资源导出配置,使其包含*.xml文件。

<build>
    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <includes>
                <include>**/*.properties</include>
                <include>**/*.xml</include>
            </includes>
        </resource>

        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.properties</include>
                <include>**/*.xml</include>
            </includes>
        </resource>
    </resources>
</build>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

若依然出现该问题,可能是main函数运行时打包问题,可以手动在IDEA右侧的maven里点击package进行主动打包。

# MyBatis 配置文件

MyBatis 配置文件的结构如下。

<?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 /><!-- 属性 -->
    <settings /><!-- 设置 -->
    <typeAliases /><!-- 类型命名 -->
    <typeHandlers /><!-- 类型处理器 -->
    <objectFactory /><!-- 对象工厂 -->
    <plugins /><!-- 插件 -->
    <environments><!-- 配置环境 -->
        <environment><!-- 环境变量 -->
            <transactionManager /><!-- 事务管理器 -->
            <dataSource /><!-- 数据源 -->
        </environment>
    </environments>
    <databaseIdProvider /><!-- 数据库厂商标识 -->
    <mappers /><!-- 映射器 -->
</configuration>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

mybatis-config.xml 文件中的元素节点是有一定顺序的,节点位置必须按以上位置排序,否则会编译错误。

configuration 元素是整个 XML 配置文件的根节点,其角色就相当于是 MyBatis 的总管,MyBatis 所有的配置信息都会存放在它里面。

# properties标签

properties 标签可以通过 resource 属性指定外部 properties 文件(database.properties),也可以通过 properties 子元素配置。

  1. 指定文件

    使用 properties 指定外部文件,代码如下。

    <properties resource="mybatisDemo/resources/database.properties"/>
    
    1

    database.properties 用于描述数据库连接的相关配置,例如数据库驱动、连接数据库的 url、数据库用户名、数据库密码等。

  2. properties子元素配置

    通过 properties 子元素 property 配置 username 和 password 变量,然后在 environments 节点中引用这些变量,代码如下。

    <properties>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
    </properties>
    
    1
    2
    3
    4

    在 environments 节点中引用 username 和 password 变量。

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${driver}"/>
                <property name="url" value="${url}"/>
                <property name="username" value="${username}"/>
                <property name="password" value="${password}"/>
            </dataSource>
        </environment>
    </environments>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    也可以不使用 properties 标签,直接将属性值写在 value 中。

# settings标签

settings 标签用于配置 MyBatis 的运行时行为,它能深刻的影响 MyBatis 的底层运行,一般不需要大量配置,大部分情况下使用其默认值即可。

settings 的配置项很多,但是真正用到的不会太多,我们把常用的配置项研究清楚就可以了。settings 配置项说明如下表所示(表中加粗字体的配置项为常用配置项)。

配置项 作用 配置选项 默认值
cacheEnabled 该配置影响所有映射器中配置缓存的全局开关 true|false true
lazyLoadingEnabled 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。在特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态 true|false false
aggressiveLazyLoading 当启用时,对任意延迟属性的调用会使带有延迟加载属性的对象完整加载;反之,每种属性将会按需加载 true|false 版本3.4.1 (不包含) 之前默认值为 true,之后为 false
multipleResultSetsEnabled 是否允许单一语句返回多结果集(需要兼容驱动) true|false true
useColumnLabel 使用列标签代替列名。不同的驱动会有不同的表现,具体可参考相关驱动文档或通过测试这两种不同的模式来观察所用驱动的结果 true|false true
useGeneratedKeys 允许JDBC 支持自动生成主键,需要驱动兼容。如果设置为 true,则这个设置强制使用自动生成主键,尽管一些驱动不能兼容但仍可正常工作(比如 Derby) true|false false
autoMappingBehavior 指定 MyBatis 应如何自动映射列到字段或属性。 NONE 表示取消自动映射。 PARTIAL 表示只会自动映射,没有定义嵌套结果集和映射结果集。 FULL 会自动映射任意复杂的结果集(无论是否嵌套) NONE、PARTIAL、FULL PARTIAL
autoMappingUnkno wnColumnBehavior 指定自动映射当中未知列(或未知属性类型)时的行为。 默认是不处理,只有当日志级别达到 WARN 级别或者以下,才会显示相关日志,如果处理失败会抛出 SqlSessionException 异常 NONE、WARNING、FAILING NONE
defaultExecutorType 配置默认的执行器。SIMPLE 是普通的执行器;REUSE 会重用预处理语句(prepared statements);BATCH 执行器将重用语句并执行批量更新 SIMPLE、REUSE、BATCH SIMPLE
defaultStatementTimeout 设置超时时间,它决定驱动等待数据库响应的秒数 任何正整数 Not Set (null)
defaultFetchSize 设置数据库驱动程序默认返回的条数限制,此参数可以重新设置 任何正整数 Not Set (null)
safeRowBoundsEnabled 允许在嵌套语句中使用分页(RowBounds)。如果允许,设置 false true|false false
safeResultHandlerEnabled 允许在嵌套语句中使用分页(ResultHandler)。如果允许,设置false true|false true
mapUnderscoreToCamelCase 是否开启自动驼峰命名规则映射,即从经典数据库列名 A_COLUMN 到经典 Java 属性名 aColumn 的类似映射 true|false false
localCacheScope MyBatis 利用本地缓存机制(Local Cache)防止循环引用(circular references)和加速联复嵌套査询。 默认值为 SESSION,这种情况下会缓存一个会话中执行的所有查询。若设置值为 STATEMENT,本地会话仅用在语句执行上,对相同 SqlScssion 的不同调用将不会共享数据 SESSION|STATEMENT SESSION
jdbcTypeForNull 当没有为参数提供特定的 JDBC 类型时,为空值指定 JDBC 类型。某些驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER NULL、VARCHAR、OTHER OTHER
lazyLoadTriggerMethods 指定哪个对象的方法触发一次延迟加载 equals、clone、hashCode、toString
defaultScriptingLanguage 指定动态 SQL 生成的默认语言 org.apache.ibatis .script.ing.xmltags .XMLDynamicLanguageDriver
callSettersOnNulls 指定当结果集中值为 null 时,是否调用映射对象的 setter(map 对象时为 put)方法,这对于 Map.kcySet() 依赖或 null 值初始化时是有用的。注意,基本类型(int、boolean 等)不能设置成 null true|false false
logPrefix 指定 MyBatis 增加到日志名称的前缀 任何字符串 Not set
loglmpl 指定 MyBatis 所用日志的具体实现,未指定时将自动査找 SLF4J|LOG4J|LOG4J2|JDK_LOGGING |COMMONS_LOGGING |ST DOUT_LOGGING|NO_LOGGING Not set
proxyFactory 指定 MyBatis 创建具有延迟加栽能力的对象所用到的代理工具 CGLIB|JAVASSIST JAVASSIST (MyBatis 版本为 3.3 及以上的)
vfsImpl 指定 VFS 的实现类 提供 VFS 类的全限定名,如果存在多个,可以使用逗号分隔 Not set
useActualParamName 允许用方法参数中声明的实际名称引用参数。要使用此功能,项目必须被编译为 Java 8 参数的选择。(从版本 3.4.1 开始可以使用) true|false true

下面给出一个全量的配置样例,如下所示。

<settings>
    <setting name="cacheEnabled" value="true"/>
    <setting name="lazyLoadingEnabled" value="true"/>
    <setting name="multipleResultSetsEnabled" value="true"/>
    <setting name="useColumnLabel" value="true"/>
    <setting name="useGeneratedKeys" value="false"/>
    <setting name="autoMappingBehavior" value="PARTIAL"/>
    <setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
    <setting name="defaultExecutorType" value="SIMPLE"/>
    <setting name="defaultStatementTimeout" value="25"/>
    <setting name="defaultFetchSize" value="100"/>
    <setting name="safeRowBoundsEnabled" value="false"/>
    <setting name="mapUnderscoreToCamelCase" value="false"/>
    <setting name="localCacheScope" value="SESSION"/>
    <setting name="jdbcTypeForNull" value="OTHER"/>
    <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# typeAliases标签

为了不在任何地方都指定类的全限定名,可以使用 typeAliases 标签定义一个别名。

例如,在 top.snake8859.pojo 包中有一个 Student 类,则该类的全限定名称为 top.snake8859.pojo.Student。使用 typeAliases 标签定义别名,这样就不用每次都书写类的全限定名称了,代码如下。

<typeAliases>
    <typeAlias alias = "Student" type = "top.snake8859.pojo.Studentt"/>
</typeAliases>
1
2
3

如果需要对同一个包下的多个类定义别名,则可以定义为:

<typeAliases>
    <package name="top.snake8859.pojo"/>
</typeAliases>
1
2
3

这样 MyBatis 将扫描 net.biancheng.po 包里面的类,将其第一个字母变为小写作为其别名,例如 Student 别名为 student,User 别名为 user。

# typeHandlers标签

typeHandlers 主要将获取的值合理地转化为 Java 类型。在 typeHandler 中,分为 jdbcType 和 javaType,其中 jdbcType 用于定义数据库类型,而 javaType 用于定义 Java 类型,typeHandler 的作用就是承担 jdbcType 和 javaType 之间的相互转换。

# environments标签

在 environments 标签中,可以配置 MyBatis 的多套运行环境,将 SQL 映射到多个不同的数据库上。

environment 是 environments 的子标签,用来配置 MyBatis 的一套运行环境,需指定运行环境 ID、事务管理、数据源配置等相关信息。

我们可以通过配置多个 environment 标签来连接多个数据库,需要注意的是必须指定其中一个为默认运行环境(通过default指定)。

environment 标签提供了两个子标签,即 transactionManager 和 dataSource。

# transactionManager标签

MyBatis 支持两个事务管理器,即 JDBC 和 MANAGED。

如果使用 JDBC 类型的事务管理器,则应用程序服务器负责事务管理操作,例如提交、回滚等。如果使用 MANAGED 类型的事务管理器,则应用程序服务器负责管理连接生命周期。

# dataSource标签

用于配置数据库的连接属性,例如要连接的数据库的驱动程序名称、URL、用户名和密码等。

dataSource 中的 type 属性用于指定数据源类型,有以下 3 种类型。

  1. UNPOOLED

    UNPOOLED 没有数据库连接池,效率低下。MyBatis 需要打开和关闭每个数据库操作的连接,它有点慢,通常应用于简单的应用程序。

  2. POOLED

    对于 POOLED 数据源类型,MyBatis 将维护一个数据库连接池。并且对于每个数据库的操作,MyBatis 都会使用连接池中的连接,并在操作完成后将它们返回到池中。减少了创建新连接所需的初始连接和身份验证时间。

  3. JNDI

    对于 JNDI 的数据源类型,MyBatis 将从 JNDI 数据源中获取连接。

dataSource 标签示例代码如下:

<dataSource type="POOLED">
    <!-- MySQL数据库驱动 -->
    <property name="driver" value="com.mysql.jdbc.Driver" />
    <!-- 连接数据库的URL -->
    <property name="url"
        value="jdbc:mysql://localhost:3306/test?characterEncoding=utf8" />
    <property name="username" value="root" />
    <property name="password" value="root" />
</dataSource>
1
2
3
4
5
6
7
8
9

# mappers标签

mappers 标签用于指定 MyBatis SQL 映射文件的路径,mapper 和package是 mappers 的子标签。

有三种方式能够注册mapper文件。

  1. resource属性

    mapper 中的 resource 属性用于指定 SQL 映射文件的路径(类资源路径)

    例如,SQL 映射文件的名称是 Student.xml,它位于名为 net.biancheng.mapper 的包中,那么可以这样配置:

    <mappers>
        <mapper resource="top/snkae8859/mapper/Student.xml"/>
    </mappers>
    
    1
    2
    3
  2. class属性

    mapper中的class属性用于指定接口。

    <mappers>
        <mapper resource="top.snake8859.dao.WebSiteDao"/>
    </mappers>
    
    1
    2
    3

    注意点:

    • 接口和它的Mapper配置文件必须同名!
    • 接口和它的Mapper配置文件必须在同一个包下!
  3. package

    package中指定要扫描的包,MyBatis会自动去识别该包下的接口和配置文件。

    <mappers>
        <package name="top.snake8859.dao"/>
    </mappers>
    
    1
    2
    3

    注意点:

    • 接口和它的Mapper配置文件必须同名!
    • 接口和它的Mapper配置文件必须在同一个包下!

# MyBatis 核心对象

MyBatis核心对象

每个 MyBatis 应用程序都以一个 SqlSessionFactory 对象的实例为核心。

首先获取 SqlSessionFactoryBuilder 对象,可以根据 XML 配置文件或者 Configuration 类的实例构建该对象。

然后获取 SqlSessionFactory 对象,该对象实例可以通过 SqlSessionFactoryBuilder 对象来获取。

有了 SqlSessionFactory 对象之后,就可以进而获取 SqlSession 实例。SqlSession 对象中完全包含以数据库为背景的所有执行 SQL 操作的方法,用该实例可以直接执行已映射的 SQL 语句。

# SqlSessionFactoryBuilder

SqlSessionFactoryBuilder 会根据配置信息或者代码生成 SqlSessionFactory,并且提供了多个 build() 方法重载,如图。

img

通过源码分析,可以发现以上方法都是在调用同一签名方法,即:

build(Reader reader, String environment, Properties properties)
1

由于参数 environment 和 properties 都可以为 null,去除重复的方法,真正的重载方法其实只有如下三种:

  • build(InputStream inputStream, String environment, Properties properties)
  • build(Reader reader, String environment, Properties properties)
  • build(Configuration config)

通过上述分析,发现配置信息可以以三种形式提供给 SqlSessionFactoryBuilder 的 build() 方法,分别是 InputStream(字节流)、Reader(字符流)、Configuration(类)。

由于字节流和字符流都属于读取配置文件的方式,所以就很容易想到构建一个 SqlSessionFactory 有两种方式,即:读取 XML 配置文件和编写代码。一般习惯为采取 XML 配置文件的方式来构造 SqlSessionFactory,这样一方面可以避免硬编码,另一方面方便日后配置人员修改,避免重复编译代码。

# 生命周期和作用域

SqlSessionFactoryBuilder 的最大特点就是用过即丢。创建 SqlSessionFactory 对象之后,这个类就不存在了,因此 SqlSessionFactoryBuilder 的最佳范围就是存在于方法体内,也就是局部变量

# SqlSessionFactory

SqlSessionFactory 是工厂接口而不是现实类,他的任务就是创建 SqlSession。

所有的 MyBatis 应用都以 SqlSessionFactory 实例为中心,SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 对象来获取。有了它以后,顾名思义,就可以通过 SqlSession 提供的 openSession() 方法来获取 SqlSession 实例。源码如下。

public interface SqlSessionFactory {
    SqlSession openSession();
    SqlSession openSession(boolean autoCommit);
    SqlSession openSession(Connection connection);
    SqlSession openSession(TransactionIsolationLevel level);
    SqlSession openSession(ExecutorType execType);
    SqlSession openSession(ExecutorType execType, boolean autoCommit);
    SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
    SqlSession openSession(ExecutorType execType, Connection connection);
    Configuration getConfiguration();
}
1
2
3
4
5
6
7
8
9
10
11

# 生命周期和作用域

SqlSessionFactory 对象一旦创建,就会在整个应用程序过程中始终存在。没有理由去销毁或再创建它,并且在应用程序运行中也不建议多次创建 SqlSessionFactory。因此 SqlSessionFactory 的最佳作用域是 Application,即随着应用程序的生命周期一直存在。这种“存在于整个应用运行期间,并且只存在一个对象实例”的模式就是所谓的单例模式(指在运行期间有且仅有一个实例)。

# SqlSession

SqlSession 是用于执行持久化操作的对象,类似于 JDBC 中的 Connection。它提供了面向数据库执行 SQL 命令所需的所有方法,可以通过 SqlSession 实例直接运行已映射的 SQL 语句。

void clearCache();
Configuration getConfiguration();
void rollback(boolean force);
void commit(boolean force);
int delete(String statement, Object parameter);
...
1
2
3
4
5
6

SqlSession 的用途主要有两种。

  1. 获取映射器。让映射器通过命名空间和方法名称找到对应的 SQL,并发送给数据库,执行后返回结果。
  2. 直接通过“命名空间(namespace)+SQL id”的方式执行 SQL,不需要获取映射器。这是 iBatis 版本留下的方式(SqlSession + 配置文件)。

# 生命周期和作用域

SqlSession 对应一次数据库会话。由于数据库会话不是永久的,因此 SqlSession 的生命周期也不是永久的,每次访问数据库时都需要创建 SqlSession 对象。

需要注意的是:每个线程都有自己的 SqlSession 实例,SqlSession 实例不能被共享,也不是线程安全的。因此 SqlSession 的作用域范围是 request 作用域或方法体作用域内。

# MyBatis 映射器(Mapper)

映射器是 MyBatis 中最重要的文件,文件中包含一组 SQL 语句(例如查询、添加、删除、修改),这些语句称为映射语句或映射 SQL 语句。

映射器由 Java 接口和 XML 文件(或注解)共同组成,它的作用如下。

  • 定义参数类型
  • 配置缓存
  • 提供 SQL 语句和动态 SQL
  • 定义查询结果和 POJO 的映射关系

映射器有以下两种实现方式。

  • 通过 XML 文件方式实现,比如在 mybatis-config.xml 文件中描述的 XML 文件,用来生成 mapper。
  • 通过注解的方式实现,使用 Configuration 对象注册 Mapper 接口。

如果 SQL 语句存在动态 SQL 或者比较复杂,使用注解写在 Java 文件里可读性差,且增加了维护的成本。所以一般建议使用 XML 文件配置的方式,避免重复编写 SQL 语句。

# XML实现映射器

XML 定义映射器分为两个部分:接口和XML。下面先定义接口 WebsiteDao。

package top.snake8859.mapper;

import top.snake8859.pojo.WebSite;

import java.util.List;

public interface WebSiteDao {
    // 新增一个网站
    void addWebSite(WebSite webSite);
    // 查询所有网站
    List<WebSite> selectAllWebSite();
}
1
2
3
4
5
6
7
8
9
10
11
12

WebSiteDao.xml代码如下。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="top.snake8859.mapper.WebSiteDao">

    <!--添加一个网站-->
    <insert id="addWebSite" parameterType="top.snake8859.pojo.WebSite">
        insert into website
            (name, url, age, country)
            value (#{name}, #{url}, #{age}, #{country})
    </insert>

    <!--查询所有网站信息-->
    <select id="selectAllWebSite" resultType="top.snake8859.pojo.WebSite">
        select * from website
    </select>

</mapper>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

下面对上述 XML 文件进行讲解。

  • namespace 用来定义命名空间,该命名空间和定义接口的全限定名一致。
  • <select> 元素表明这是一条查询语句,属性 id 用来标识这条 SQL。resultType 表示返回的是一个 Website 类型的值。

在 MyBatis 配置文件中添加以下代码。

 <mapper resource="top/snake8859/mapper/WebSiteDao.xml"></mapper>
1

该语句用来引入 XML 文件,MyBatis 会读取 WebsiteMapper.xml 文件,生成映射器。

下面进行测试,用 SqlSession 来获取 Mapper,Test 类代码如下。

@Test
public void Test2() throws IOException {
    //读取配置文件mybatis-config.xml
    InputStream config = Resources.getResourceAsStream("mybatis-config.xml");
    // 根据配置文件构建SqlSessionFactory
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(config);
    //通过SqlSessionFactory创建SqlSession
    SqlSession sqlSession = sqlSessionFactory.openSession();
    //利用SqlSession获取接口
    WebSiteDao webSiteDao = sqlSession.getMapper(WebSiteDao.class);
    //调用接口方法,操作数据库
    List<WebSite> webSites = webSiteDao.selectAllWebSite();
    for (WebSite webSite : webSites) {
        System.out.println(webSite);
    }
    sqlSession.commit();
    sqlSession.close();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

运行结果如下。

DEBUG [main] - ==>  Preparing: select * from website
DEBUG [main] - ==> Parameters: 
DEBUG [main] - <==      Total: 7
WebSite{id=1, name='snake_8859', url='https://www.snake8859.top/', age=23, country='CN', createtime=null}
WebSite{id=2, name='snake_8859', url='https://www.snake8859.top/', age=23, country='CN', createtime=null}
WebSite{id=4, name='snake_8859', url='https://www.baidu.com/', age=18, country='CN', createtime=null}
WebSite{id=5, name='snake_8859', url='https://www.google.com/', age=22, country='CN', createtime=null}
WebSite{id=6, name='snake_8859', url='https://www.stackoverflow.com/', age=20, country='CN', createtime=null}
WebSite{id=7, name='snake_8859', url='https://www.stackoverflow.com/', age=20, country='CN', createtime=null}
WebSite{id=8, name='Stack Overflow', url='https://www.stackoverflow.com/', age=20, country='CN', createtime=null}
1
2
3
4
5
6
7
8
9
10

# 注解实现映射器

使用注解的方式实现映射器,只需要在接口中使用 Java 注解,注入 SQL 即可。如下所示。

package top.snake8859.mapper;

import org.apache.ibatis.annotations.Select;
import top.snake8859.pojo.WebSite;

import java.util.List;

public interface WebSiteDao1 {
    @Select(value = "select * from website")
    public List<WebSite> selectAllWebsite();
}

1
2
3
4
5
6
7
8
9
10
11
12

使用了 @Select 注解,并且注入了和 XML 中相同的 select 语句。

如果使用注解和 XML 文件两种方式同时定义,那么 XML 方式将覆盖掉注解方式。

虽然这里注解的方式看起来比 XML 简单,但是现实中我们遇到的 SQL 会比该例子复杂得多。如果 SQL 语句中有多个表的关联、多个查询条件、级联、条件分支等,显然这条 SQL 就会复杂的多,所以并不建议使用这种方式。此外,XML 可以相互引入,而注解是不可以的,所以在一些比较复杂的场景下,使用 XML 方式会更加灵活和方便。

最后将接口在mybatis-config.xml配置。

<mapper class="top.snake8859.mapper.WebSiteDao1"></mapper>
1

也可以使用 configuration 对象注册这个接口,比如:

configuration.addMapper(WebSiteDao1.class);
1

# MyBatis执行SQL的两种方式

# SqlSession发送SQL

有映射器之后就可以通过 SqlSession 发送 SQL 了。MyBatis 常用的查询方法有 2 种,分别为 selectOne 和 selectList。

  1. selectOne

    selectOne 方法表示使用查询并且只返回一个对象,必须指定查询条件。只能查询 0 或 1 条记录,大于 1 条记录则运行错误。常用格式如下(也有其它重载方法,根据需要选择)。

    sqlSession.selectOne(String arg0, Object arg1)
    Website website = (Website)sqlSession.selectOne("top.snake8859.mapper.WebSiteDao.getWebsite",1);
    
    1
    2
  2. selectList

    selectList 方法表示使用查询并且返回一个列表。可以查询 0 或 N 条记录。常用格式如下。

    sqlSession.selectOne(String arg0)
    
    1

    也可指定参数:

    sqlSession.selectList(String arg0, Object arg1)
    
    1

    以上语法格式中,String 对象由一个命名空间加 SQL id 组合而成,它完全定位了一条 SQL,这样 MyBatis 就会找到对应的 SQL。Object 对象为需要传递的参数,也就是查询条件。

    selectOne 实现的 selectList 都可以实现,即 list 中只有一个对象。但 selectList 能实现的,selectOne 不一定能实现。

    如果 MyBatis 中只有一个 id 的 SQL,那么也可以简写为:

    Website website = (Website )sqlSession.selectOne("getWbsite",1);
    
    1

    这是 MyBatis 前身 iBatis 所留下的方式。

# Mapper接口发送SQL

SqlSession 还可以获取 Mapper 接口,通过 Mapper 接口发送 SQL,如下所示。

WebsiteMapper websiteMapper = sqlSession.getMapper(WebsiteMapper.class);
WebSiteDao webSiteDao = sqlSession.getMapper(WebSiteDao.class);
1
2

通过 SqlSession 的 getMapper 方法获取一个 Mapper 接口,然后就可以调用它的方法了。因为 XML 文件或者接口注解定义的 SQL 都可以通过“类的全限定名+方法名”查找,所以 MyBatis 会启用对应的 SQL 运行,并返回结果。

# 区别

上面分别讲解了 MyBatis 两种发送 SQL 的方式,一种用 SqlSession 直接发送,另外一种通过 SqlSession 获取 Mapper 接口再发送。建议采用 Mapper 接口发送 SQL 的方式,理由如下:

  • 使用 Mapper 接口编程可以消除 SqlSession 带来的功能性代码,提高可读性,而 SqlSession 发送 SQL,需要一个 SQL id 去匹配 SQL,比较晦涩难懂。
  • 使用 Mapper 接口,类似 websiteMapper.getWebsite(1) 则是完全面向对象的语言,更能体现业务的逻辑。
  • 使用 websiteMapper.getWebsite(1) 方式,IDE 会提示错误和校验,而使用 sqlSession.selectOne("getWebsite",1L) 语法,只有在运行中才能知道是否会产生错误。

# MyBatis增删改查

# select标签

在 MyBatis 中,select 标签是最常用也是功能最强大的 SQL 语言,用于执行查询操作。

select 示例语句如下。

<select id="selectAllWebsite" resultType="top.snake8859.pojo.WebSite" parameterType="string">
    SELECT id,NAME,url FROM website WHERE NAME LIKE CONCAT ('%',#{name},'%')
</select>
1
2
3

以上是一个 id 为 selectAllWebsite 的映射语句,参数类型为 string,返回结果类型为 Website。

执行 SQL 语句时可以定义参数,参数可以是一个简单的参数类型,例如 int、float、String;也可以是一个复杂的参数类型,例如 JavaBean、Map 等。MyBatis 提供了强大的映射规则,执行 SQL 后,MyBatis 会将结果集自动映射到 JavaBean 中。

为了使数据库的查询结果和返回值类型中的属性能够自动匹配,通常会对 MySQL 数据库和 JavaBean 采用同一套命名规则,即 Java 命名驼峰规则,这样就不需要再做映射了(数据库表字段名和属性名不一致时需要手动映射)。

参数的传递使用#{参数名},相当于告诉 MyBatis 生成 PreparedStatement 参数。对于 JDBC,该参数会被标识为“?”。以上 SQL 语句可以使用 JDBC 实现,实现代码如下。

String sql = "SELECT id,NAME,url FROM website WHERE NAME LIKE CONCAT ('%',?,'%')";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1,userName);
1
2
3

# select标签的常用属性

属性名称 描 述 备注
id 它和 Mapper 的命名空间组合起来使用,是唯一标识符,供 MyBatis 调用 如果命名空间+id不唯一,那么 MyBatis 抛出异常
parameterType 表示传入 SQL 语句传入参数类型的全限定名或别名。它是一个可选属性,MyBatis 能推断出具体传入语句的参数 支持基本数据类型和 JavaBean、Map 等复杂数据类型
resultType SQL 语句执行后返回的类型(全限定名或者别名)。如果是集合类型,返回的是集合元素的类型,返回时可以使用 resultType 或 resultMap 之一 -
resultMap 它是映射集的引用,与<resultMap>元素一起使用,返回时可以使用 resultType 或 resultMap 之一 是 MyBatis 最复杂的元素,可以配置映射规则、级联、typeHandler 等
flushCache 用于设置在调用 SQL 语句后是否要求 MyBatis 清空之前查询的本地缓存和二级缓存 默认值为 false,如果设置为 true,则任何时候只要 SQL 语句被调用都将清空本地缓存和二级缓存
useCache 启动二级缓存的开关,默认值为 true,表示将査询结果存入二级缓存中 -
timeout 用于设置超时参数,单位是秒(s),超时将抛出异常 -
fetchSize 获取记录的总条数设定 默认值是数据库厂商提供的 JDBC 驱动所设置的条数
statementType 告诉 MyBatis 使用哪个 JDBC 的 Statement 工作,取值为 STATEMENT(Statement)、 PREPARED(PreparedStatement)、CALLABLE(CallableStatement) -
resultSetType 这是针对 JDBC 的 ResultSet 接口而言,其值可设置为 FORWARD_ONLY(只允许向前访问)、SCROLL_SENSITIVE(双向滚动,但不及时更新)、SCROLLJNSENSITIVE(双向滚动,及时更新) -

# 传递多个参数

涉及多个参数传递,可采用以下三种方法。

  1. 使用Map传递参数

    使用 MyBatis 提供的 Map 接口作为参数实现,如下所示。

    <!-- 根据name和url模糊查询网站信息 -->
    <select id="selectWebsiteByMap" resultType="top.snake8859.pojoWebSite" parameterType="map">
        SELECT id,NAME,url FROM website
        WHERE name LIKE CONCAT ('%',#{name},'%')
        AND url LIKE CONCAT ('%',#{url},'%')
    </select>
    
    1
    2
    3
    4
    5
    6

    在 WebSiteDao 接口中,方法如下。

    //根据name和url模糊查询网站信息(Map)
    public List<WebSite> selectWebSiteByMap(Map<String, String> params);
    
    1
    2

    测试代码如下。

    @Test
    public void Test3() throws IOException{
        //读取配置文件mybatis-config.xml
        InputStream config = Resources.getResourceAsStream("mybatis-config.xml");
        // 根据配置文件构建SqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(config);
        //通过SqlSessionFactory创建SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();
        //利用SqlSession获取接口
        WebSiteDao webSiteDao = sqlSession.getMapper(WebSiteDao.class);
        //调用接口方法,操作数据库
        Map<String,String> paramsMap = new HashMap<String, String>();
        paramsMap.put("name", "snake8859");
        paramsMap.put("url", "snake8859");
        List<WebSite> webSites = webSiteDao.selectWebSiteByMap(paramsMap);
        for (WebSite webSite : webSites) {
            System.out.println(webSite);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    运行结果如下。

    DEBUG [main] - ==>  Preparing: SELECT id,NAME,url FROM website WHERE name LIKE CONCAT ('%',?,'%') AND url LIKE CONCAT ('%',?,'%')
    DEBUG [main] - ==> Parameters: snake_8859(String), snake8859(String)
    DEBUG [main] - <==      Total: 2
    WebSite{id=1, name='snake_8859', url='https://www.snake8859.top/', age=0, country='null', createtime=null}
    WebSite{id=2, name='snake_8859', url='https://www.snake8859.top/', age=0, country='null', createtime=null}
    
    1
    2
    3
    4
    5

    使用 Map 传递参数虽然简单易用,但是由于这样设置参数需要键值对应,业务关联性不强,开发人员需要深入到程序中看代码,造成可读性下降。

  2. 使用注解传递参数

    使用 MyBatis 的注解 @Param() 传递参数,如下所示。

    <select id="selectWebSiteByAn" resultType="top.snake8859.pojo.WebSite">
        SELECT id,NAME,url FROM website
        WHERE name LIKE CONCAT ('%',#{name},'%')
        AND url LIKE CONCAT ('%',#{url},'%')
    </select>
    
    1
    2
    3
    4
    5

    在 WebSiteDao 接口中,方法如下。

    //根据name和url模糊查询网站信息(注解)
    public List<WebSite> selectWebSiteByAn(@Param("name") String name, @Param("url") String url);
    
    1
    2

    当我们把参数传递给后台时,MyBatis 通过 @Param 提供的名称就会知道 #{name} 代表 name 参数,提高了参数可读性。但是如果这条 SQL 拥有 10 个参数的查询,就会造成可读性下降,增强了代码复杂性。

  3. 使用JavaBean传递参数

    在参数过多的情况下,MyBatis 允许组织一个 JavaBean,通过简单的 setter 和 getter 方法设置参数,提高可读性。如下所示。

    <!--添加一个网站-->
    <insert id="addWebSite" parameterType="top.snake8859.pojo.WebSite">
        insert into website
        (name, url, age, country)
        value (#{name}, #{url}, #{age}, #{country})
    </insert>
    
    1
    2
    3
    4
    5
    6

    在 WebSiteDao 接口中,方法如下。

    // 新增一个网站
    public void addWebSite(WebSite webSite);
    
    1
    2

# 区别

以上 3 种方式的区别如下。

  • 使用 Map 传递参数会导致业务可读性的丧失,继而导致后续扩展和维护的困难,所以在实际应用中我们应该果断废弃该方式。
  • 使用 @Param 注解传递参数会受到参数个数的影响。当 n≤5 时,它是最佳的传参方式,因为它更加直观;当 n>5 时,多个参数将给调用带来困难。
  • 当参数个数大于 5 个时,建议使用 JavaBean 方式。

# insert标签

MyBatis insert 标签用来定义插入语句,执行插入操作。当 MyBatis 执行完一条插入语句后,就会返回其影响数据库的行数。

下面通过一个示例演示 insert 标签的具体用法。

  1. 在WebSiteDao.xml,增加插入语句,代码如下。

    <!--添加一个网站-->
    <insert id="addWebSite" parameterType="top.snake8859.pojo.WebSite">
        insert into website
        (name, url, age, country)
        value (#{name}, #{url}, #{age}, #{country})
    </insert>
    
    1
    2
    3
    4
    5
    6
  2. 在 WebSiteDao 接口中定义add方法,代码如下

    // 新增一个网站
    public void addWebSite(WebSite webSite);
    
    1
    2
  3. 测试

    @Test
        public void Test1() throws IOException {
            //读取配置文件mybatis-config.xml
            InputStream config = Resources.getResourceAsStream("mybatis-config.xml");
            // 根据配置文件构建SqlSessionFactory
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(config);
            //通过SqlSessionFactory创建SqlSession
            SqlSession sqlSession = sqlSessionFactory.openSession();
            // SqlSession执行文件中定义的SQL,并返回映射结果
            // 添加网站
            WebSite website = new WebSite();
            website.setName("snake8859");
            website.setUrl("https://www.snake8859.top/");
            website.setAge(23);
            website.setCountry("CN");
            sqlSession.insert("top.snake8859.mapper.WebSiteMapper.addWebSite", website);
        }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

# insert标签常用属性

属性名称 描述 备注
id 它和 Mapper 的命名空间组合起来使用,是唯一标识符,供 MyBatis 调用 如果命名空间+ id 不唯一,那么 MyBatis 抛出异常
parameterType 传入 SQL 语句的参数类型的全限定名或别名,它是一个可选属性。 支持基本数据类型和 JavaBean、Map 等复杂数据类型
keyProperty 该属性的作用是将插入操作的返回值赋给 PO 类的某个属性,通常为主键对应的属性。如果是联合主键,可以将多个值用逗号隔开。 -
useGeneratedKe 该属性用来设置,是否使用 JDBC 提供的 getGenereatedKeys() 方法,获取数据库内部产生的主键并赋值到 keyProperty 属性设置的请求对象的属性中,例如 MySQL、SQL Server 等自动递增的字段,其默认值为 false。 该属性值设置为 true 后,会将数据库生成的主键回填到请求对象中,以供其他业务使用。
flushCache 该属性用于设置执行该操作后,是否会清空二级缓存和本地缓存,默认值为 true。 -
timeout 该属性用于设置执行该操作的最大时限,如果超时,就抛异常。 -
databaseId 取值范围 oracle、mysql 等,表示数据库厂家;元素内部可通过 <if test="_databaseId = 'oracle'"> 来为特定数据库指定不同的 sql 语句。 MyBatis 可以根据不同的数据库厂商执行不同的语句,这种多厂商的支持是基于映射语句中的 databaseId 属性。 MyBatis 会加载不带 databaseId 属性和带有匹配当前数据库 databaseId 属性的所有语句。 如果同时找到带有 databaseId 和不带 databaseId 的相同语句,则后者会被舍弃。
keyColumn 该属性用于设置第几列是主键,当主键列不是表中的第 1 列时,就需要设置该属性。如果是联合主键,可以将多个值用逗号隔开。 -

注意:insert 标签中没有 resultType 属性,只有查询操作才需要对返回结果类型进行相应的指定。

# 传递多个参数

若插入语句需要传递多个参,可采用与select标签一致的方式,来给映射器传递多个参数。

# 主键(自动递增)回填

MySQL、SQL Server 等数据库表可以采用自动递增的字段作为其主键,当向这样的数据库表插入数据时,即使不指定自增主键的值,数据库也会根据自增规则自动生成主键并插入到表中。

一些特殊情况下,我们可能需要将这个刚刚生成的主键回填到请求对象(原本不包含主键信息的请求对象)中,供其他业务使用。此时,我们就可以通过在 insert 标签中添加 keyProperty 和 useGeneratedKeys 属性,来实现该功能。

  1. 为 WebSiteDao.xml 中 id 为 addWebSite 的 insert 标签添加 keyProperty 和 useGeneratedKeys 属性,具体代码如下:

    <!--添加一个网站-->
    <insert id="addWebSite" parameterType="top.snake8859.pojo.WebSite" keyProperty="id" useGeneratedKeys="true">
        insert into website
        (name, url, age, country)
        value (#{name}, #{url}, #{age}, #{country})
    </insert>
    
    1
    2
    3
    4
    5
    6
  2. 测试代码

    // 添加网站
    WebSite website = new WebSite();
    //插入的对象中不包含主键 id 
    website.setName("snake8859");
    website.setUrl("https://www.snake8859.top/");
    website.setAge(23);
    website.setCountry("CN");
    //执行插入
    int num = websiteMapper.addWebsite(website);
    System.out.println("添加了 " + num + " 条记录");
    //获取回填的主键
    System.out.println("添加记录的主键是:" + website.getId());
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

# 自定义注解

如果在实际项目中,若数据库不支持主键自动递增(例如 Oracle),或者取消了主键自动递增的规则,我们可以使用 MyBatis 的 <selectKey>标签自定义生成主键,具体配置代码如下。

<!-- 添加一个网站,#{name}为 net.biancheng.po.Website 的属性值 -->
<insert id="insertWebsite" parameterType="top.snake8859.pojo.WebSite">
    <!-- 先使用selectKey标签定义主键,然后再定义SQL语句 -->
    <selectKey keyProperty="id" resultType="Integer" order="BEFORE">
        select if(max(id) is null,1,max(id)+1) as newId from Website
    </selectKey>
    insert into Website (id,name,url) values(#{id},#{name},#{url})
</insert>
1
2
3
4
5
6
7
8

# update标签

MyBatis update 标签用于定义更新语句,执行更新操作。当 MyBatis 执行完一条更新语句后,会返回一个整数,表示受影响的数据库记录的行数。

下面通过一个示例演示 update 标签的用法。

  1. 在WebSiteDao.xml,新增更新语句,代码如下。

    <update id="updateWebSite" parameterType="string">
        update website set name = #{name}
    </update>
    
    1
    2
    3
  2. 在 WebSiteDao 接口中新增一个updateWebSite()方法,代码如下。

    //更新WebSite名称
    public int updateWebSite(String name);
    
    1
    2
  3. 测试

    @Test
    public void Test4() throws IOException{
        InputStream config = Resources.getResourceAsStream("mybatis-config.xml");
        // 根据配置文件构建SqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(config);
        //通过SqlSessionFactory创建SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();
        //利用SqlSession获取接口
        WebSiteDao webSiteDao = sqlSession.getMapper(WebSiteDao.class);
        //调用接口方法,操作数据库
        int i = webSiteDao.updateWebSite("snake_8859");
        System.out.println("共更新了" + i + "条记录");
        // 提交事务
        sqlSession.commit();
        sqlSession.close();
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
  4. 运行结果如下

    DEBUG [main] - ==>  Preparing: update website set name = ?
    DEBUG [main] - ==> Parameters: snake_8859(String)
    DEBUG [main] - <==    Updates: 7
    共更新了7条记录
    
    1
    2
    3
    4

# update标签常用属性

属性名称 描述 备注
id 它和 Mapper 的命名空间组合起来使用,是唯一标识符,供 MyBatis 调用 如果命名空间+ id 不唯一,那么 MyBatis 抛出异常
parameterType 传入 SQL 语句的参数类型的全限定名或别名,它是一个可选属性。 支持基本数据类型和 JavaBean、Map 等复杂数据类型
flushCache 该属性用于设置执行该操作后,是否会清空二级缓存和本地缓存,默认值为 true。 -
timeout 该属性用于设置 SQL 执行的超时时间,如果超时,就抛异常。 -
statementType 执行 SQL 时使用的 statement 类型, 默认为 PREPARED,可选值:STATEMENT,PREPARED 和 CALLABLE。 -

注意:update 标签中没有 resultType 属性,只有查询操作才需要对返回结果类型进行相应的指定。

# delete标签

MyBatis delete 标签用于定义 delete 语句,执行删除操作。当 MyBatis 执行完一条更新语句后,会返回一个整数,表示受影响的数据库记录的行数。

下面通过一个示例演示 delete 标签的用法。

  1. 在WebSiteDao.xml,新增删除语句,代码如下。

    <delete id="deleteWebSiteById" parameterType="int">
        delete from website where id = #{id}
    </delete>
    
    1
    2
    3
  2. 在 WebSiteDao 接口中新增一个updateWebSite()方法,代码如下。

    //根据id删除WebSite
    public int deleteWebSiteById(int id);
    
    1
    2
  3. 测试

    @Test
    public void Test5() throws IOException{
        InputStream config = Resources.getResourceAsStream("mybatis-config.xml");
        // 根据配置文件构建SqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(config);
        //通过SqlSessionFactory创建SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();
        //利用SqlSession获取接口
        WebSiteDao webSiteDao = sqlSession.getMapper(WebSiteDao.class);
        //调用接口方法,操作数据库
        int i = webSiteDao.deleteWebSiteById(3);
        System.out.println("共删除了 " + i + " 条记录");
        sqlSession.commit();
        sqlSession.close();
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  4. 运行结果如下

    DEBUG [main] - ==>  Preparing: delete from website where id = ?
    DEBUG [main] - ==> Parameters: 3(Integer)
    DEBUG [main] - <==    Updates: 0
    共删除了 0 条记录
    
    1
    2
    3
    4

# delete标签常用属性

属性名称 描述 备注
id 它和 Mapper 的命名空间组合起来使用,是唯一标识符,供 MyBatis 调用 如果命名空间+ id 不唯一,那么 MyBatis 抛出异常
parameterType 传入 SQL 语句的参数类型的全限定名或别名,它是一个可选属性。 支持基本数据类型和 JavaBean、Map 等复杂数据类型
flushCache 该属性用于设置执行该操作后,是否会清空二级缓存和本地缓存,默认值为 true。 -
timeout 该属性用于设置 SQL 执行的超时时间,如果超时,就抛异常。 -
statementType 执行 SQL 时使用的 statement 类型, 默认为 PREPARED,可选值:STATEMENT,PREPARED 和 CALLABLE。 -

注意:delete 标签中没有 resultType 属性,只有查询操作才需要对返回结果类型进行相应的指定。

# MyBatis结果集映射

resultMap 是 MyBatis 中最复杂的元素,主要用于解决实体类属性名与数据库表中字段名不一致的情况,可以将查询结果映射成实体对象。

# resultMap元素的构成

resultMap 元素还可以包含以下子元素,代码如下。

<resultMap id="" type="">
    <constructor><!-- 类再实例化时用来注入结果到构造方法 -->
        <idArg/><!-- ID参数,结果为ID -->
        <arg/><!-- 注入到构造方法的一个普通结果 --> 
    </constructor>
    <id/><!-- 用于表示哪个列是主键 -->
    <result/><!-- 注入到字段或JavaBean属性的普通结果 -->
    <association property=""/><!-- 用于一对一关联 -->
    <collection property=""/><!-- 用于一对多、多对多关联 -->
    <discriminator javaType=""><!-- 使用结果值来决定使用哪个结果映射 -->
        <case value=""/><!-- 基于某些值的结果映射 -->
    </discriminator>
</resultMap>
1
2
3
4
5
6
7
8
9
10
11
12
13

其中:

  • <resultMap>元素的 type 属性表示需要的 POJO,id 属性是 resultMap 的唯一标识。
  • 子元素 <constructor> 用于配置构造方法。当一个 POJO 没有无参数构造方法时使用。
  • 子元素 <id> 用于表示哪个列是主键。允许多个主键,多个主键称为联合主键。
  • 子元素 <result>用于表示 POJO 和 SQL 列名的映射关系。
  • 子元素 <association><collection><discriminator> 用在级联的情况下。

id 和 result 元素都有以下属性。

元素 说明
property 映射到列结果的字段或属性。如果 POJO 的属性和 SQL 列名(column元素)是相同的,那么 MyBatis 就会映射到 POJO 上
column 对应 SQL 列
javaType 配置 Java 类型。可以是特定的类完全限定名或 MyBatis 上下文的别名
jdbcType 配置数据库类型。这是 JDBC 类型,MyBatis 已经为我们做了限定,基本支持所有常用数据库类型
typeHandler 类型处理器。允许你用特定的处理器来覆盖 MyBatis 默认的处理器。需要指定 jdbcType 和 javaType 相互转化的规则

一条 SQL 查询语句执行后会返回结果集,结果集有两种存储方式,即使用 Map 存储和使用 POJO 存储。

# 使用Map存储结果集

任何 select 语句都可以使用 Map 存储,代码如下。

<!-- 查询所有网站信息存到Map中 -->
<select id="selectAllWebsite" resultType="map">
    select * from website
</select>
1
2
3
4

在 WebSiteDao接口中添加以下方法。

public List<Map<String,Object>> selectAllWebSite();
1

Map 的 key 是 select 语句查询的字段名(必须完全一样),而 Map 的 value 是查询返回结果中字段对应的值,一条记录映射到一个 Map 对象中。

使用 Map 存储结果集很方便,但可读性稍差,所以一般推荐使用 POJO 的方式。

# 使用POJO存储结果集

因为 MyBatis 提供了自动映射,所以使用 POJO 存储结果集是最常用的方式。但有时候需要更加复杂的映射或级联,这时就需要使用 select 元素的 resultMap 属性配置映射集合。

WebSite类,代码如下。

package top.snake8859.pojo;

import java.util.Date;

public class WebSite {
      private int id;
    private String uname;
    private String url;
    private int age;
    private String country;
    private Date createtime;
    /* setter和getter方法*/
    @Override
    public String toString() {
        return "Website[id=" + id + ",uname=" + uname + ",url=" + url + ",age=" + age + ",country=" + country
                + ",createtime=" + createtime + "]";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

WebSiteDao.xml代码如下。

<!--使用自定义结果集类型 -->
<resultMap type="top.snake8859.pojo.WebSite" id="myResult">
    <!-- property 是 net.biancheng.po.Website 类中的属性 -->
    <!-- column是查询结果的列名,可以来自不同的表 -->
    <id property="id" column="id" />
    <result property="name" column="name" />
</resultMap>
1
2
3
4
5
6
7

resultMap 元素的属性 id 代表这个 resultMap 的标识,type 标识需要映射的 POJO。

这里使用 property 元素指定 WebSite 的属性名称 uname,column 表示数据库中 website 表的 SQL 列名 name,将 POJO 和 SQL 的查询结果一 一对应。

# resultType和resultMap的区别

MyBatis 的每一个查询映射的返回类型都是 resultMap,只是当我们提供的返回类型是 resultType 时,MyBatis 会自动把对应的值赋给 resultType 所指定对象的属性,而当我们提供的返回类型是 resultMap 时,MyBatis 会将数据库中的列数据复制到对象的相应属性上,可用于复制查询。

# MyBatis 关联查询

级联关系是一个数据库实体的概念,有 3 种级联关系,分别是一对一级联、一对多级联以及多对多级联。例如,一个角色可以分配给多个用户,也可以只分配给一个用户,SQL代码如下。

SELECT r.*,u.* FROM t_role r
INNER JOIN t_user_role ur ON r.id = ur.id
INNER JOIN t_user u ON ur.user_id = u.id
WHERE r.id = #{id}
1
2
3
4

在级联中存在 3 种对应关系。

  • 一对多的关系,如角色和用户的关系。通俗的理解就是,一家软件公司会存在许多软件工程师,公司和软件工程师就是一对多的关系。
  • 一对一的关系。每个软件工程师都有一个编号(ID),这是他在公司的标识,它与工程师是一对一的关系。
  • 多对多的关系,有些公司一个角色可以对应多个用户,但是一个用户可以兼任多个角色。通俗的说,一个人既可以是总经理,同时也是技术总监,而技术总监这个职位可以对应多个人,这就是多对多的关系。

实际应用中,由于多对多的关系比较复杂,会增加理解和关联的复杂度,所以应用较少。推荐的方法是,用一对多的关系把它分解为双向关系,以降低关系的复杂度,简化程序。

级联的优点是获取关联数据十分便捷。但是级联过多会增加系统的复杂度,同时降低系统的性能,此增彼减。所以记录超过 3 层时,就不要考虑使用级联了,因为这样会造成多个对象的关联,导致系统的耦合、负载和难以维护。

# 一对一关联

一对一级联关系在现实生活中是十分常见的,例如一个大学生只有一个学号,一个学号只属于一个学生。同样,人与身份证也是一对一的级联关系。

在 MyBatis 中,通过<resultMap>元素的子元素<association>处理一对一级联关系。示例代码如下。

<association property="studentCard" column="cardId"
                     javaType="top.snake8859.pojo.StudentCard" 
             		select="top.snake8859.mapper.StudentCarMapper.selectStuCardById"/>
1
2
3

<association>元素中通常使用以下属性。

  • property:指定映射到实体类的对象属性。
  • column:指定表中对应的字段(即查询返回的列名)。
  • javaType:指定映射到实体对象属性的类型。
  • select:指定引入嵌套查询的子 SQL 语句,该属性用于关联映射中的嵌套查询。

一对一关联查询可采用以下两种方式:

  • 单步查询,通过关联查询实现
  • 分步查询,通过两次或多次查询,为一对一关系的实体 Bean 赋值

下面以学生和学号为例讲解一对一关联查询的处理过程。

  1. 创建数据库

    创建 studentcard(学号)和student(学生)数据表,SQL 语句如下。

    CREATE TABLE `studentcard` (
                                   `id` int(20) NOT NULL AUTO_INCREMENT,
                                   `studentId` int(20) DEFAULT NULL,
                                   `startDate` date DEFAULT NULL,
                                   `endDate` date DEFAULT NULL,
                                   PRIMARY KEY (`id`),
                                   KEY `studentId` (`studentId`)
    ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;
    insert  into `studentcard`(`id`,`studentId`,`startDate`,`endDate`) values (1,20200311,'2021-03-01','2021-03-11'),(2,20200314,'2021-03-01','2021-03-11'),(3,20200709,'2021-03-01','2021-03-11'),(4,20200508,'2021-03-01','2021-03-11'),(5,20207820,'2021-03-01','2021-03-11');
    
    CREATE TABLE `student` (
                               `id` int(11) NOT NULL AUTO_INCREMENT,
                               `name` varchar(20) CHARACTER SET utf8 COLLATE utf8_unicode_ci DEFAULT NULL,
                               `sex` tinyint(4) DEFAULT NULL,
                               `cardId` int(20) DEFAULT NULL,
                               PRIMARY KEY (`id`),
                               KEY `cardId` (`cardId`),
                               CONSTRAINT `student_ibfk_1` FOREIGN KEY (`cardId`) REFERENCES `studentcard` (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
    insert  into `student`(`id`,`name`,`sex`,`cardId`) values (1,'snake',0,1),(2,'8859',0,2),(3,'赵小红',1,3),(4,'李晓明',0,4),(5,'李紫薇',1,5),(6,'钱百百',0,NULL);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
  2. 创建持久化类

    在 myBatisDemo 应用的 top.snake8859.pojo包下创建数据表对应的持久化类 Student 和 StudentCard。

    Student 的代码如下:

    package top.snake8859.pojo;
    
    public class Student {
        private int id;
        private String name;
        private int sex;
        private StudentCard studentCard;
    
    	/*省略setter和getter方法*/
    
        @Override
        public String toString() {
            return "Student{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    ", sex=" + sex +
                    ", studentCard=" + studentCard +
                    '}';
        }
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    StudentCard 的代码如下:

    package top.snake8859.pojo;
    
    import java.util.Date;
    
    public class StudentCard {
        private int id;
        private int studentId;
        private Date startDate;
        private Date endDate;
    
        /*省略setter和getter方法*/
    
        @Override
        public String toString() {
            return "StudentCard{" +
                    "id=" + id +
                    ", studentId=" + studentId +
                    ", startDate=" + startDate +
                    ", endDate=" + endDate +
                    '}';
        }
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

# 分步查询

新建 StudentCardMapper 类,代码如下。

package top.snake8859.mapper;

import top.snake8859.pojo.StudentCard;

public interface StudentCarMapper {
    // 根据id查询学生卡
    public StudentCard selectStuCardById(int id);
}
1
2
3
4
5
6
7
8

StudentCardMapper.xml 对应映射 SQL 语句代码如下。

<mapper namespace="top.snake8859.mapper.StudentCarMapper">
    <select id="selectStuCardById" resultType="top.snake8859.pojo.StudentCard" parameterType="java.lang.Integer">
        select * from studentcard where id = #{id}
    </select>
</mapper>
1
2
3
4
5

StudentMapper 类方法代码如下。

package top.snake8859.mapper;

import top.snake8859.pojo.Student;

public interface StudentMapper {
    //根据学生卡id查询学生
    public Student selectStuById1(int id);
    //根据学生卡id查询学生
    public Student selectStuById2(int id);
}
1
2
3
4
5
6
7
8
9
10

StudentMapper.xml 代码如下。

<resultMap id="cardAndStud1" type="top.snake8859.pojo.Student">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <result property="sex" column="sex"/>
    <!--一对一关联查询-->
    <association property="studentCard" column="cardId"
                 javaType="top.snake8859.pojo.StudentCard" select="top.snake8859.mapper.StudentCarMapper.selectStuCardById"/>
</resultMap>
<select id="selectStuById1" parameterType="java.lang.Integer" resultMap="cardAndStud1">
    select * from student where id = #{id}
</select>
1
2
3
4
5
6
7
8
9
10
11

测试代码如下。

@Test
public void Test1() throws IOException {
    InputStream config = Resources.getResourceAsStream("mybatis-config.xml");
    SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(config);
    SqlSession ss = ssf.openSession();
    Student student = ss.getMapper(StudentMapper.class).selectStuById1(2);
    System.out.println(student);
}
1
2
3
4
5
6
7
8

运行结果如下。

DEBUG [main] - ==>  Preparing: select * from student where id = ?
DEBUG [main] - ==> Parameters: 2(Integer)
DEBUG [main] - ====>  Preparing: select * from studentcard where id = ?
DEBUG [main] - ====> Parameters: 2(Integer)
DEBUG [main] - <====      Total: 1
DEBUG [main] - <==      Total: 1
Student{id=2, name='8859', sex=0, studentCard=StudentCard{id=2, studentId=20200314, startDate=Mon Mar 01 00:00:00 CST 2021, endDate=Thu Mar 11 00:00:00 CST 2021}}
1
2
3
4
5
6
7

# 单步查询

在 StudentMapper.xml 中添加以下代码。

<resultMap id="cardAndStu2" type="top.snake8859.pojo.Student">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <result property="sex" column="sex"/>
    <!--一对一级联查询-->
    <association property="studentCard" javaType="top.snake8859.pojo.StudentCard">
        <id property="id" column="id"/>
        <result property="studentId" column="studentId"/>
    </association>
</resultMap>
<select id="selectStuById2" parameterType="java.lang.Integer" resultMap="cardAndStu2">
    SELECT s.*,sc.studentId FROM student s,studentCard sc
    WHERE
    s.cardId = sc.id AND s.id=#{id}
</select>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

在 StudentMapper 中添加以下方法。

//根据学生卡id查询学生
public Student selectStuById2(int id);
1
2

测试代码如下。

@Test
public void Test2() throws IOException{
    InputStream config = Resources.getResourceAsStream("mybatis-config.xml");
    SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(config);
    SqlSession ss = ssf.openSession();
    Student student = ss.getMapper(StudentMapper.class).selectStuById2(2);
    System.out.println(student);
}
1
2
3
4
5
6
7
8

运行结果如下。

DEBUG [main] - ==>  Preparing: SELECT s.*,sc.studentId FROM student s,studentCard sc WHERE s.cardId = sc.id AND s.id=?
DEBUG [main] - ==> Parameters: 2(Integer)
DEBUG [main] - <==      Total: 1
Student{id=2, name='8859', sex=0, studentCard=StudentCard{id=2, studentId=20200314, startDate=null, endDate=null}}
1
2
3
4

# 一对多关联

在实际生活中也有许多一对多级联关系,例如一个用户可以有多个订单,而一个订单只属于一个用户。同样,国家和城市也属于一对多级联关系。

在 MyBatis 中,通过<resultMap>元素的子元素<collection>处理一对多级联关系,collection 可以将关联查询的多条记录映射到一个 list 集合属性中。示例代码如下。

<collection property="orderList" 
            ofType="top.snake8859.pojo.Order" 
            column="id" 
            select="top.snake8859.mapper.OrderMapper.selectOrderById"/>
1
2
3
4

<collection>元素中通常使用以下属性。

  • property:指定映射到实体类的对象属性。
  • column:指定表中对应的字段(即查询返回的列名)。
  • javaType:指定映射到实体对象属性的类型。
  • select:指定引入嵌套查询的子 SQL 语句,该属性用于关联映射中的嵌套查询。

一对多关联查询可采用以下两种方式:

  • 分步查询,通过两次或多次查询,为一对多关系的实体 Bean 赋值
  • 单步查询,通过关联查询实现

下面以用户和订单为例讲解一对多关联查询(实现“根据 id 查询用户及其关联的订单信息”的功能)的处理过程。

  1. 创建数据表

    CREATE TABLE `user` (
                            `id` int(11) NOT NULL AUTO_INCREMENT,
                            `name` varchar(20) DEFAULT NULL,
                            `pwd` varchar(20) DEFAULT NULL,
                            PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
    insert  into `user`(`id`,`name`,`pwd`) values (1,'snake','123'),(2,'8859','456'),(3,'赵小红','123'),(4,'李晓明','345'),(5,'杨小胤','123'),(6,'谷小乐','789');
    
    CREATE TABLE `order` (
                             `id` int(11) NOT NULL AUTO_INCREMENT,
                             `ordernum` int(25) DEFAULT NULL,
                             `userId` int(11) DEFAULT NULL,
                             PRIMARY KEY (`id`),
                             KEY `userId` (`userId`),
                             CONSTRAINT `order_ibfk_1` FOREIGN KEY (`userId`) REFERENCES `user` (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;
    insert  into `order`(`id`,`ordernum`,`userId`) values (1,20200107,1),(2,20200806,2),(3,20206702,3),(4,20200645,1),(5,20200711,2),(6,20200811,2),(7,20201422,3),(8,20201688,4);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
  2. 创建持久化对象

    创建持久化类 User 和 Order,代码分别如下。

    package top.snake8859.pojo;
    
    import java.util.List;
    
    public class User {
        private int id;
        private String name;
        private String pwd;
        private List<Order> orderList;
    
     	/*省略setter和getter方法*/
    
        @Override
        public String toString() {
            return "User{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    ", pwd='" + pwd + '\'' +
                    ", orderList=" + orderList +
                    '}';
        }
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    Order 类代码如下。

    package top.snake8859.pojo;
    
    public class Order {
        private int id;
        private int ordernum;
    
        /*省略setter和getter方法*/
    
        @Override
        public String toString() {
            return "Order{" +
                    "id=" + id +
                    ", ordernum=" + ordernum +
                    '}';
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

# 分步查询

OrderMapper 类代码如下。

public List<Order> selectOrderById(int id);
1

OrderMapper.xml 中相应的映射 SQL 语句如下。

<?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="top.snake8859.mapper.OrderMapper">
    <select id="selectOrderById" resultType="top.snake8859.pojo.Order" parameterType="java.lang.Integer">
        SELECT * FROM `order` where userId=#{id}
    </select>
</mapper>
1
2
3
4
5
6
7
8
9
10

UserMapper 类代码如下。

public User selectUserOrderById1(int id);
1

UserMapper.xml 中相应的映射 SQL 语句如下。

<?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="top.snake8859.mapper.UserMapper">
    <resultMap id="userAndOrder1" type="top.snake8859.pojo.User">
        <id property="id" column="id"/>
        <result property="name" column="name" />
        <result property="pwd" column="pwd" />
        <!--一对多级联查询,ofType表示集合中的元素类型,将id传递给selectOrderById-->
        <collection property="orderList" ofType="top.snake8859.pojo.Order" column="id" select="top.snake8859.mapper.OrderMapper.selectOrderById"/>
    </resultMap>

    <select id="selectUserOrderById1" parameterType="java.lang.Integer" resultMap="userAndOrder1">
        select * from user where id=#{id}
    </select>
</mapper>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

测试代码如下。

@Test
public void Test3() throws IOException{
    InputStream config = Resources.getResourceAsStream("mybatis-config.xml");
    SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(config);
    SqlSession ss = ssf.openSession();
    User us = ss.getMapper(UserMapper.class).selectUserOrderById1(1);
    System.out.println(us);
    ss.commit();
    ss.close();
}
1
2
3
4
5
6
7
8
9
10

运行结果如下。

DEBUG [main] - ==>  Preparing: select * from user where id=?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - ====>  Preparing: SELECT * FROM `order` where userId=?
DEBUG [main] - ====> Parameters: 1(Integer)
DEBUG [main] - <====      Total: 2
DEBUG [main] - <==      Total: 1
User{id=1, name='snake', pwd='123', orderList=[Order{id=1, ordernum=20200107}, Order{id=4, ordernum=20200645}]}
1
2
3
4
5
6
7

# 单步查询

该种方式实现一对多关联查询需要修改 Order 持久化类,因为 Order 中的 id 不能和 User 中的 id 重复。

package top.snake8859.pojo;
public class Order {
    private int oId;
    private int ordernum;
    /*省略setter和getter方法*/
    @Override
    public String toString() {
        return "Order [id=" + oId+ ", ordernum=" + ordernum + "]";
    }
}
1
2
3
4
5
6
7
8
9
10

UserMapper 类代码如下。

public User selectUserOrderById2(int id);
1

UserMapper.xml 中相关映射 SQL 语句如下。

<!-- 一对多 根据id查询用户及其关联的订单信息:级联查询的第二种方法(单步查询) -->
<resultMap type="top.snake8859.pojo.User" id="userAndOrder2">
    <id property="id" column="id" />
    <result property="name" column="name" />
    <result property="pwd" column="pwd" />
    <!-- 一对多级联查询,ofType表示集合中的元素类型 -->
    <collection property="orderList"
        ofType="top.snake8859.pojo.Order">
        <id property="oId" column="oId" />
        <result property="ordernum" column="ordernum" />
    </collection>
</resultMap>
<select id="selectUserOrderById2" parameterType="Integer"
    resultMap="userAndOrder2">
    SELECT u.*,o.id as oId,o.ordernum FROM `user` u,`order` o
    WHERE
    u.id=o.`userId` AND u.id=#{id}
</select>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 多对多关联

实际应用中,由于多对多的关系比较复杂,会增加理解和关联的复杂度,所以应用较少。MyBatis 没有实现多对多级联,推荐通过两个一对多级联替换多对多级联,以降低关系的复杂度,简化程序。

例如,一个订单可以有多种商品,一种商品可以对应多个订单,订单与商品就是多对多的级联关系。可以使用一个中间表(订单记录表)将多对多级联转换成两个一对多的关系。

下面以订单和商品(实现“查询所有订单以及每个订单对应的商品信息”的功能)为例讲解多对多关联查询。

  1. 创建数据表

    CREATE TABLE `order` (
                             `id` int(11) NOT NULL AUTO_INCREMENT,
                             `ordernum` int(25) DEFAULT NULL,
                             `userId` int(11) DEFAULT NULL,
                             PRIMARY KEY (`id`),
                             KEY `userId` (`userId`),
                             CONSTRAINT `order_ibfk_1` FOREIGN KEY (`userId`) REFERENCES `user` (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;
    insert  into `order`(`id`,`ordernum`,`userId`) values (1,20200107,1),(2,20200806,2),(3,20206702,3),(4,20200645,1),(5,20200711,2),(6,20200811,2),(7,20201422,3),(8,20201688,4);
    
    CREATE TABLE `product` (
                               `pid` int(11) NOT NULL AUTO_INCREMENT,
                               `name` varchar(25) DEFAULT NULL,
                               `price` double DEFAULT NULL,
                               PRIMARY KEY (`pid`)
    ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
    insert  into `product`(`pid`,`name`,`price`) values (1,'Java教程',128),(2,'C语言教程',138),(3,'Python教程',132.35);
    
    CREATE TABLE `orders_detail` (
                                     `id` int(11) NOT NULL AUTO_INCREMENT,
                                     `orderId` int(11) DEFAULT NULL,
                                     `productId` int(11) DEFAULT NULL,
                                     PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
    insert  into `orders_detail`(`id`,`orderId`,`productId`) values (1,1,1),(2,1,2),(3,1,3),(4,2,3),(5,2,1),(6,3,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
  2. 创建持久化对象

    Order 类代码如下。

    package top.snake8859.pojo;
    import java.util.List;
    public class Order {
        private int id;
        private int ordernum;
        private List<Product> products;
        /*省略setter和getter方法*/
        @Override
        public String toString() {
            return "Order [id=" + id + ", ordernum=" + ordernum + ", products=" + products + "]";
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    Product 类方法如下。

    package top.snake8859.pojo;
    import java.util.List;
    public class Product {
        private int pid;
        private String name;
        private Double price;
        // 多对多中的一个一对多
        private List<Order> orders;
        /*省略setter和getter方法*/
        @Override
        public String toString() {
            return "Product [id=" + pid + ", name=" + name + ", price=" + price + "]";
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
  3. 创建接口和映射文件

    OrderMapper 接口代码如下。

    package top.snake8859.mapper;
    
    import top.snake8859.pojo.Order;
    
    import java.util.List;
    
    public interface OrderMapper {
        public List<Order> selectAllOrdersAndProducts();
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    OrderMapper.xml 代码如下。

    <resultMap type="top.snake8859.pojo.Order" id="orderMap">
        <id property="id" column="id" />
        <result property="ordernum" column="ordernum" />
        <collection property="products"
                    ofType="ntop.snake8859.pojo.Product">
            <id property="pid" column="pid" />
            <result property="name" column="name" />
            <result property="price" column="price" />
        </collection>
    </resultMap>
    <select id="selectAllOrdersAndProducts" parameterType="Integer"
            resultMap="orderMap">
        SELECT o.id,o.`ordernum`,p.`pid`,p.`name`,p.`price` FROM
        `order` o
        INNER JOIN orders_detail od ON o.id=od.`orderId`
        INNER JOIN
        product p
        ON p.pid = od.`productId`
    </select>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
  4. 创建测试类

    @Test
    public void Test4() throws IOException{
        InputStream config = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(config);
        SqlSession ss = ssf.openSession();
        List<Order> orders = ss.getMapper(OrderMapper.class).selectAllOrdersAndProducts();
        for (Order order : orders) {
            System.out.println(order);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  5. 运行结果如下。

    DEBUG [main] - ==>  Preparing: SELECT o.id,o.`ordernum`,p.`pid`,p.`name`,p.`price` FROM `order` o INNER JOIN orders_detail od ON o.id=od.`orderId` INNER JOIN product p ON p.pid = od.`productId`
    DEBUG [main] - ==> Parameters: 
    DEBUG [main] - <==      Total: 6
    Order{id=1, ordernum=20200107, products=[Product{pid=1, name='Java教程', price=128.0, orders=null}, Product{pid=2, name='C语言教程', price=138.0, orders=null}, Product{pid=3, name='Python教程', price=132.35, orders=null}]}
    Order{id=2, ordernum=20200806, products=[Product{pid=3, name='Python教程', price=132.35, orders=null}, Product{pid=1, name='Java教程', price=128.0, orders=null}]}
    Order{id=3, ordernum=20206702, products=[Product{pid=2, name='C语言教程', price=138.0, orders=null}]}
    
    1
    2
    3
    4
    5
    6

# MyBatis 动态SQL

动态 SQL 是 MyBatis 的强大特性之一。在 JDBC 或其它类似的框架中,开发人员通常需要手动拼接 SQL 语句。根据不同的条件拼接 SQL 语句是一件极其痛苦的工作。例如,拼接时要确保添加了必要的空格,还要注意去掉列表最后一个列名的逗号。而动态 SQL 恰好解决了这一问题,可以根据场景动态的构建查询。

动态 SQL 只有几个基本元素,与 JSTL 或 XML 文本处理器相似,十分简单明了,大量的判断都可以在 MyBatis 的映射 XML 文件里配置,以达到许多需要大量代码才能实现的功能。

动态 SQL 大大减少了编写代码的工作量,更体现了 MyBatis 的灵活性、高度可配置性和可维护性。

MyBatis 的动态 SQL 包括以下几种元素,如下表所示。

元素 作用 备注
if (opens new window) 判断语句 单条件分支判断
choose(when、otherwise) (opens new window) 相当于 Java 中的 switch case 语句 多条件分支判断
trim (opens new window)where (opens new window) 辅助元素 用于处理一些SQL拼装问题
foreach (opens new window) 循环语句 在in语句等列举条件常用
bind (opens new window) 辅助元素 拼接参数

# if标签

MyBatis if 类似于 Java 中的 if 语句,是 MyBatis 中最常用的判断语句。使用 if 标签可以节省许多拼接 SQL 的工作,把精力集中在 XML 的维护上。

if 语句使用方法简单,常常与 test 属性联合使用。语法如下。

<if test="判断条件">
    SQL语句
</if>
1
2
3

当判断条件为 true 时,才会执行所包含的 SQL 语句。

最常见的场景是在 if 语句中包含 where 子句,例如。

<select id="selectAllWebSite" resultMap="myResult">
    select id,name,url from website
    <if test="name != null">
        where name like #{name}
    </if>
</select>
1
2
3
4
5
6

以上代表表示根据网站名称去查找相应的网站信息,但是网站名称是一个可填可不填的条件,不填写的时候不作为查询条件。

可多个 if 语句同时使用。以下语句表示为可以按照网站名称(name)或者网址(url)进行模糊查询。如果您不输入名称或网址,则返回所有的网站记录。但是,如果你传递了任意一个参数,它就会返回与给定参数相匹配的记录。

<select id="selectAllWebSite" resultMap="myResult">
    select id,name,url from website where 1=1
    <if test="name != null">
        AND name like #{name}
    </if>
    <if test="url!= null">
        AND url like #{url}
    </if>
</select>
1
2
3
4
5
6
7
8
9

# chose、when和otherwise标签

MyBatis 中动态语句 choose-when-otherwise 类似于 Java 中的 switch-case-default 语句。由于 MyBatis 并没有为 if 提供对应的 else 标签,如果想要达到<if>...<else>...</else> </if>的效果,可以借助 <choose>、<when>、<otherwise>来实现。

动态语句 choose-when-otherwise 语法如下。

<choose>
    <when test="判断条件1">
        SQL语句1
    </when >
    <when test="判断条件2">
        SQL语句2
    </when >
    <when test="判断条件3">
        SQL语句3
    </when >
    <otherwise>
        SQL语句4
    </otherwise>
</choose>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

choose 标签按顺序判断其内部 when 标签中的判断条件是否成立,如果有一个成立,则执行相应的 SQL 语句,choose 执行结束;如果都不成立,则执行 otherwise 中的 SQL 语句。这类似于 Java 的 switch 语句,choose 为 switch,when 为 case,otherwise 则为 default。

以下示例要求:

  • 当网站名称不为空时,只用网站名称作为条件进行模糊查询;
  • 当网站名称为空,而网址不为空时,则用网址作为条件进行模糊查询;
  • 当网站名称和网址都为空时,则要求网站年龄不为空。

WebSiteMapper.xml 代码如下。

<mapper namespace="top.snake8859.mapper.WebSiteMapper">
    <select id="selectWebSite"
        parameterType="top.snake8859.pojo.WebSite"
        resultType="top.snake8859.pojo.WebSite">
        SELECT id,name,url,age,country
        FROM website WHERE 1=1
        <choose>
            <when test="name != null and name !=''">
                AND name LIKE CONCAT('%',#{name},'%')
            </when>
            <when test="url != null and url !=''">
                AND url LIKE CONCAT('%',#{url},'%')
            </when>
            <otherwise>
                AND age is not null
            </otherwise>
        </choose>
    </select>
</mapper>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# where标签

在chose、when和otherwise标签示例内可以发现SQL 语句中加入了一个条件“1=1”,如果没有加入这个条件,那么可能就会变成下面这样一条错误的语句。

SELECT id,name,url,age,country FROM website AND name LIKE CONCAT('%',#{name},'%')
1

显然以上语句会出现 SQL 语法异常,但加入“1=1”这样的条件又非常奇怪,所以 MyBatis 提供了 where 标签。

where 标签主要用来简化 SQL 语句中的条件判断,可以自动处理 AND/OR 条件,语法如下。

<where>
    <if test="判断条件">
        AND/OR ...
    </if>
</where>
1
2
3
4
5

if 语句中判断条件为 true 时,where 关键字才会加入到组装的 SQL 里面,否则就不加入。where 会检索语句,它会将 where 后的第一个 SQL 条件语句的 AND 或者 OR 关键词去掉。

示例:根据网站名称或网址对网站进行模糊查询。

WebSiteMapper.xml 代码如下。

<select id="selectWebSite" resultType="top.snake8859.pojo.WebSite">
    select id,name,url from website
    <where>
        <if test="name != null">
            AND name like #{name}
        </if>
        <if test="url!= null">
            AND url like #{url}
        </if>
    </where>
</select>
1
2
3
4
5
6
7
8
9
10
11

# set标签

在 Mybatis 中,update 语句可以使用 set 标签动态更新列。set 标签可以为 SQL 语句动态的添加 set 关键字,剔除追加到条件末尾多余的逗号。

案例:根据 id 修改网站名称或网址。

WebSiteMapper.xml 代码如下。

<mapper namespace="top.snake8859.mapper.WebSiteMapper">
    <!--使用set元素动态修改一个网站记录 -->
    <update id="updateWebsite"
        parameterType="top.snake8859.pojo.WebSite">
        UPDATE website
        <set>
            <if test="name!=null">name=#{name}</if>
            <if test="url!=null">url=#{url}</if>
        </set>
        WHERE id=#{id}
    </update>
</mapper>
1
2
3
4
5
6
7
8
9
10
11
12

# foreach标签

对于一些 SQL 语句中含有 in 条件,需要迭代条件集合来生成的情况,可以使用 foreach 来实现 SQL 条件的迭代。

Mybatis foreach 标签用于循环语句,它很好的支持了数据和 List、set 接口的集合,并对此提供遍历的功能。语法格式如下。

<foreach item="item" index="index" collection="list|array|map key" open="(" separator="," close=")">
    参数值
</foreach>
1
2
3

foreach 标签主要有以下属性,说明如下。

  • item:表示集合中每一个元素进行迭代时的别名。
  • index:指定一个名字,表示在迭代过程中每次迭代到的位置。
  • open:表示该语句以什么开始(既然是 in 条件语句,所以必然以(开始)。
  • separator:表示在每次进行迭代之间以什么符号作为分隔符(既然是 in 条件语句,所以必然以,作为分隔符)。
  • close:表示该语句以什么结束(既然是 in 条件语句,所以必然以)开始)。

使用 foreach 标签时,最关键、最容易出错的是 collection 属性,该属性是必选的,但在不同情况下该属性的值是不一样的,主要有以下 3 种情况:

  • 如果传入的是单参数且参数类型是一个 List,collection 属性值为 list。
  • 如果传入的是单参数且参数类型是一个 array 数组,collection 的属性值为 array。
  • 如果传入的参数是多个,需要把它们封装成一个 Map,当然单参数也可以封装成 Map。Map 的 key 是参数名,collection 属性值是传入的 List 或 array 对象在自己封装的 Map 中的 key。

现有website表包含以下记录。

image-20220226221815133

WebSiteMapper.xml 代码如下。

<select id="selectWebSite" parameterType="top.snake8859.pojo.WebSite" resultType="top.snake8859.pojo.WebSite">
    SELECT id,name,url,age,country
    FROM website WHERE age in
    <foreach collection="list" item="age" index="index" open="(" close=")" separator=",">
        #{age}
    </foreach>
</select>
1
2
3
4
5
6
7

测试代码如下。

  @Test
public void Test6() throws IOException{
    // 读取配置文件mybatis-config.xml
    InputStream config = Resources.getResourceAsStream("mybatis-config.xml"); // 根据配置文件构建
    SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(config);
    // 通过SqlSessionFactory创建SqlSession
    SqlSession ss = ssf.openSession();
    List<Integer> ageList = new ArrayList<Integer>();
    ageList.add(18);
    ageList.add(22);
    List<WebSite> siteList = ss.selectList("top.snake8859.mapper.WebSiteMapper.selectWebSite", ageList);
    for (WebSite ws : siteList) {
        System.out.println(ws);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

运行结果如下。

DEBUG [main] - ==>  Preparing: SELECT id,name,url,age,country FROM website WHERE age in ( ? , ? )
DEBUG [main] - ==> Parameters: 18(Integer), 22(Integer)
DEBUG [main] - <==      Total: 2
WebSite{id=4, name='snake_8859', url='https://www.baidu.com/', age=18, country='CN', createtime=null}
WebSite{id=5, name='snake_8859', url='https://www.google.com/', age=22, country='CN', createtime=null}
1
2
3
4
5

# bind标签

每个数据库的拼接函数或连接符号都不同,例如 MySQL 的 concat 函数、Oracle 的连接符号“||”等。这样 SQL 映射文件就需要根据不同的数据库提供不同的实现,显然比较麻烦,且不利于代码的移植。幸运的是,MyBatis 提供了 bind 标签来解决这一问题。

bind 标签可以通过 OGNL 表达式自定义一个上下文变量。

比如,按照网站名称进行模糊查询,SQL 映射文件如下。

<select id="selectWebSite" resultType="top.snake8859.pojo.WebSite">
    <bind name="pattern" value="'%'+_parameter+'%'" />
    SELECT id,name,url,age,country
    FROM website
    WHERE name like #{pattern}
</select>
1
2
3
4
5
6

bind 元素属性如下。

  • value:对应传入实体类的某个字段,可以进行字符串拼接等特殊处理。
  • name:给对应参数取的别名。

以上代码中的“_parameter”代表传递进来的参数,它和通配符连接后,赋给了 pattern,然后就可以在 select 语句中使用这个变量进行模糊查询,不管是 MySQL 数据库还是 Oracle 数据库都可以使用这样的语句,提高了可移植性。

# trim标签

在 MyBatis 中除了使用 if+where 实现多条件查询,还有一个更为灵活的元素 trim 能够替代之前的做法。

trim 一般用于去除 SQL 语句中多余的 AND 关键字、逗号或者给 SQL 语句前拼接 where、set 等后缀,可用于选择性插入、更新、删除或者条件查询等操作。trim 语法格式如下。

<trim prefix="前缀" suffix="后缀" prefixOverrides="忽略前缀字符" suffixOverrides="忽略后缀字符">
    SQL语句
</trim>
1
2
3

trim 中属性说明如下。

属性 描述
prefix 给SQL语句拼接的前缀,为 trim 包含的内容加上前缀
suffix 给SQL语句拼接的后缀,为 trim 包含的内容加上后缀
prefixOverrides 去除 SQL 语句前面的关键字或字符,该关键字或者字符由 prefixOverrides 属性指定。
suffixOverrides 去除 SQL 语句后面的关键字或者字符,该关键字或者字符由 suffixOverrides 属性指定。

示例:根据网站名称或网址对网站进行模糊查询。

WebSiteMapper.xml 代码如下。

<select id="selectWebSite" resultType="top.snake8859.top.WebSite">
    SELECT id,name,url,age,country
    FROM website
    <trim prefix="where" prefixOverrides="and">
        <if test="name != null and name !=''">
            AND name LIKE CONCAT ('%',#{name},'%')
        </if>
        <if test="url!= null">
            AND url like concat ('%',#{url},'%')
        </if>
    </trim>
</select>
1
2
3
4
5
6
7
8
9
10
11
12

# MyBatis 分页

MyBatis 的分页功能是基于内存的分页,即先查询出所有记录,再按起始位置和页面容量取出结果。

WebSiteDao中方法如下。

/**
     * MyBatis分页
     * @param site
     * @param currentPageNo 起始位置
     * @param pageSize 页面容量
     * @return
     */
public List<WebSite> selectWebSite(@Param("site") WebSite site, @Param("from") Integer currentPageNo, @Param("pageSize") Integer pageSize);
1
2
3
4
5
6
7
8

相比原来的 selectWebSite方法,增加了两个参数,起始位置(from)和页面容量(pageSize),用于实现分页查询。

修改 WebSiteDao.xml 的查询语句,增加 limit 关键字,SQL 映射代码如下。

<select id="selectWebSite" resultType="top.snake8859.pojo.WebSite">
    select id,name,url,age,country from website
    <trim prefix="where" prefixOverrides="and">
        <if test="site.name != null and site.name != ''">
            AND name LIKE CONCAT('%', #{site.name}, '%')
        </if>
        <if test="site.url != null and site.url!=''">
            AND url LIKE CONCAT('%', #{site.url} '%')
        </if>
        ORDER BY id limit #{from},#{pageSize}
    </trim>
</select>
1
2
3
4
5
6
7
8
9
10
11
12

测试代码如下。

@Test
public void Test7() throws IOException{
    // 读取配置文件mybatis-config.xml
    InputStream config = Resources.getResourceAsStream("mybatis-config.xml");
    // 根据配置文件构建
    SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(config);
    // 通过SqlSessionFactory创建SqlSession
    SqlSession ss = ssf.openSession();
    // 创建查询资料
    WebSite site = new WebSite();
    site.setUrl("http");
    Integer pageSize = 3;
    Integer currentPageNo = 1;

    List<WebSite> webSites = ss.getMapper(WebSiteDao.class).selectWebSite(site, currentPageNo, pageSize);
    for (WebSite webSite : webSites) {
        System.out.println(webSite);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

运行结果如下。

DEBUG [main] - ==>  Preparing: select id,name,url,age,country from website where url LIKE CONCAT('%', ? '%') ORDER BY id limit ?,?
DEBUG [main] - ==> Parameters: http(String), 1(Integer), 3(Integer)
DEBUG [main] - <==      Total: 3
WebSite{id=2, name='snake_8859', url='https://www.snake8859.top/', age=23, country='CN', createtime=null}
WebSite{id=4, name='snake_8859', url='https://www.baidu.com/', age=18, country='CN', createtime=null}
WebSite{id=5, name='snake_8859', url='https://www.google.com/', age=22, country='CN', createtime=null}
1
2
3
4
5
6

上述代码中,根据传入的起始位置(currentPageNo=0)和页面容量(pageSize=3)进行相应分页,查看第一页的数据列表,运行测试方法,输出正确的分页列表。

注意:MyBatis 实现分页查询属于 DAO 层操作,由于 DAO 层不牵涉任何业务实现,所以实现分页的方法中第一个参数为 limit 的起始位置(下标从 0 开始),而不是用户输入的真正页码(页码从1开始)。

# MyBatis 缓存

缓存可以将数据保存在内存中,是互联网系统常常用到的。目前流行的缓存服务器有 MongoDB、Redis、Ehcache 等。缓存是在计算机内存上保存的数据,读取时无需再从磁盘读入,因此具备快速读取和使用的特点。

和大多数持久化框架一样,MyBatis 提供了一级缓存和二级缓存的支持。默认情况下,MyBatis 只开启一级缓存。

# 一级缓存

一级缓存是基于 PerpetualCache(MyBatis自带)的 HashMap 本地缓存,作用范围为 session 域内。当 session flush(刷新)或者 close(关闭)之后,该 session 中所有的 cache(缓存)就会被清空。

在参数和 SQL 完全一样的情况下,我们使用同一个 SqlSession 对象调用同一个 mapper 的方法,往往只执行一次 SQL。因为使用 SqlSession 第一次查询后,MyBatis 会将其放在缓存中,再次查询时,如果没有刷新,并且缓存没有超时的情况下,SqlSession 会取出当前缓存的数据,而不会再次发送 SQL 到数据库。

由于 SqlSession 是相互隔离的,所以如果你使用不同的 SqlSession 对象,即使调用相同的 Mapper、参数和方法,MyBatis 还是会再次发送 SQL 到数据库执行,返回结果。

一级缓存示例:

  1. 在 WebSiteDao 类中添加 selectWebSiteById方法,代码如下。

    //根据id查询WebSite
    public WebSite selectWebSiteById(int id);
    
    1
    2
  2. WebSiteDao.xml 中添加相应的映射 SQL 语句,代码如下。

    <select id="selectWebSiteById" parameterType="java.lang.Integer" resultType="top.snake8859.pojo.WebSite">
    	select * from website where id = #{id}
    </select>
    
    1
    2
    3
  3. 测试代码

     @Test
    public void Test8() throws IOException{
        InputStream config = Resources.getResourceAsStream("mybatis-config.xml"); // 根据配置文件构建
        SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(config);
        SqlSession ss = ssf.openSession();
    
        WebSiteDao webSiteDao = ss.getMapper(WebSiteDao.class);
    
        WebSite site = webSiteDao.selectWebSiteById(1);
        logger.debug("使用同一个sqlsession再执行一次");
        WebSite site1 = webSiteDao.selectWebSiteById(1);
        // 请注意,当我们使用二级缓存的时候,sqlSession调用了 commit方法后才会生效
        ss.commit();
    
        logger.debug("现在创建一个新的SqlSeesion对象在执行一次");
        SqlSession ss2 = ssf.openSession();
        WebSiteDao webSiteDao1 = ss2.getMapper(WebSiteDao.class);
        WebSite site2 = webSiteDao1.selectWebSiteById(1);
        // 请注意,当我们使用二级缓存的时候,sqlSession调用了 commit方法后才会生效
        ss2.commit();
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
  4. 运行结果如下

    DEBUG [main] - ==>  Preparing: select * from website where id = ?
    DEBUG [main] - ==> Parameters: 1(Integer)
    DEBUG [main] - <==      Total: 1
    DEBUG [main] - ==>  Preparing: select * from website where id = ?
    DEBUG [main] - ==> Parameters: 1(Integer)
    DEBUG [main] - <==      Total: 1
    
    1
    2
    3
    4
    5
    6

从运行结果可以看出,第一个 SqlSession 实际只发生过一次查询,而第二次查询就从缓存中取出了,也就是 SqlSession 层面的一级缓存。

# 二级缓存

二级缓存是全局缓存,作用域超出 session 范围之外,可以被所有 SqlSession 共享。

一级缓存缓存的是 SQL 语句,二级缓存缓存的是结果对象。

二级缓存配置:

  1. MyBatis 的全局缓存配置需要在 mybatis-config.xml 的 settings 元素中设置,代码如下。

    <settings>
        <setting name="cacheEnabled" value="true" />
    </settings>
    
    1
    2
    3
  2. 在 mapper 文件中设置缓存,默认不开启缓存。需要注意的是,二级缓存的作用域是针对 mapper 的 namescape 而言,即只有再次在 namescape 内(net.biancheng.WebsiteMapper)的查询才能共享这个缓存,代码如下。

    <mapper namescape="top.snkae8859.WebSiteDao">
        <!-- cache配置 -->
        <cache
            eviction="FIFO"
            flushInterval="60000"
            size="512"
            readOnly="true" />
        ...
    </mapper>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    以上属性说明如下。

    属性 说明
    eviction 代表的是缓存回收策略,目前 MyBatis 提供以下策略。LRU:使用较少,移除最长时间不用的对象;FIFO:先进先出,按对象进入缓存的顺序来移除它们;SOFT:软引用,移除基于垃圾回收器状态和软引用规则的对象;WEAK:弱引用,更积极地移除基于垃圾收集器状态和弱引用规则的对象。
    flushInterval 刷新间隔时间,单位为毫秒,这里配置的是 100 秒刷新,如果省略该配置,那么只有当 SQL 被执行的时候才会刷新缓存。
    size 引用数目,正整数,代表缓存最多可以存储多少个对象,不宜设置过大。设置过大会导致内存溢出。这里配置的是 1024 个对象。
    readOnly 只读,默认值为 false,意味着缓存数据只能读取而不能修改,这样设置的好处是可以快速读取缓存,缺点是没有办法修改缓存。
  3. 在 mapper 文件配置支持 cache 后,如果需要对个别查询进行调整,可以单独设置 cache,代码如下。

    <select id="getWebsiteList" resultType="top.snake8859.pojo.WebSite" usecache="true">
        ...
    </select>
    
    1
    2
    3

对于 MyBatis 缓存仅作了解即可,因为面对一定规模的数据量,内置的 Cache 方式就派不上用场了,并且对查询结果集做缓存并不是 MyBatis 所擅长的,它专心做的应该是 SQL 映射。对于缓存,采用 OSCache、Memcached 等专门的缓存服务器来做更为合理。

# MyBatis与Hibernate区别

MyBatis和Hibernate的区别

Hibernate 和 MyBatis 都是目前业界中主流的对象关系映射(ORM)框架,它们的主要区别如下。

  1. SQL优化方面

    • Hibernate 使用 HQL(Hibernate Query Language)语句,独立于数据库。不需要编写大量的 SQL,就可以完全映射,但会多消耗性能,且开发人员不能自主的进行 SQL 性能优化。提供了日志、缓存、级联(级联比 MyBatis 强大)等特性。
    • MyBatis 需要手动编写 SQL,所以灵活多变。支持动态 SQL、处理列表、动态生成表名、支持存储过程。工作量相对较大。
  2. 开发方面

    • MyBatis 是一个半自动映射的框架,因为 MyBatis 需要手动匹配 POJO 和 SQL 的映射关系。
    • Hibernate 是一个全表映射的框架,只需提供 POJO 和映射关系即可。
  3. 缓存机制方面

    • Hibernate 的二级缓存配置在 SessionFactory 生成的配置文件中进行详细配置,然后再在具体的表-对象映射中配置缓存。

    • MyBatis 的二级缓存配置在每个具体的表-对象映射中进行详细配置,这样针对不同的表可以自定义不同的缓存机制。并且 Mybatis 可以在命名空间中共享相同的缓存配置和实例,通过 Cache-ref 来实现。

    Hibernate 对查询对象有着良好的管理机制,用户无需关心 SQL。所以在使用二级缓存时如果出现脏数据,系统会报出错误并提示。而 MyBatis 在这一方面,使用二级缓存时需要特别小心。如果不能完全确定数据更新操作的波及范围,避免 Cache 的盲目使用。否则脏数据的出现会给系统的正常运行带来很大的隐患。

  4. Hibernate优势

    • Hibernate 的 DAO 层开发比 MyBatis 简单,Mybatis 需要维护 SQL 和结果映射。
    • Hibernate 对对象的维护和缓存要比 MyBatis 好,对增删改查的对象的维护要方便。
    • Hibernate 数据库移植性很好,MyBatis 的数据库移植性不好,不同的数据库需要写不同 SQL。
    • Hibernate 有更好的二级缓存机制,可以使用第三方缓存。MyBatis 本身提供的缓存机制不佳。
  5. MyBatis优化

    • MyBatis 可以进行更为细致的 SQL 优化,可以减少查询字段。
    • MyBatis 容易掌握,而 Hibernate 门槛较高。
  6. 应用场景

    MyBatis 适合需求多变的互联网项目,例如电商项目、金融类型、旅游类、售票类项目等。

    Hibernate 适合需求明确、业务固定的项目,例如 OA 项目、ERP 项目和 CRM 项目等。

总结:

总的来说,MyBatis 是一个小巧、方便、高效、简单、直接、半自动化的持久层框架,Hibernate 是一个强大、方便、高效、复杂、间接、全自动化的持久层框架。

对于性能要求不太苛刻的系统,比如管理系统、ERP 等推荐使用 Hibernate,而对于性能要求高、响应快、灵活的系统则推荐使用 MyBatis。

# 参考资料

Last Updated: 9/13/2023, 9:34:21 PM