目录
6.MyBatis配置文件(mybatis-config.xml)
21.MyBatis choose、when和otherwise标签
1.MyBatis是什么
MyBatis 是一个开源、轻量级的数据持久化框架,是 JDBC 和 Hibernate 的替代方案。MyBatis 内部封装了 JDBC,简化了加载驱动、创建连接、创建 statement 等繁杂的过程,开发者只需要关注 SQL 语句本身。
数据持久化是将内存中的数据模型转换为存储模型,以及将存储模型转换为内存中数据模型的统称。例如,文件的存储、数据的读取以及对数据表的增删改查等都是数据持久化操作。
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 官方文档:mybatis – MyBatis 3 | 简介
优点
- MyBatis 是免费且开源的。
- 与 JDBC 相比,减少了 50% 以上的代码量。
- MyBatis 是最简单的持久化框架,小巧并且简单易学。
- MyBatis 相当灵活,不会对应用程序或者数据库的现有设计强加任何影响,SQL 写在 XML 中,和程序逻辑代码分离,降低耦合度,便于同一管理和优化,提高了代码的可重用性。
- 提供 XML 标签,支持编写动态 SQL 语句。
- 提供映射标签,支持对象与数据库的 ORM 字段关系映射。
- 支持存储过程。MyBatis 以存储过程的形式封装 SQL,可以将业务逻辑保留在数据库之外,增强应用程序的可移植性、更易于部署和测试。
缺点
- 编写 SQL 语句工作量较大,对开发人员编写 SQL 语句的功底有一定要求。
- SQL 语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。
使用场景
MyBatis 专注于 SQL 本身,是一个足够灵活的 DAO 层解决方案。适用于性能要求高,且需求变化较多的项目,如互联网项目。
如果您想了解 MyBatis 与 Hibernate 的区别,可阅读学习《MyBatis和Hibernate的区别》一节。
拓展
Mybatis-Plus(简称 MP)是 Mybatis 的增强工具,在 Mybatis 的基础上只做增强不做改变,支持 Mybatis 所有原生的特性,为简化开发、提高效率而生。
2.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。
3.MyBatis下载(多种方式)
在实际开发之前,我们必须为 MyBatis 搭建适当的环境。下面介绍 MyBatis 的下载以及目录结构。
MyBatis下载
在 MyBatis 的官方网站 http://mybatis.org,可以下载到最新版本的 MyBatis,本教程使用版本为 MyBatis 3.5.5。
如果您打不开网站或下载进度较慢,可以通过 Releases · mybatis/mybatis-3 · GitHub 网址下载。
如果您使用的 Maven,那么 pom.xml 文件内容如下(根据自己的版本修改相应的内容)。
- <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>
MyBatis文件目录结构
下载完成后,mybatis-3.5.5.zip 解压后目录结构如下。
上图中的 mybatis-3.5.5.jar 是 MyBatis 的核心包,mybatis-3.5.5.pdf 是 MyBatis 官方使用手册,lib 文件夹下的 jar 文件是 MyBatis 的依赖包,如下图所示。
上图中 jar 文件说明如下。
名称 | 说明 |
---|---|
asm-7.1.jar | 操作Java字节码的类库 |
cglib-3.3.0.jar | 用来动态继承Java类或实现接口 |
commons-logging-1.2.jar | 用于通用日志处理 |
javassist-3.27.0-GA.jar | 分析、编码和创建Java类库 |
log4j-1.2.17.jar | 日志系统 |
log4j-api-2.13.3.jar | log4j到log4j2的桥接包 |
log4j-core-2.13.3.jar | log4j到log4j2的桥接包 |
ognl-3.2.14.jar | OGNL的类库 |
slf4j-api-1.7.30.jar | 日志系统的封装,对外提供统一的API接口 |
slf4j-log4j12-1.7.30.jar | slf4j 对 log4j 的相应驱动,完成 slf4j 绑定 log4j |
在使用 MyBatis 框架时,需要把它的核心包和依赖包引入到应用程序中。如果是 Web 应用,只需将核心包和依赖包复制到 /WEB-INF/lib 目录中。
4.第一个MyBatis程序
在创建 MyBatis 项目之前,首先创建 website 数据表,SQL 语句如下。
- 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;
创建 MyBatis 程序的步骤为:下载jar包 -> 部署jar包 -> 编写MyBatis核心配置文件 -> 创建实体类 -> 创建DAO接口 -> 创建SQL映射文件 -> 编写测试类
下面介绍如何使用 Eclipse IDE 创建一个简单的 MyBatis 程序。
1. 创建Web应用,部署jar包
在 Eclipse 中创建 Web 项目 mybatisDemo,并将下载的 MyBatis 的核心 jar 包、依赖 jar 包以及 MySQL 数据库的驱动 jar 包复制到 /WEB-INF/lib 目录中。下载jar包详细步骤在《MyBatis下载》一节讲解。
2. 创建日志文件
MyBatis 默认使用 log4j 输出日志信息,如果开发者需要查看控制台输出的 SQL 语句,可以在 classpath 路径下配置其日志文件。在 mybatisDemo 的 src 目录下创建 log4j.properties 文件,其内容如下:
# Global logging configuration
log4j.rootLogger=ERROR,stdout
# MyBatis logging configuration...
log4j.logger.net.biancheng=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
在日志文件中配置了全局的日志配置、MyBatis 的日志配置和控制台输出,其中 MyBatis 的日志配置用于将 net.biancheng 包下所有类的日志记录级别设置为 DEBUG。该配置文件内容不需要开发者全部手写,可以从 MyBatis 使用手册中的 Logging 小节复制,然后进行简单修改。
3. 创建持久化类
在 src 目录下创建一个名为 net.biancheng.po 的包,在该包中创建持久化类 Website。注意,在类中声明的属性与数据表 website 的字段一致。
Website 类代码如下。
- package net.biancheng.po;
- 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;
- /*省略setter和getter方法*/
- @Override
- public String toString() {
- return "id" + id + "name" + name + "url" + url + "age" + age + "country" + country + "createtime" + createtime;
- }
- }
4. 创建映射文件
在 src 目录下创建 net.biancheng.mapper 包,在该包下创建映射文件 WebsiteMapper.xml。
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="net.biancheng.mapper.WebsiteMapper">
- <!-- 添加一个网站 -->
- <insert id="addWebsite" parameterType="net.biancheng.po.Website">
- insert into website
- (name,url,age,country)
- values(#{name},#{url},#{age},#{country})
- </insert>
- <!-- 查询所有网站信息 -->
- <select id="selectAllWebsite"
- resultType="net.biancheng.po.Website">
- select * from website
- </select>
- </mapper>
上述代码中,<mapper> 元素是配置文件的根元素,它包含了 namespace 属性,该属性值通常设置为“包名+SQL映射文件名”,用于指定唯一的命名空间。
子元素 <select>、<insert> 中的信息用于执行查询、添加操作。在定义的 SQL 语句中,“#{}”表示一个占位符,相当于“?”,而“#{name}”表示该占位符待接收参数的名称为 name。
5. 创建配置文件
MyBatis 核心配置文件主要用于配置数据库连接和 MyBatis 运行时所需的各种特性,包含了设置和影响 MyBatis 行为的属性。
在 src 目录下创建 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" />
- <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>
- </environment>
- </environments>
- <!-- 将mapper文件加入到配置文件中 -->
- <mappers>
- <mapper resource="net/biancheng/mapper/WebsiteMapper.xml" />
- </mappers>
- </configuration>
上述映射文件和配置文件都不需要读者完全手动编写,都可以从 MyBatis 使用手册中复制,然后做简单修改。
为了方便管理以后各框架集成所需的配置文件,可以在项目工程下新建 Source Folder 类型的 resources 目录,并在此目录下添加 MyBatis 的核心配置文件,默认文件名为 " configuration.xml"。但需要注意的是,为了方便在框架集成时更好地区分各个配置文件,我们一般将此文件名命名“mybatis-config.xml”,该文件用于配置数据库连接信息和 MyBatis 的参数。
6. 创建测试类
在 src 目录下创建一个名为 net.biancheng.test 的包,在该包中创建 MyBatisTest 测试类。在测试类中首先使用输入流读取配置文件,然后根据配置信息构建 SqlSessionFactory 对象。
接下来通过 SqlSessionFactory 对象创建 SqlSession 对象,并使用 SqlSession 对象的方法执行数据库操作。 MyBatisTest 测试类的代码如下:
- package net.biancheng.test;
- import java.io.IOException;
- import java.io.InputStream;
- import java.util.List;
- 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 net.biancheng.po.Website;
- public class Test {
- public static void main(String[] args) throws IOException {
- // 读取配置文件mybatis-config.xml
- InputStream config = Resources.getResourceAsStream("mybatis-config.xml");
- // 根据配置文件构建SqlSessionFactory
- SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(config);
- // 通过SqlSessionFactory创建SqlSession
- SqlSession ss = ssf.openSession();
- // SqlSession执行文件中定义的SQL,并返回映射结果
- // 添加网站
- Website website = new Website();
- website.setName("编程帮");
- website.setUrl("https://www.biancheng.net/");
- website.setAge(21);
- website.setCountry("CN");
- ss.insert("net.biancheng.mapper.WebsiteMapper.addWebsite", website);
- // 查询所有网站
- List<Website> listWeb = ss.selectList("net.biancheng.mapper.WebsiteMapper.selectAllWebsite");
- for (Website site : listWeb) {
- System.out.println(site);
- }
- // 提交事务
- ss.commit();
- // 关闭 SqlSession
- ss.close();
- }
- }
运行结果如下。
DEBUG [main] - ==> Preparing: insert into website (name,url,age,country) values(?,?,?,?)
DEBUG [main] - ==> Parameters: 编程帮(String), https://www.biancheng.net/(String), 21(Integer), CN(String)
DEBUG [main] - <== Updates: 1
DEBUG [main] - ==> Preparing: select * from website
DEBUG [main] - ==> Parameters:
DEBUG [main] - <== Total: 1
Website[id=1,name=编程帮,url=https://www.biancheng.net/,age=21,country=CN,createtime=Tue Feb 23 10:20:40 CST 2021]
5.MyBatis核心对象
MyBatis 有三个基本要素:
- 核心接口和类
- MyBatis核心配置文件(mybatis-config.xml)
- SQL映射文件(mapper.xml)
下面首先介绍 MyBatis 的核心接口和类,如下所示。
每个 MyBatis 应用程序都以一个 SqlSessionFactory 对象的实例为核心。
首先获取 SqlSessionFactoryBuilder 对象,可以根据 XML 配置文件或者 Configuration 类的实例构建该对象。
然后获取 SqlSessionFactory 对象,该对象实例可以通过 SqlSessionFactoryBuilder 对象来获取。
有了 SqlSessionFactory 对象之后,就可以进而获取 SqlSession 实例。SqlSession 对象中完全包含以数据库为背景的所有执行 SQL 操作的方法,用该实例可以直接执行已映射的 SQL 语句。
SqlSessionFactoryBuilder
SqlSessionFactoryBuilder 会根据配置信息或者代码生成 SqlSessionFactory,并且提供了多个 build() 方法重载,如图。
通过源码分析,可以发现以上方法都是在调用同一签名方法,即:
build(Reader reader, String environment, Properties properties)
由于参数 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的生命周期和作用域
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();
- }
SqlSessionFactory的生命周期和作用域
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);
- ...
SqlSession 的用途主要有两种。
- 获取映射器。让映射器通过命名空间和方法名称找到对应的 SQL,并发送给数据库,执行后返回结果。
- 直接通过“命名空间(namespace)+SQL id”的方式执行 SQL,不需要获取映射器。这是 iBatis 版本留下的方式。例如《第一个MyBatis程序》一节的示例就是这种方式执行的 SQL 语句。
SqlSession生命周期和作用域
SqlSession 对应一次数据库会话。由于数据库会话不是永久的,因此 SqlSession 的生命周期也不是永久的,每次访问数据库时都需要创建 SqlSession 对象。
需要注意的是:每个线程都有自己的 SqlSession 实例,SqlSession 实例不能被共享,也不是线程安全的。因此 SqlSession 的作用域范围是 request 作用域或方法体作用域内。
6.MyBatis配置文件(mybatis-config.xml)
在《第一个MyBatis程序》一节中,我们编写了 MyBatis 的配置文件 mybatis-config.xml,在该文件中,您可以配置各种元素。
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>
mybatis-config.xml 文件中的元素节点是有一定顺序的,节点位置必须按以上位置排序,否则会编译错误。
下面介绍 XML 配置文件中的重要元素(标签)。
configuration 元素是整个 XML 配置文件的根节点,其角色就相当于是 MyBatis 的总管,MyBatis 所有的配置信息都会存放在它里面。
properties标签
properties 标签可以通过 resource 属性指定外部 properties 文件(database.properties),也可以通过 properties 子元素配置。
1. 指定文件
使用 properties 指定外部文件,代码如下。
- <properties resource="mybatisDemo/resources/database.properties"/>
database.properties 用于描述数据库连接的相关配置,例如数据库驱动、连接数据库的 url、数据库用户名、数据库密码等。
2. properties子元素配置
通过 properties 子元素 property 配置 username 和 password 变量,然后在 environments 节点中引用这些变量,代码如下。
- <properties>
- <property name="username" value="root"/>
- <property name="password" value="root"/>
- </properties>
在 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>
也可以不使用 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>
typeAliases标签
为了不在任何地方都指定类的全限定名,我们可以使用 typeAliases 标签定义一个别名。
例如,在 net.bianchengbang.po 包中有一个 Student 类,则该类的全限定名称为 net.bianchengbang.po.Student。使用 typeAliases 标签定义别名,这样就不用每次都书写类的全限定名称了,代码如下。
- <typeAliases>
- <typeAlias alias = "Student" type = "net.bianchengbang.po.Student"/>
- </typeAliases>
如果需要对同一个包下的多个类定义别名,则可以定义为:
- <typeAliases>
- <package name="net.biancheng.po"/>
- </typeAliases>
这样 MyBatis 将扫描 net.biancheng.po 包里面的类,将其第一个字母变为小写作为其别名,例如 Student 别名为 student,User 别名为 user。
typeHandlers标签
typeHandlers 主要将获取的值合理地转化为 Java 类型。在 typeHandler 中,分为 jdbcType 和 javaType,其中 jdbcType 用于定义数据库类型,而 javaType 用于定义 Java 类型,typeHandler 的作用就是承担 jdbcType 和 javaType 之间的相互转换。
MyBatis 支持自定义处理类型,在自定义处理类型时,需要实现 org.apache.ibatis.type.TypeHandler 接口或继承 org.apache.ibatis.type.BaseTypeHandle 类。详细可参考官网:mybatis – MyBatis 3 | 配置
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>
mappers标签
mappers 标签用于指定 MyBatis SQL 映射文件的路径。
mapper 是 mappers 的子标签,mapper 中的 resource 属性用于指定 SQL 映射文件的路径(类资源路径)
例如,SQL 映射文件的名称是 Student.xml,它位于名为 net.biancheng.mapper 的包中,那么您可以这样配置:
- <mappers>
- <mapper resource="net/biancheng/mapper/Student.xml"/>
- </mappers>
7.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。下面先定义接口 WebsiteMapper(本节基于《第一个MyBatis程序》中的示例实现)。
- package net.biancheng.mapper;
- import java.util.List;
- import net.biancheng.po.Website;
- public interface WebsiteMapper {
- public List<Website> selectAllWebsite();
- }
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="net.biancheng.mapper.WebsiteMapper">
- <!-- 查询所有网站信息 -->
- <select id="selectAllWebsite"
- resultType="net.biancheng.po.Website">
- select * from website
- </select>
- </mapper>
下面对上述 XML 文件进行讲解。
- namespace 用来定义命名空间,该命名空间和定义接口的全限定名一致。
- <select> 元素表明这是一条查询语句,属性 id 用来标识这条 SQL。resultType 表示返回的是一个 Website 类型的值。
在 MyBatis 配置文件中添加以下代码。
- <mapper resource="net/biancheng/mapper/WebsiteMapper.xml" />
该语句用来引入 XML 文件,MyBatis 会读取 WebsiteMapper.xml 文件,生成映射器。
下面进行测试,用 SqlSession 来获取 Mapper,Test 类代码如下。
- public class Test {
- public static void main(String[] args) throws IOException {
- InputStream config = Resources.getResourceAsStream("mybatis-config.xml");
- SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(config);
- SqlSession ss = ssf.openSession();
- WebsiteMapper websiteMapper = ss.getMapper(WebsiteMapper.class);
- List<Website> websitelist = websiteMapper.selectAllWebsite();
- for (Website site : websitelist) {
- System.out.println(site);
- }
- ss.commit();
- ss.close();
- }
- }
运行结果如下。
DEBUG [main] - ==> Preparing: select * from website
DEBUG [main] - ==> Parameters:
DEBUG [main] - <== Total: 1
Website[id=1,name=编程帮,url=https://www.biancheng.net/,age=21,country=CN,createtime=Tue Feb 23 10:20:40 CST 2021]
注解实现映射器
使用注解的方式实现映射器,只需要在接口中使用 Java 注解,注入 SQL 即可。如下所示。
- package net.biancheng.mapper;
- import java.util.List;
- import org.apache.ibatis.annotations.Select;
- import net.biancheng.po.Website;
- public interface WebsiteMapper2 {
- @Select(value = "select * from website")
- public List<Website> selectAllWebsite();
- }
这里我们使用了 @Select 注解,并且注入了和 XML 中相同的 select 语句。
如果使用注解和 XML 文件两种方式同时定义,那么 XML 方式将覆盖掉注解方式。
虽然这里注解的方式看起来比 XML 简单,但是现实中我们遇到的 SQL 会比该例子复杂得多。如果 SQL 语句中有多个表的关联、多个查询条件、级联、条件分支等,显然这条 SQL 就会复杂的多,所以并不建议读者使用这种方式。比如下面这条 SQL。
- select * from t_user u
- left join t_user_role ur on u.id = ur.user_id
- left join t_role r on ur.role_id = r.id
- left join t_user_info ui on u.id = ui.user_id
- left join t_female_health fh on u.id = fh.user_id
- left join t_male_health mh on u.id = mh.user_id
- where u.user_name like concat('%', ${userName},'%')
- and r.role_name like concat('%', ${roleName},'%')
- and u.sex = 1
- and ui.head_image is not null;
如果把以上 SQL 放到 @Select 注解中,无疑会大大降低代码的可读性。如果同时还要考虑使用动态 SQL 或需要加入其他的逻辑,这样就使得这个注解更加复杂了,不利于日后的维护和修改。
此外,XML 可以相互引入,而注解是不可以的,所以在一些比较复杂的场景下,使用 XML 方式会更加灵活和方便。因此大部分的企业都以 XML 为主,本教程也会保持一致,以 XML 方式来创建映射器。当然在一些简单的表和应用中使用注解方式也会比较简单。
这个接口可以在 XML 中定义,将在 mybatis-config.xml 中配置 XML 的语句修改为以下语句即可。
- <mapper resource="com/mybatis/mapper/WebsiteMapper2" />
也可以使用 configuration 对象注册这个接口,比如:
- configuration.addMapper(WebsiteMapper2.class);
MyBatis 映射器的主要元素
下面介绍在映射器中可以定义哪些元素,以及它们的作用。
元素名称 | 描述 | 备注 |
---|---|---|
mapper | 映射文件的根节点,只有 namescape 一个属性 | namescape 作用如下:
|
select | 查询语句,最常用、最复杂的元素之一 | 可以自定义参数,返回结果集等 |
insert | 插入语句 | 执行后返回一个整数,代表插入的条数 |
update | 更新语句 | 执行后返回一个整数,代表更新的条数 |
delete | 删除语句 | 执行后返回一个整数,代表删除的条数 |
parameterMap | 定义参数映射关系 | 即将被删除的元素,不建议使用 |
sql | 允许定义一部分的 SQL,然后在各个地方引用它 | 例如,一张表列名,我们可以一次定义,在多个 SQL 语句中使用 |
resultMap | 用来描述数据库结果集与对象的对应关系,它是最复杂、最强大的元素 | 提供映射规则 |
cache | 配置给定命名空间的缓存 | - |
cache-ref | 其它命名空间缓存配置的引用 | - |
后面文章中会详细介绍以上元素。
拓展
关于 MyBatis 的 SQL 映射文件中的 mapper 元素的 namescape 属性有如下要求。
- namescape 的命名必须跟某个 DAO 接口同名,同属于 DAO 层,因此代码结构上,映射文件与该接口应放置在同一 package 下(如 net.biancheng.dao.website),并且习惯上是以 Mapper 结尾(如 WebsiteMapper.java、WebsiteMapper.xml)。
- 不同的 mapper 文件中子元素的 id 可以相同,MyBatis 通过 namescape 和子元素的 id 联合区分。接口中的方法与映射文件中的 SQL 语句 id 应一 一对应。
8.MyBatis执行SQL的两种方式
本节主要介绍 MyBatis 执行 SQL 语句的两种方式和它们的区别。
MyBatis 有两种执行 SQL 语句的方式,如下:
- 通过 SqlSession 发送 SQL
- 通过 SqlSession 获取 Mapper 接口,通过 Mapper 接口发送 SQL
SqlSession发送SQL
有映射器之后就可以通过 SqlSession 发送 SQL 了。我们以 getWebsite 这条 SQL 为例看看如何发送 SQL。
- Website website = (Website)sqlSession.selectOne("net.biancheng.mapper.WebsiteMapper.getWebsite",1);
MyBatis 常用的查询方法有 2 种,分别为 selectOne 和 selectList。
1)selectOne
selectOne 方法表示使用查询并且只返回一个对象,必须指定查询条件。只能查询 0 或 1 条记录,大于 1 条记录则运行错误。常用格式如下(也有其它重载方法,根据需要选择)。
- sqlSession.selectOne(String arg0, Object arg1)
2)selectList
selectList 方法表示使用查询并且返回一个列表。可以查询 0 或 N 条记录。常用格式如下。
- sqlSession.selectOne(String arg0)
也可指定参数:
- sqlSession.selectList(String arg0, Object arg1)
以上语法格式中,String 对象由一个命名空间加 SQL id 组合而成,它完全定位了一条 SQL,这样 MyBatis 就会找到对应的 SQL。Object 对象为需要传递的参数,也就是查询条件。
selectOne 实现的 selectList 都可以实现,即 list 中只有一个对象。但 selectList 能实现的,selectOne 不一定能实现。
如果 MyBatis 中只有一个 id 为 getWbsite 的 SQL,那么也可以简写为:
- Website website = (Website )sqlSession.selectOne("getWbsite",1);
这是 MyBatis 前身 iBatis 所留下的方式。
Mapper接口发送 SQL
SqlSession 还可以获取 Mapper 接口,通过 Mapper 接口发送 SQL,如下所示。
- WebsiteMapper websiteMapper = sqlSession.getMapper(WebsiteMapper.class);
- Website website = websiteMapper.getWebsite(1);
通过 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) 语法,只有在运行中才能知道是否会产生错误。
目前使用 Mapper 接口编程已成为主流,尤其在 Spring 中运用 MyBatis 时,Mapper 接口的使用就更为简单,所以本教程使用 Mapper 接口的方式讨论 MyBatis。
9.MyBatis select标签
在 MyBatis 中,select 标签是最常用也是功能最强大的 SQL 语言,用于执行查询操作。
select 示例语句如下。
- <select id="selectAllWebsite" resultType="net.biancheng.po.Website" parameterType="string">
- SELECT id,NAME,url FROM website WHERE NAME LIKE CONCAT ('%',#{name},'%')
- </select>
以上是一个 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);
select标签常用属性
下面介绍 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(双向滚动,及时更新) | - |
传递多个参数
现在需要根据 id 和 name 来模糊查询网站信息,显然这涉及到了两个参数。给映射器传递多个参数分为以下三种方法。
- 使用Map传递参数
- 使用注解传递参数
- 使用JavaBean传递参数
下面分别讲解这 3 种方法的具体实现。
1. 使用Map传递参数
使用 MyBatis 提供的 Map 接口作为参数实现,如下所示。
- <!-- 根据name和url模糊查询网站信息 -->
- <select id="selectWebsiteByMap" resultType="net.biancheng.po.Website" parameterType="map">
- SELECT id,NAME,url FROM website
- WHERE name LIKE CONCAT ('%',#{name},'%')
- AND url LIKE CONCAT ('%',#{url},'%')
- </select>
在 WebsiteMapper 接口中,方法如下。
- public List<Website> selectWebsiteByMap(Map<String, String> params);
测试代码如下。
- Map<String,String> paramsMap = new HashMap<String,String>();
- paramsMap.put("name","编程");
- paramsMap.put("url","biancheng");
- websiteMapper.selectWebsiteByMap(paramsMap);
使用 Map 传递参数虽然简单易用,但是由于这样设置参数需要键值对应,业务关联性不强,开发人员需要深入到程序中看代码,造成可读性下降。
2. 使用注解传递参数
使用 MyBatis 的注解 @Param() 传递参数,如下所示。
- <!-- 根据name和url模糊查询网站信息 -->
- <select id="selectWebsiteByAn" resultType="net.biancheng.po.Website">
- SELECT id,NAME,url FROM website
- WHERE name LIKE CONCAT ('%',#{name},'%')
- AND url LIKE CONCAT ('%',#{url},'%')
- </select>
WebsiteMapper 接口中方法如下。
- public List<Website> selectWebsiteByAn(@Param("name") String name, @Param("url") String url);
当我们把参数传递给后台时,MyBatis 通过 @Param 提供的名称就会知道 #{name} 代表 name 参数,提高了参数可读性。但是如果这条 SQL 拥有 10 个参数的查询,就会造成可读性下降,增强了代码复杂性。
3. 使用JavaBean传递参数
在参数过多的情况下,MyBatis 允许组织一个 JavaBean,通过简单的 setter 和 getter 方法设置参数,提高可读性。如下所示。
- <!-- 根据name和url模糊查询网站信息 -->
- <select id="selectWebsiteByAn" resultType="net.biancheng.po.Website">
- SELECT id,NAME,url FROM website
- WHERE name LIKE CONCAT ('%',#{name},'%')
- AND url LIKE CONCAT ('%',#{url},'%')
- </select>
WebsiteMapper 接口中方法如下。
- public List<Website> selectWebsiteByAn(Website website);
这就是通过 JavaBean 传递多个参数的方式。
区别
以上 3 种方式的区别如下。
- 使用 Map 传递参数会导致业务可读性的丧失,继而导致后续扩展和维护的困难,所以在实际应用中我们应该果断废弃该方式。
- 使用 @Param 注解传递参数会受到参数个数的影响。当 n≤5 时,它是最佳的传参方式,因为它更加直观;当 n>5 时,多个参数将给调用带来困难。
- 当参数个数大于 5 个时,建议使用 JavaBean 方式。
10.MyBatis insert标签
MyBatis insert 标签用来定义插入语句,执行插入操作。当 MyBatis 执行完一条插入语句后,就会返回其影响数据库的行数。
下面通过一个示例演示 insert 标签的具体用法。
1. 修改 WebsiteMapper.xml,增加插入语句,代码如下。
- <!-- 增加网站信息 -->
- <insert id="addWebsite" parameterType="string">
- insert into website(name)
- values(#{name})
- </insert>
2. 在 WebsiteMapper 接口中定义一个 add() 方法,代码如下。
- public int addWebsite(String name);
参数为 Sting 类型的字符串;返回值为 int 类型,即执行 SQL 后,插入记录的行数。
3. 测试代码如下。
- //插入 name 为编程帮4 的记录
- String name = "编程帮4";
- int i = websiteMapper.addWebsite(name);
- System.out.println("共插入了 " + i + " 条记录");
4. 执行测试代码,控制台输出如下。
共插入了 1 条记录
insert 标签常用属性
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 属性,只有查询操作才需要对返回结果类型进行相应的指定。
传递多个参数
在上面的示例中,我们在插入语句中只使用了一个 String 类型的参数,而在实际的开发过程中,我们的插入语句往往需要使用多个参数,Mybatis 为我们提供以下 3 种方式,来实现给映射器传递多个参数:
- 使用 Map 传递参数
- 使用注解传递参数
- 使用 JavaBean 传递参数
使用 Map 传递参数
我们可以将参数封装到一个 Map 对象中,然后传递给 MyBatis 的映射器,示例如下。
1. 在 WebsiteMapper 接口中,定义一个 addWebsiteByMap() 方法,并使用 Map 传递参数,方法如下。
- int addWebsiteByMap(Map<String, String> params);
2. 在 WebsiteMapper.xml 中,使用 insert 标签定义一条插入语句,并接收通过 Map 传递的参数,配置如下。
- <!--接收 Map 参数-->
- <insert id="addWebsiteByMap" parameterType="map">
- insert into Website (name, url) values (#{name}, #{url})
- </insert>
3. 测试代码如下。
- Map<String, String> params = new HashMap<>();
- params.put("name", "编程帮");
- params.put("url", "https://www.biancheng.net/");
- int i = websiteMapper.addWebsiteByMap(params);
- System.out.println("通过 Map 成功向数据库中添加了 " + i + " 条记录");
4. 执行测试代码,控制台输出如下。
通过 Map 成功向数据库中添加了 1 条记录
使用注解传递参数
我们还可以使用 MyBatis 提供的 @Param 注解给注解器传递参数,示例代码如下。
1. 在 WebsiteMapper 接口中,定义一个 addWebsiteByParam() 方法,并使用 @Param 注解传递参数,方法如下
- int addWebsiteByParam(@Param("name") String name, @Param("url") String url);
2. 在 WebsiteMapper.xml 中使用 insert 标签定义一条插入语句,并接收通过 @Param 注解传递的参数,配置如下。
- <!--接收 @Param 注解传递的参数-->
- <insert id="addWebsiteByParam">
- insert into Website (name, url) values (#{name}, #{url})
- </insert>
3. 测试代码如下。
- int i = websiteMapper.addWebsiteByParam("bianchengbang", "www.biancheng.net");
- System.out.println("通过 @Param 注解成功向数据库中添加了 " + i + " 条记录");
4. 执行测试代码,控制台输出如下。
通过 @Param 注解成功向数据库中添加了 1 条记录
使用 JavaBean 传递参数
在参数过多的情况下,我们可以将参数通过 setter 方法封装到 JavaBean(实体类)对象中 ,传递给映射器。
1. 在 WebsiteMapper 接口中,定义一个 addWebsiteByJavaBean() 方法,并使用 JavaBean 传递参数,方法如下。
- int addWebsiteByJavaBean(Website website);
2. 在 WebsiteMapper.xml 中使用 insert 标签定义一条插入语句,并接收通过 JavaBean 传递的参数,配置如下。
- <!--接收通过 JavaBean 传递的参数-->
- <insert id="addWebsiteByJavaBean" parameterType="net.biancheng.www.po.Website">
- insert into Website (name, url) values (#{name}, #{url})
- </insert>
3. 测试代码如下。
- //创建 JavaBean 对象
- Website website = new Website();
- //通过 setter 方法将参数封装
- website.setName("编程帮 JavaBean");
- website.setUrl("www.biancheng.net");
- int i = websiteMapper.addWebsiteByJavaBean(website);
- System.out.println("通过 JavaBean 成功向数据库中添加了 " + i + " 条记录");
4. 执行测试代码,控制台输出如下。
通过 JavaBean 成功向数据库中添加了 1 条记录
区别
以上 3 种方式的区别如下:
- 使用 Map 传递参数会导致业务可读性的丧失,继而导致后续扩展和维护的困难,所以在实际应用中我们应该果断废弃该方式。
- 使用 @Param 注解传递参数会受到参数个数的影响。当 n≤5 时,它是最佳的传参方式,因为它更加直观;当 n>5 时,多个参数将给调用带来困难。
- 当参数个数大于 5 个时,建议使用 JavaBean 方式。
主键(自动递增)回填
我们知道,MySQL、SQL Server 等数据库表可以采用自动递增的字段作为其主键,当向这样的数据库表插入数据时,即使不指定自增主键的值,数据库也会根据自增规则自动生成主键并插入到表中。
一些特殊情况下,我们可能需要将这个刚刚生成的主键回填到请求对象(原本不包含主键信息的请求对象)中,供其他业务使用。此时,我们就可以通过在 insert 标签中添加 keyProperty 和 useGeneratedKeys 属性,来实现该功能。
下面我们通过一个示例,来演示主键(自动递增)回填功能。
1. 为 WebsiteMapper.xml 中 id 为 addWebsite 的 insert 标签添加 keyProperty 和 useGeneratedKeys 属性,具体代码如下:
- <!--添加一个网站信息,成功后将主键值返回填给id(po的属性)-->
- <insert id="addWebsite" parameterType="net.biancheng.po.Website" keyProperty="id" useGeneratedKeys="true">
- insert into Website (name,url) values(#{name},#{url})
- </insert>
2. 测试代码如下:
- // 添加一个网站信息
- Website addsite = new Website();
- //插入的对象中不包含主键 id
- addsite.setName("编程帮");
- addsite.setUrl("https://www.biancheng.net/");
- //执行插入
- int num = websiteMapper.addWebsite(addsite);
- System.out.println("添加了 " + num + " 条记录");
- //获取回填的主键
- System.out.println("添加记录的主键是:" + addsite.getId());
3. 执行测试代码,控制台输出如下。
添加了 1 条记录 添加记录的主键是:3
自定义主键
如果在实际项目中,若数据库不支持主键自动递增(例如 Oracle),或者取消了主键自动递增的规则,我们可以使用 MyBatis 的 <selectKey> 标签自定义生成主键,具体配置代码如下。
- <!-- 添加一个网站,#{name}为 net.biancheng.po.Website 的属性值 -->
- <insert id="insertWebsite" parameterType="net.biancheng.po.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>
<selectKey> 标签中属性说明如下:
- keyProperty:用于指定主键值对应的 PO 类的属性。
- order:该属性取值可以为 BEFORE 或 AFTER。BEFORE 表示先执行 <selectKey> 标签内的语句,再执行插入语句;AFTER 表示先执行插入语句再执行 <selectKey> 标签内的语句。
11.MyBatis update标签
MyBatis update 标签用于定义更新语句,执行更新操作。当 MyBatis 执行完一条更新语句后,会返回一个整数,表示受影响的数据库记录的行数。
下面我们通过一个示例演示 update 标签的用法。
1. 在 WebsiteMapper.xml 中添加以下更新语句,代码如下。
- <!--update 标签-->
- <update id="updateWebsite" parameterType="string">
- update website set name = #{name}
- </update>
2. 在 WebsiteMapper 接口中增加一个 updateWebsite() 方法,代码如下。
- int updateWebsite(String name);
参数为 String 类型的字符串;返回值为 int 类型,表示执行 sql 语句后受影响的记录的行数。
3. 测试代码如下。
- int i = websiteMapper.updateWebsite("C语言中文网");
- System.out.println("共更新了 " + i + " 条记录");
4.执行测试代码,控制台输出如下。
共更新了 9 条记录
通过控制台输出可知,该更新语句执行后,共影响了 9 条数据库记录。
update 标签常用属性
update 标签常用属性如下表。
属性名称 | 描述 | 备注 |
---|---|---|
id | 它和 Mapper 的命名空间组合起来使用,是唯一标识符,供 MyBatis 调用 | 如果命名空间+ id 不唯一,那么 MyBatis 抛出异常 |
parameterType | 传入 SQL 语句的参数类型的全限定名或别名,它是一个可选属性。 | 支持基本数据类型和 JavaBean、Map 等复杂数据类型 |
flushCache | 该属性用于设置执行该操作后,是否会清空二级缓存和本地缓存,默认值为 true。 | - |
timeout | 该属性用于设置 SQL 执行的超时时间,如果超时,就抛异常。 | - |
statementType | 执行 SQL 时使用的 statement 类型, 默认为 PREPARED,可选值:STATEMENT,PREPARED 和 CALLABLE。 | - |
注意:update 标签中没有 resultType 属性,只有查询操作才需要对返回结果类型进行相应的指定。
传递多个参数
在上面的示例中,我们在更新语句中只使用了一个 String 类型的参数,而在实际的开发过程中,大多数时候,我们的更新语句都需要使用多个参数,Mybatis 为我们提供以下 3 种方式,来实现给映射器传递多个参数:
- 使用 Map 传递参数
- 使用注解传递参数
- 使用 JavaBean 传递参数
使用 Map 传递参数
我们可以将参数封装到一个 Map 对象中,然后传递给 MyBatis 的映射器,示例如下。
1. 在 WebsiteMapper 接口中,定义一个 updateWebsiteByMap() 方法,并使用 Map 传递参数,代码如下。
- int updateWebsiteByMap(Map<String, Object> params);
2. 在 WebsiteMapper.xml 使用 update 标签定义一个 update 语句,并接收通过 Map 传递的参数,代码如下。
- <!--更新语句接收 Map 传递的参数-->
- <update id="updateWebsiteByMap" parameterType="map">
- update website set name = #{name},url= #{url} where id = #{id}
- </update>
3. 测试代码如下。
- //使用 Map 向 update 标签传递参数
- Map<String, Object> params = new HashMap<>();
- params.put("id", 1);
- params.put("name", "编程帮");
- params.put("url", "www.biancheng.net");
- int i = websiteMapper.updateWebsiteByMap(params);
- System.out.println("通过 Map 传递参数,共更新了 " + i + " 条记录");
4. 执行测试代码,控制台输出如下。
通过 Map 传递参数,共更新了 1 条记录
使用注解传递参数
我们还可以使用 MyBatis 提供的 @Param 注解给注解器传递参数,示例如下。
1. 在 WebsiteMapper 接口中,定义一个 updateWebsiteByParam() 方法,并使用 @Param 注解传递参数,方法如下。
- int updateWebsiteByParam(@Param("name") String name, @Param("url") String url, @Param("id") Integer id);
2. 在 WebsiteMapper.xml 中使用 update 标签定义一个 update 语句,并接收通过 @Param 注解传递的参数,配置如下。
- <!--更新语句接收 @Param 注解传递的参数-->
- <update id="updateWebsiteByParam">
- update website set name = #{name},url= #{url} where id = #{id}
- </update>
3. 测试代码如下。
- // 使用@Param 注解传递参数到更新语句中
- String name = "编程帮2";
- String url = "www.biancheng.net";
- Integer id = 2;
- int i = websiteMapper.updateWebsiteByParam(name, url, id);
- System.out.println("通过 @Param 注解传递参数,共更新了 " + i + " 条记录");
4. 执行测试代码,控制台输出如下。
通过 @Param 注解传递参数,共更新了 1 条记录
使用 JavaBean 传递参数
在参数过多的情况下,我们还可以将参数通过 setter 方法封装到 JavaBean(实体类)对象中传递给映射器。
1. 在 WebsiteMapper 接口中,定义一个 updateWebsiteByJavaBean() 方法,并使用 JavaBean 传递参数,方法如下。
- int updateWebsiteByJavaBean(Website website);
2. 在 WebsiteMapper.xml 中使用 update 标签定义一个 update 语句,并接收通过 JavaBean 传递的参数,配置如下。
- <!--更新语句接收 JavaBean 传递的参数-->
- <update id="updateWebsiteByJavaBean" parameterType="net.biancheng.www.po.Website">
- update website set name = #{name},url= #{url} where id = #{id}
- </update>
3. 测试代码如下。
- //使用 JavaBean 传递参数到更新语句中
- Website website = new Website();
- website.setName("编程帮3");
- website.setUrl("www.biancheng.net");
- website.setId(3);
- int i = websiteMapper.updateWebsiteByJavaBean(website);
- System.out.println("通过 JavaBean 传递参数,共更新了 " + i + " 条记录");
4. 执行测试代码,控制台输出如下。
通过 JavaBean 传递参数,共更新了 1 条记录
区别
以上 3 种方式的区别如下:
- 使用 Map 传递参数会导致业务可读性的丧失,继而导致后续扩展和维护的困难,所以在实际应用中我们应该果断废弃该方式。
- 使用 @Param 注解传递参数会受到参数个数的影响。当 n≤5 时,它是最佳的传参方式,因为它更加直观;当 n>5 时,多个参数将给调用带来困难。
- 当参数个数大于 5 个时,建议使用 JavaBean 方式。
12.MyBatis delete标签
MyBatis delete 标签用于定义 delete 语句,执行删除操作。当 MyBatis 执行完一条更新语句后,会返回一个整数,表示受影响的数据库记录的行数。
下面我们通过一个示例演示 delete 标签的用法。
1. 在 WebsiteMapper.xml 中使用 delete 标签添加一条 delete 语句,代码如下。
- <delete id="deleteWebsite" parameterType="string">
- delete from website where name = #{name}
- </delete>
2. 在 WebsiteMapper 接口中增加一个 deleteWebsite() 方法,代码如下。
- int deleteWebsite(String name);
参数为 String 类型的字符串;返回值为 int 类型,表示执行 sql 语句后,被删除记录的行数。
3. 测试代码如下。
- //删除 name 为编程帮3 的记录
- String name = "编程帮3";
- int i = websiteMapper.deleteWebsite(name);
- System.out.println("共删除了 " + i + " 条记录");
4. 执行测试代码,控制台输出如下。
共删除了 3 条记录
delete 标签常用属性
delete 标签常用属性如下表。
属性名称 | 描述 | 备注 |
---|---|---|
id | 它和 Mapper 的命名空间组合起来使用,是唯一标识符,供 MyBatis 调用 | 如果命名空间+ id 不唯一,那么 MyBatis 抛出异常 |
parameterType | 传入 SQL 语句的参数类型的全限定名或别名,它是一个可选属性。 | 支持基本数据类型和 JavaBean、Map 等复杂数据类型 |
flushCache | 该属性用于设置执行该操作后,是否会清空二级缓存和本地缓存,默认值为 true。 | - |
timeout | 该属性用于设置 SQL 执行的超时时间,如果超时,就抛异常。 | - |
statementType | 执行 SQL 时使用的 statement 类型, 默认为 PREPARED,可选值:STATEMENT,PREPARED 和 CALLABLE。 | - |
注意:delete 标签中没有 resultType 属性,只有查询操作才需要对返回结果类型进行相应的指定。
传递多个参数
在上面的示例中,我们在 delete 语句中只使用了一个 String 类型的参数,而在实际的开发过程中,我们的 delete 语句往往需要使用多个参数对记录进行筛选,Mybatis 为我们提供以下 3 种方式,来实现给映射器传递多个参数:
- 使用 Map 传递参数
- 使用注解传递参数
- 使用 JavaBean 传递参数
使用 Map 传递参数
我们可以将参数封装到一个 Map 对象中,然后传递给 MyBatis 的映射器,示例如下。
1. 在 WebsiteMapper 接口中,定义一个 deleteWebsiteByMap() 方法,并使用 Map 传递参数,代码如下。
- int deleteWebsiteByMap(Map<String, Object> params);
2.在 WebsiteMapper.xml 中使用 delete 标签定义一个 delete 语句,并接收通过 Map 传递的参数,配置如下。
- <!--通过 Map 传递参数,执行删除操作-->
- <delete id="deleteWebsiteByMap" parameterType="map">
- delete
- from website
- where name = #{name}
- and url = #{url}
- </delete>
3. 测试代码如下。
- //使用 Map 向 delete 标签传递参数
- Map<String, Object> params = new HashMap<>();
- params.put("name", "编程帮");
- params.put("url", "www.biancheng.net");
- int i = websiteMapper.deleteWebsiteByMap(params);
- System.out.println("通过 Map 传递参数,共删除了 " + i + " 条记录");
4. 执行测试代码,控制台输出如下。
通过 Map 传递参数,共删除了 1 条记录
使用注解传递参数
我们还可以使用 MyBatis 提供的 @Param 注解给注解器传递参数,示例如下。
1. 在 WebsiteMapper 接口中,定义一个 deleteWebsiteByParam() 方法,并使用 @Param 注解传递参数,方法如下
- int deleteWebsiteByParam(@Param("name") String name, @Param("url") String url);
2. 在 WebsiteMapper.xml 中使用 delete 标签定义一个 delete 语句,并接收通过 @Param 注解传递的参数,配置如下。
- <!--通过 @Param 注解传递参数,执行删除操作-->
- <delete id="deleteWebsiteByParam">
- delete
- from website
- where name = #{name}
- and url = #{url}
- </delete>
3. 测试代码如下。
- //使用 @Param 注解传递参数
- String name = "编程帮2";
- String url = "www.biancheng.net";
- int i = websiteMapper.deleteWebsiteByParam(name, url);
- System.out.println("通过 @Param 注解传递参数,共删除了 " + i + " 条记录");
4. 执行测试代码,控制台输出如下。
通过 @Param 注解传递参数,共删除了 1 条记录
使用 JavaBean 传递参数
在参数过多的情况下,我们还可以将参数通过 setter 方法封装到 JavaBean(实体类)对象中传递给映射器。
1. 在 WebsiteMapper 接口中,定义一个 deleteWebsiteByJavaBean() 方法,并使用 JavaBean 传递参数,方法如下。
- int deleteWebsiteByJavaBean(Website website);
2. 在 WebsiteMapper.xml 中使用 delete 标签定义一个 delete 语句,并接收通过 JavaBean 传递的参数,配置如下。
- <!--通过 JavaBean 传递参数,执行删除操作-->
- <delete id="deleteWebsiteByJavaBean" parameterType="net.biancheng.www.po.Website">
- delete
- from website
- where name = #{name}
- and url = #{url}
- </delete>
3. 测试代码如下。
- //使用 JavaBean 传递参数到更新语句中
- Website website = new Website();
- website.setName("C语言中文网");
- website.setUrl("https://www.biancheng.net/");
- int i = websiteMapper.deleteWebsiteByJavaBean(website);
- System.out.println("通过 JavaBean 传递参数,共删除了 " + i + " 条记录");
4. 执行测试代码,控制台输出如下。
通过 JavaBean 传递参数,共删除了 3 条记录
区别
以上 3 种方式的区别如下:
- 使用 Map 传递参数会导致业务可读性的丧失,继而导致后续扩展和维护的困难,所以在实际应用中我们应该果断废弃该方式。
- 使用 @Param 注解传递参数会受到参数个数的影响。当 n≤5 时,它是最佳的传参方式,因为它更加直观;当 n>5 时,多个参数将给调用带来困难。
- 当参数个数大于 5 个时,建议使用 JavaBean 方式。
13.MyBatis resultMap元素
resultMap 是 MyBatis 中最复杂的元素,主要用于解决实体类属性名与数据库表中字段名不一致的情况,可以将查询结果映射成实体对象。下面我们先从最简单的功能开始介绍。
现有的 MyBatis 版本只支持 resultMap 查询,不支持更新或者保存,更不必说级联的更新、删除和修改。
resultMap元素的构成
resultMap 元素还可以包含以下子元素,代码如下。
- <resultMap id="" type="">
- <constructor><!-- 类再实例化时用来注入结果到构造方法 -->
- <idArg/><!-- ID参数,结果为ID -->
- <arg/><!-- 注入到构造方法的一个普通结果 -->
- </constructor>
- <id/><!-- 用于表示哪个列是主键 -->
- <result/><!-- 注入到字段或JavaBean属性的普通结果 -->
- <association property=""/><!-- 用于一对一关联 -->
- <collection property=""/><!-- 用于一对多、多对多关联 -->
- <discriminator javaType=""><!-- 使用结果值来决定使用哪个结果映射 -->
- <case value=""/><!-- 基于某些值的结果映射 -->
- </discriminator>
- </resultMap>
其中:
- <resultMap> 元素的 type 属性表示需要的 POJO,id 属性是 resultMap 的唯一标识。
- 子元素 <constructor> 用于配置构造方法。当一个 POJO 没有无参数构造方法时使用。
- 子元素 <id> 用于表示哪个列是主键。允许多个主键,多个主键称为联合主键。
- 子元素 <result> 用于表示 POJO 和 SQL 列名的映射关系。
- 子元素 <association>、<collection> 和 <discriminator> 用在级联的情况下。关于级联的问题比较复杂,在《MyBatis一对一关联查询》和《MyBatis一对多关联查询》一节详细讲解。
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>
在 WebsiteMapper 接口中添加以下方法。
- public List<Map<String,Object>> selectAllWebsite();
Map 的 key 是 select 语句查询的字段名(必须完全一样),而 Map 的 value 是查询返回结果中字段对应的值,一条记录映射到一个 Map 对象中。
使用 Map 存储结果集很方便,但可读性稍差,所以一般推荐使用 POJO 的方式。
使用POJO存储结果集
因为 MyBatis 提供了自动映射,所以使用 POJO 存储结果集是最常用的方式。但有时候需要更加复杂的映射或级联,这时就需要使用 select 元素的 resultMap 属性配置映射集合。
修改 Website 类,代码如下。
- package net.biancheng.po;
- 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 + "]";
- }
- }
WebsiteMapper.xml 代码如下。
- <!--使用自定义结果集类型 -->
- <resultMap type="net.biancheng.po.Website" id="myResult">
- <!-- property 是 net.biancheng.po.Website 类中的属性 -->
- <!-- column是查询结果的列名,可以来自不同的表 -->
- <id property="id" column="id" />
- <result property="uname" column="name" />
- </resultMap>
resultMap 元素的属性 id 代表这个 resultMap 的标识,type 标识需要映射的 POJO。我们可以使用 MyBatis 定义好的类的别名或自定义类的全限定名。
这里使用 property 元素指定 Website 的属性名称 uname,column 表示数据库中 website 表的 SQL 列名 name,将 POJO 和 SQL 的查询结果一 一对应。
WebsiteMapper.xml 中 select 元素配置代码如下。
- <select id="selectAllWebsite" resultMap="myResult">
- select id,name,url from website
- </select>
可以发现 SQL 语句的列名和 myResult 中的 column 一一对应。
resultType和resultMap的区别
MyBatis 的每一个查询映射的返回类型都是 resultMap,只是当我们提供的返回类型是 resultType 时,MyBatis 会自动把对应的值赋给 resultType 所指定对象的属性,而当我们提供的返回类型是 resultMap 时,MyBatis 会将数据库中的列数据复制到对象的相应属性上,可用于复制查询。
需要注意的是,resultMap 和 resultType 不能同时使用。
14.MyBatis注解(3种类型)
为了简化 XML 的配置,MyBatis 提供了注解。我们可以通过 MyBatis 的 jar 包查看注解,如下图所示。
以上注解主要分为三大类,即 SQL 语句映射、结果集映射和关系映射。下面分别进行讲解。
1. SQL 语句映射
1)@Insert:实现新增功能
- @Insert("insert into user(id,name) values(#{id},#{name})")
- public int insert(User user);
2)@Select:实现查询功能
- @Select("Select * from user")
- @Results({
- @Result(id = true, column = "id", property = "id"),
- @Result(column = "name", property = "name"),
- @Result(column = "sex", property = "sex"),
- @Result(column = "age", property = "age")
- })
- List<User> queryAllUser();
3)@SelectKey:插入后,获取id的值
以 MySQL 为例,MySQL 在插入一条数据后,使用 select last_insert_id() 可以获取到自增 id 的值。
- @Insert("insert into user(id,name) values(#{id},#{name})")
- @SelectKey(statement = "select last_insert_id()", keyProperty = "id", keyColumn = "id", resultType = int,before = false)
- public int insert(User user);
@SelectKey 各个属性含义如下。
- statement:表示要运行的 SQL 语句;
- keyProperty:可选项,表示将查询结果赋值给代码中的哪个对象;
- keyColumn:可选项,表示将查询结果赋值给数据表中的哪一列;
- resultType:指定 SQL 语句的返回值;
- before:默认值为 true,在执行插入语句之前,执行 select last_insert_id()。值为 flase,则在执行插入语句之后,执行 select last_insert_id()。
4)@Insert:实现插入功能
- @Insert("insert into user(name,sex,age) values(#{name},#{sex},#{age}")
- int saveUser(User user);
5)@Update:实现更新功能
- @Update("update user set name= #{name},sex = #{sex},age =#{age} where id = #{id}")
- void updateUserById(User user);
6)@Delete:实现删除功能
- @Delete("delete from user where id =#{id}")
- void deleteById(Integer id);
7)@Param:映射多个参数
@Param 用于在 Mapper 接口中映射多个参数。
- int saveUser(@Param(value="user") User user,@Param("name") String name,@Param("age") Int age);
@Param 中的 value 属性可省略,用于指定参数的别名。
2. 结果集映射
@Result、@Results、@ResultMap 是结果集映射的三大注解。
声明结果集映射关系代码:
- @Select({"select id, name, class_id from student"})
- @Results(id="studentMap", value={
- @Result(column="id", property="id", jdbcType=JdbcType.INTEGER, id=true),
- @Result(column="name", property="name", jdbcType=JdbcType.VARCHAR),
- @Result(column="class_id ", property="classId", jdbcType=JdbcType.INTEGER)
- })
- List<Student> selectAll();
下面为 @Results 各个属性的含义。
- id:表示当前结果集声明的唯一标识;
- value:表示结果集映射关系;
- @Result:代表一个字段的映射关系。其中,column 指定数据库字段的名称,property 指定实体类属性的名称,jdbcType 数据库字段类型,id 为 true 表示主键,默认 false。
可使用 @ResultMap 来引用映射结果集,其中 value 可省略。
- @Select({"select id, name, class_id from student where id = #{id}"})
- @ResultMap(value="studentMap")
- Student selectById(Integer id);
这样不需要每次声明结果集映射时都复制冗余代码,简化开发,提高了代码的复用性。
3. 关系映射
1)@one:用于一对一关系映射
- @Select("select * from student")
- @Results({
- @Result(id=true,property="id",column="id"),
- @Result(property="name",column="name"),
- @Result(property="age",column="age"),
- @Result(property="address",column="address_id",one=@One(select="net.biancheng.mapper.AddressMapper.getAddress"))
- })
- public List<Student> getAllStudents();
2)@many:用于一对多关系映射
纯文本复制
- @Select("select * from t_class where id=#{id}")
- @Results({
- @Result(id=true,column="id",property="id"),
- @Result(column="class_name",property="className"),
- @Result(property="students", column="id", many=@Many(select="net.biancheng.mapper.StudentMapper.getStudentsByClassId"))
- })
- public Class getClass(int id);
15.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}
在级联中存在 3 种对应关系。
- 一对多的关系,如角色和用户的关系。通俗的理解就是,一家软件公司会存在许多软件工程师,公司和软件工程师就是一对多的关系。
- 一对一的关系。每个软件工程师都有一个编号(ID),这是他在公司的标识,它与工程师是一对一的关系。
- 多对多的关系,有些公司一个角色可以对应多个用户,但是一个用户可以兼任多个角色。通俗的说,一个人既可以是总经理,同时也是技术总监,而技术总监这个职位可以对应多个人,这就是多对多的关系。
实际应用中,由于多对多的关系比较复杂,会增加理解和关联的复杂度,所以应用较少。推荐的方法是,用一对多的关系把它分解为双向关系,以降低关系的复杂度,简化程序。具体操作方法可以参考《MyBatis多对多关联查询》一节。
级联的优点是获取关联数据十分便捷。但是级联过多会增加系统的复杂度,同时降低系统的性能,此增彼减。所以记录超过 3 层时,就不要考虑使用级联了,因为这样会造成多个对象的关联,导致系统的耦合、负载和难以维护。
本教程分为 3 节对 MyBatis 的级联关系进行详细讲解,小伙伴们请点击下方链接阅读学习。
16.MyBatis一对一关联查询
一对一级联关系在现实生活中是十分常见的,例如一个大学生只有一个学号,一个学号只属于一个学生。同样,人与身份证也是一对一的级联关系。
在 MyBatis 中,通过 <resultMap> 元素的子元素 <association> 处理一对一级联关系。示例代码如下。
- <association property="studentCard" column="cardId"
- javaType="net.biancheng.po.StudentCard"
- select="net.biancheng.mapper.StudentCardMapper.selectStuCardById" />
在 <association> 元素中通常使用以下属性。
- property:指定映射到实体类的对象属性。
- column:指定表中对应的字段(即查询返回的列名)。
- javaType:指定映射到实体对象属性的类型。
- select:指定引入嵌套查询的子 SQL 语句,该属性用于关联映射中的嵌套查询。
一对一关联查询可采用以下两种方式:
- 单步查询,通过关联查询实现
- 分步查询,通过两次或多次查询,为一对一关系的实体 Bean 赋值
示例
下面以学生和学号为例讲解一对一关联查询的处理过程。
1)创建数据表
创建 student(学生)和 studentcard(学号)数据表,SQL 语句如下。
- 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,'C语言中文网',0,1),(2,'编程帮',0,2),(3,'赵小红',1,3),(4,'李晓明',0,4),(5,'李紫薇',1,5),(6,'钱百百',0,NULL);
- DROP TABLE IF EXISTS `studentcard`;
- 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');
2)创建持久化类
在 myBatisDemo 应用的 net.biancheng.po 包下创建数据表对应的持久化类 Student 和 StudentCard。
Student 的代码如下:
- package net.biancheng.po;
- 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 + "]";
- }
- }
StudentCard 的代码如下:
- package net.biancheng.po;
- 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 + "]";
- }
- }
分步查询
新建 StudentCardMapper 类,代码如下。
- package net.biancheng.mapper;
- import net.biancheng.po.StudentCard;
- public interface StudentCardMapper {
- public StudentCard selectStuCardById(int id);
- }
StudentCardMapper.xml 对应映射 SQL 语句代码如下。
- <mapper namespace="net.biancheng.mapper.StudentCardMapper">
- <select id="selectStuCardById"
- resultType="net.biancheng.po.StudentCard">
- SELECT * FROM studentCard WHERE id = #{id}
- </select>
- </mapper>
StudentMapper 类方法代码如下。
- package net.biancheng.mapper;
- import net.biancheng.po.Student;
- public interface StudentMapper {
- public Student selectStuById1(int id);
- public Student selectStuById2(int id);
- }
StudentMapper.xml 代码如下。
- <mapper namespace="net.biancheng.mapper.StudentMapper">
- <!-- 一对一根据id查询学生信息:级联查询的第一种方法(嵌套查询,执行两个SQL语句) -->
- <resultMap type="net.biancheng.po.Student" id="cardAndStu1">
- <id property="id" column="id" />
- <result property="name" column="name" />
- <result property="sex" column="sex" />
- <!-- 一对一级联查询 -->
- <association property="studentCard" column="cardId"
- javaType="net.biancheng.po.StudentCard"
- select="net.biancheng.mapper.StudentCardMapper.selectStuCardById" />
- </resultMap>
- <select id="selectStuById1" parameterType="Integer"
- resultMap="cardAndStu1">
- select * from student where id=#{id}
- </select>
- </mapper>
测试代码如下。
- public class Test {
- public static void main(String[] args) throws IOException {
- InputStream config = Resources.getResourceAsStream("mybatis-config.xml");
- SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(config);
- SqlSession ss = ssf.openSession();
- Student stu = ss.getMapper(StudentMapper.class).selectStuById1(2);
- System.out.println(stu);
- }
- }
运行结果如下。
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=编程帮, sex=0, studentCard=StudentCard [id=2, studentId=20200314]]
单步查询
在 StudentMapper.xml 中添加以下代码。
- <resultMap type="net.biancheng.po.Student" id="cardAndStu2">
- <id property="id" column="id" />
- <result property="name" column="name" />
- <result property="sex" column="sex" />
- <!-- 一对一级联查询 -->
- <association property="studentCard"
- javaType="net.biancheng.po.StudentCard">
- <id property="id" column="id" />
- <result property="studentId" column="studentId" />
- </association>
- </resultMap>
- <select id="selectStuById2" parameterType="Integer"
- resultMap="cardAndStu2">
- SELECT s.*,sc.studentId FROM student s,studentCard sc
- WHERE
- s.cardId = sc.id AND s.id=#{id}
- </select>
在 StudentMapper 中添加以下方法。
- public Student selectStuById2(int id);
运行结果如下。
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=编程帮, sex=0, studentCard=StudentCard [id=2, studentId=20200314]]
17.MyBatis一对多关联查询
在《MyBatis一对一关联查询》一节中介绍了 MyBatis 如何处理一对一级联关系。但在实际生活中也有许多一对多级联关系,例如一个用户可以有多个订单,而一个订单只属于一个用户。同样,国家和城市也属于一对多级联关系。
在 MyBatis 中,通过 <resultMap> 元素的子元素 <collection> 处理一对多级联关系,collection 可以将关联查询的多条记录映射到一个 list 集合属性中。示例代码如下。
- <collection property="orderList"
- ofType="net.biancheng.po.Order" column="id"
- select="net.biancheng.mapper.OrderMapper.selectOrderById" />
在 <collection> 元素中通常使用以下属性。
- property:指定映射到实体类的对象属性。
- column:指定表中对应的字段(即查询返回的列名)。
- javaType:指定映射到实体对象属性的类型。
- select:指定引入嵌套查询的子 SQL 语句,该属性用于关联映射中的嵌套查询。
一对多关联查询可采用以下两种方式:
- 分步查询,通过两次或多次查询,为一对多关系的实体 Bean 赋值
- 单步查询,通过关联查询实现
示例
下面以用户和订单为例讲解一对多关联查询(实现“根据 id 查询用户及其关联的订单信息”的功能)的处理过程。
1)创建数据表
本实例需要两张数据表,一张是用户表 user,一张是订单表 order,这两张表具有一对多的级联关系。SQL 语句如下:
- 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);
- DROP TABLE IF EXISTS `user`;
- 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,'编程帮','123'),(2,'C语言中文网','456'),(3,'赵小红','123'),(4,'李晓明','345'),(5,'杨小胤','123'),(6,'谷小乐','789');
2)创建持久化类
创建持久化类 User 和 Order,代码分别如下。
- package net.biancheng.po;
- 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 + ", orderList=" + orderList + "]";
- }
- }
Order 类代码如下。
- package net.biancheng.po;
- public class Order {
- private int id;
- private int ordernum;
- /*省略setter和getter方法*/
- @Override
- public String toString() {
- return "Order [id=" + id + ", ordernum=" + ordernum + "]";
- }
- }
分步查询
OrderMapper 类代码如下。
- public List<Order> selectOrderById(int id);
OrderMapper.xml 中相应的映射 SQL 语句如下。
- <!-- 根据id查询订单信息 -->
- <select id="selectOrderById" resultType="net.biancheng.po.Order"
- parameterType="Integer">
- SELECT * FROM `order` where userId=#{id}
- </select>
UserMapper 类代码如下。
- public User selectUserOrderById1(int id);
UserMapper.xml 中相应的映射 SQL 语句如下。
- <!-- 一对多 根据id查询用户及其关联的订单信息:级联查询的第一种方法(分步查询) -->
- <resultMap type="net.biancheng.po.User" id="userAndOrder1">
- <id property="id" column="id" />
- <result property="name" column="name" />
- <result property="pwd" column="pwd" />
- <!-- 一对多级联查询,ofType表示集合中的元素类型,将id传递给selectOrderById -->
- <collection property="orderList"
- ofType="net.biancheng.po.Order" column="id"
- select="net.biancheng.mapper.OrderMapper.selectOrderById" />
- </resultMap>
- <select id="selectUserOrderById1" parameterType="Integer"
- resultMap="userAndOrder1">
- select * from user where id=#{id}
- </select>
测试代码如下。
- public class Test {
- public static void main(String[] args) 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);
- }
- }
运行结果如下。
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=编程帮, orderList=[Order [id=0, ordernum=20200107], Order [id=0, ordernum=20200645]]]
单步查询
该种方式实现一对多关联查询需要修改 Order 持久化类,因为 Order 中的 id 不能和 User 中的 id 重复。
- package net.biancheng.po;
- public class Order {
- private int oId;
- private int ordernum;
- /*省略setter和getter方法*/
- @Override
- public String toString() {
- return "Order [id=" + oId+ ", ordernum=" + ordernum + "]";
- }
- }
UserMapper 类代码如下。
- public User selectUserOrderById2(int id);
UserMapper.xml 中相关映射 SQL 语句如下。
- <!-- 一对多 根据id查询用户及其关联的订单信息:级联查询的第二种方法(单步查询) -->
- <resultMap type="net.biancheng.po.User" id="userAndOrder2">
- <id property="id" column="id" />
- <result property="name" column="name" />
- <result property="pwd" column="pwd" />
- <!-- 一对多级联查询,ofType表示集合中的元素类型 -->
- <collection property="orderList"
- ofType="net.biancheng.po.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>
测试代码修改调用方法,如下。
- public class Test {
- public static void main(String[] args) 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).selectUserOrderById2(1);
- System.out.println(us);
- }
- }
运行结果如下。
DEBUG [main] - ==> Preparing: SELECT u.*,o.id as oId,o.ordernum FROM `user` u,`order` o WHERE u.id=o.`userId` AND u.id=?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 2
User [id=1, name=编程帮, orderList=[Order [id=1, ordernum=20200107], Order [id=4, ordernum=20200645]]]
18.MyBatis多对多关联查询
实际应用中,由于多对多的关系比较复杂,会增加理解和关联的复杂度,所以应用较少。MyBatis 没有实现多对多级联,推荐通过两个一对多级联替换多对多级联,以降低关系的复杂度,简化程序。
例如,一个订单可以有多种商品,一种商品可以对应多个订单,订单与商品就是多对多的级联关系。可以使用一个中间表(订单记录表)将多对多级联转换成两个一对多的关系。
示例
下面以订单和商品(实现“查询所有订单以及每个订单对应的商品信息”的功能)为例讲解多对多关联查询。
1. 创建数据表
创建 order(订单),product(商品)和 order_details(订单和商品中间表),SQL 语句如下。
- CREATE TABLE `order` (
- `oid` int(11) NOT NULL AUTO_INCREMENT,
- `ordernum` int(25) DEFAULT NULL,
- `userId` int(11) DEFAULT NULL,
- PRIMARY KEY (`oid`),
- KEY `userId` (`userId`),
- CONSTRAINT `order_ibfk_1` FOREIGN KEY (`userId`) REFERENCES `user` (`id`)
- ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;
- insert into `order`(`oid`,`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),(9,NULL,5);
- DROP TABLE IF EXISTS `orders_detail`;
- 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);
- DROP TABLE IF EXISTS `product`;
- 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);
2. 创建持久化类
Order 类代码如下。
- package net.biancheng.po;
- import java.util.List;
- public class Order {
- private int oid;
- private int ordernum;
- private List<Product> products;
- /*省略setter和getter方法*/
- @Override
- public String toString() {
- return "Order [id=" + oid + ", ordernum=" + ordernum + ", products=" + products + "]";
- }
- }
Product 类方法如下。
- package net.biancheng.po;
- 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 + "]";
- }
- }
3. 创建接口和映射文件
OrderMapper 接口代码如下。
- package net.biancheng.mapper;
- import java.util.List;
- import net.biancheng.po.Order;
- public interface OrderMapper {
- public List<Order> selectAllOrdersAndProducts();
- }
OrderMapper.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="net.biancheng.mapper.OrderMapper">
- <resultMap type="net.biancheng.po.Order" id="orderMap">
- <id property="oid" column="oid" />
- <result property="ordernum" column="ordernum" />
- <collection property="products"
- ofType="net.biancheng.po.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.oid,o.`ordernum`,p.`pid`,p.`name`,p.`price` FROM
- `order` o
- INNER JOIN orders_detail od ON o.oid=od.`orderId`
- INNER JOIN
- product p
- ON p.pid = od.`productId`
- </select>
- </mapper>
4. 创建测试类
测试类代码如下。
- package net.biancheng.test;
- import java.io.IOException;
- import java.io.InputStream;
- import java.util.List;
- 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 net.biancheng.mapper.OrderMapper;
- import net.biancheng.po.Order;
- public class Test {
- public static void main(String[] args) throws IOException {
- InputStream config = Resources.getResourceAsStream("mybatis-config.xml");
- SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(config);
- SqlSession ss = ssf.openSession();
- List<Order> orderList = ss.getMapper(OrderMapper.class).selectAllOrdersAndProducts();
- for (Order or : orderList) {
- System.out.println(or);
- }
- }
- }
运行结果如下。
DEBUG [main] - ==> Preparing: SELECT o.oid,o.`ordernum`,p.`pid`,p.`name`,p.`price` FROM `order` o INNER JOIN orders_detail od ON o.oid=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 [id=1, name=Java教程, price=128.0], Product [id=2, name=C语言教程, price=138.0], Product [id=3, name=Python教程, price=132.35]]]
Order [id=2, ordernum=20200806, products=[Product [id=3, name=Python教程, price=132.35], Product [id=1, name=Java教程, price=128.0]]]
Order [id=3, ordernum=20206702, products=[Product [id=2, name=C语言教程, price=138.0]]]
19.MyBatis动态SQL
动态 SQL 是 MyBatis 的强大特性之一。在 JDBC 或其它类似的框架中,开发人员通常需要手动拼接 SQL 语句。根据不同的条件拼接 SQL 语句是一件极其痛苦的工作。例如,拼接时要确保添加了必要的空格,还要注意去掉列表最后一个列名的逗号。而动态 SQL 恰好解决了这一问题,可以根据场景动态的构建查询。
动态 SQL 只有几个基本元素,与 JSTL 或 XML 文本处理器相似,十分简单明了,大量的判断都可以在 MyBatis 的映射 XML 文件里配置,以达到许多需要大量代码才能实现的功能。
动态 SQL 大大减少了编写代码的工作量,更体现了 MyBatis 的灵活性、高度可配置性和可维护性。
MyBatis 也可以在注解中配置 SQL,但是由于注解功能受限,且对于复杂的 SQL 语句来说可读性差,所以使用较少。本教程不对它们进行介绍。
MyBatis 的动态 SQL 包括以下几种元素,如下表所示。
元素 | 作用 | 备注 |
---|---|---|
if | 判断语句 | 单条件分支判断 |
choose(when、otherwise) | 相当于 Java 中的 switch case 语句 | 多条件分支判断 |
trim、where | 辅助元素 | 用于处理一些SQL拼装问题 |
foreach | 循环语句 | 在in语句等列举条件常用 |
bind | 辅助元素 | 拼接参数 |
下面我们讨论这些动态元素的用法,小伙伴们可点击下方链接阅读学习。
- MyBatis if语句
- MyBatis choose、when和otherwise语句
- MyBatis where语句
- MyBatis trim语句
- MyBatis foreach语句
- MyBatis bind语句
20.MyBatis if标签:条件判断
MyBatis if 类似于 Java 中的 if 语句,是 MyBatis 中最常用的判断语句。使用 if 标签可以节省许多拼接 SQL 的工作,把精力集中在 XML 的维护上。
if 语句使用方法简单,常常与 test 属性联合使用。语法如下。
- <if test="判断条件">
- SQL语句
- </if>
当判断条件为 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>
以上代表表示根据网站名称去查找相应的网站信息,但是网站名称是一个可填可不填的条件,不填写的时候不作为查询条件。
可多个 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>
21.MyBatis choose、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>
choose 标签按顺序判断其内部 when 标签中的判断条件是否成立,如果有一个成立,则执行相应的 SQL 语句,choose 执行结束;如果都不成立,则执行 otherwise 中的 SQL 语句。这类似于 Java 的 switch 语句,choose 为 switch,when 为 case,otherwise 则为 default。
示例
以下示例要求:
- 当网站名称不为空时,只用网站名称作为条件进行模糊查询;
- 当网站名称为空,而网址不为空时,则用网址作为条件进行模糊查询;
- 当网站名称和网址都为空时,则要求网站年龄不为空。
下面使用 choose-when-otherwise 标签实现(本节示例基于《第一个MyBatis程序》一节的代码实现)。
WebsiteMapper.xml 代码如下。
- <mapper namespace="net.biancheng.mapper.WebsiteMapper">
- <select id="selectWebsite"
- parameterType="net.biancheng.po.Website"
- resultType="net.biancheng.po.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>
WebsiteMapper 类中方法如下。
- public List<Website> selectWebsite(Website website);
测试类代码如下。
- public class Test {
- public static void main(String[] args) 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.setname("编程");
- List<Website> siteList = ss.selectList("net.biancheng.mapper.WebsiteMapper.selectWebsite", site);
- for (Website ws : siteList) {
- System.out.println(ws);
- }
- }
- }
输出结果如下。
DEBUG [main] - ==> Preparing: SELECT id,name,url,age,country FROM website WHERE 1=1 AND name LIKE CONCAT('%',?,'%')
DEBUG [main] - ==> Parameters: 编程(String)
DEBUG [main] - <== Total: 1
Website[id=1,name=编程帮,url=https://www.biancheng.net/,age=10,country=CN,createtime=null]
这样 MyBatis 就会根据参数的设置进行判断来动态组装 SQL,以满足不同业务的要求。远比 Hibernate 和 JDBC 中大量判断 Java 代码要清晰和明确。
22.MyBatis where标签
细心的读者可能会发现,我们在《MyBatis choose、when和otherwise语句》一节的 SQL 语句中加入了一个条件“1=1”,如果没有加入这个条件,那么可能就会变成下面这样一条错误的语句。
- SELECT id,name,url,age,country FROM website AND name LIKE CONCAT('%',#{name},'%')
显然以上语句会出现 SQL 语法异常,但加入“1=1”这样的条件又非常奇怪,所以 MyBatis 提供了 where 标签。
where 标签主要用来简化 SQL 语句中的条件判断,可以自动处理 AND/OR 条件,语法如下。
- <where>
- <if test="判断条件">
- AND/OR ...
- </if>
- </where>
if 语句中判断条件为 true 时,where 关键字才会加入到组装的 SQL 里面,否则就不加入。where 会检索语句,它会将 where 后的第一个 SQL 条件语句的 AND 或者 OR 关键词去掉。
示例
要求:根据网站名称或网址对网站进行模糊查询(本节示例基于《第一个MyBatis程序》一节的代码实现)。
WebsiteMapper.xml 代码如下。
- <select id="selectWebsite" resultType="net.biancheng.po.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>
WebsiteMapper 类中方法如下。
- public List<Website> selectWebsite(Website website);
测试类代码如下。
- public class Test {
- public static void main(String[] args) 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.setname("编程");
- List<Website> siteList = ss.selectList("net.biancheng.mapper.WebsiteMapper.selectWebsite", site);
- for (Website ws : siteList) {
- System.out.println(ws);
- }
- }
- }
输出结果如下。
DEBUG [main] - ==> Preparing: SELECT id,name,url,age,country FROM website WHERE name LIKE CONCAT('%',?,'%')
DEBUG [main] - ==> Parameters: 编程(String)
DEBUG [main] - <== Total: 1
Website[id=1,name=编程帮,url=https://www.biancheng.net/,age=10,country=CN]
23.MyBatis set标签
在 Mybatis 中,update 语句可以使用 set 标签动态更新列。set 标签可以为 SQL 语句动态的添加 set 关键字,剔除追加到条件末尾多余的逗号。
示例
要求:根据 id 修改网站名称或网址(本节示例基于《第一个MyBatis程序》一节的代码实现)。
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="net.biancheng.mapper.WebsiteMapper">
- <select id="selectWebsite" resultType="net.biancheng.po.Website">
- SELECT * FROM website
- <where>
- <if test="id!=null and id!=''">
- id=#{id}
- </if>
- </where>
- </select>
- <!--使用set元素动态修改一个网站记录 -->
- <update id="updateWebsite"
- parameterType="net.biancheng.po.Website">
- UPDATE website
- <set>
- <if test="name!=null">name=#{name}</if>
- <if test="url!=null">url=#{url}</if>
- </set>
- WHERE id=#{id}
- </update>
- </mapper>
WebsiteMapper 类中方法如下。
- package net.biancheng.mapper;
- import java.util.List;
- import org.apache.ibatis.annotations.Param;
- import net.biancheng.po.Website;
- public interface WebsiteMapper {
- public List<Website> selectWebsite(Website site);
- public int updateWebsite(Website site);
- }
测试类代码如下。
- package net.biancheng.test;
- import java.io.IOException;
- import java.io.InputStream;
- import java.util.List;
- 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 net.biancheng.mapper.WebsiteMapper;
- import net.biancheng.po.Website;
- public class Test {
- public static void main(String[] args) throws IOException {
- InputStream config = Resources.getResourceAsStream("mybatis-config.xml");
- SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(config);
- SqlSession ss = ssf.openSession();
- Website site = new Website();
- site.setId(1);
- site.setUrl("www.biancheng.net");
- // 执行update语句前
- List<Website> siteList = ss.getMapper(WebsiteMapper.class).selectWebsite(site);
- for (Website st : siteList) {
- System.out.println(st);
- }
- int num = ss.getMapper(WebsiteMapper.class).updateWebsite(site);
- System.out.println("影响数据库行数" + num);
- // 执行update语句后
- List<Website> siteList2 = ss.getMapper(WebsiteMapper.class).selectWebsite(site);
- for (Website st : siteList2) {
- System.out.println(st);
- }
- ss.commit();
- ss.close();
- }
- }
输出结果如下。
DEBUG [main] - ==> Preparing: SELECT * FROM website WHERE id=?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
Website[id=1,name=编程帮,url=http://www.biancheng.net/,age=10,country=CN]
DEBUG [main] - ==> Preparing: UPDATE website SET url=? where id=?
DEBUG [main] - ==> Parameters: www.biancheng.net(String), 1(Integer)
DEBUG [main] - <== Updates: 1
影响数据库行数1
DEBUG [main] - ==> Preparing: SELECT * FROM website WHERE id=?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
Website[id=1,name=编程帮,url=www.biancheng.net,age=10,country=CN]
24.MyBatis foreach标签
前面我们学习了如何使用 Mybatis if、where、trim 等动态语句来处理一些简单的查询操作。对于一些 SQL 语句中含有 in 条件,需要迭代条件集合来生成的情况,可以使用 foreach 来实现 SQL 条件的迭代。
Mybatis foreach 标签用于循环语句,它很好的支持了数据和 List、set 接口的集合,并对此提供遍历的功能。语法格式如下。
- <foreach item="item" index="index" collection="list|array|map key" open="(" separator="," close=")">
- 参数值
- </foreach>
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。
示例
本节示例基于《第一个MyBatis程序》一节的代码实现。
现有 website 表包含以下记录。
+----+----------------+----------------------------+-----+---------+---------------------+ | id | name | url | age | country | createtime | +----+----------------+----------------------------+-----+---------+---------------------+ | 1 | 编程帮 | https://www.biancheng.net/ | 10 | CN | 2021-02-23 10:20:40 | | 2 | C语言中文网 | http://c.biancheng.net/ | 12 | CN | 2021-03-08 11:23:27 | | 3 | 百度 | https://www.baidu.com/ | 18 | CN | 2021-03-08 11:23:53 | | 4 | 淘宝 | https://www.taobao.com/ | 17 | CN | 2021-03-10 10:33:54 | | 5 | Google | https://www.google.com/ | 23 | US | 2021-03-10 10:34:34 | | 6 | GitHub | https://github.com/ | 13 | US | 2021-03-10 10:34:34 | | 7 | Stack Overflow | https://stackoverflow.com/ | 16 | US | 2021-03-10 10:34:34 | | 8 | Yandex | http://www.yandex.ru/ | 11 | RU | 2021-03-10 10:34:34 | +----+----------------+----------------------------+-----+---------+---------------------+
WebsiteMapper.xml 中代码如下。
- <select id="selectWebsite"
- parameterType="net.biancheng.po.Website"
- resultType="net.biancheng.po.Website">
- SELECT id,name,url,age,country
- FROM website WHERE age in
- <foreach item="age" index="index" collection="list" open="("
- separator="," close=")">
- #{age}
- </foreach>
- </select>
WebsiteMapper 类中相应方法如下。
- public List<Website> selectWebsite(List<Integer> ageList);
测试代码如下。
- public class Test {
- public static void main(String[] args) 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(10);
- ageList.add(12);
- List<Website> siteList = ss.selectList("net.biancheng.mapper.WebsiteMapper.selectWebsite", ageList);
- for (Website ws : siteList) {
- System.out.println(ws);
- }
- }
- }
输出结果如下。
DEBUG [main] - ==> Preparing: SELECT id,name,url,age,country FROM website WHERE age in ( ? , ? )
DEBUG [main] - ==> Parameters: 10(Integer), 12(Integer)
DEBUG [main] - <== Total: 2
Website[id=1,name=编程帮,url=https://www.biancheng.net/,age=10,country=CN]
Website[id=2,name=C语言中文网,url=http://c.biancheng.net/,age=12,country=CN]
拓展
在使用 foreach 标签时,应提前预估一下 collection 对象的长度。因为大量数据的 in 语句会影响性能,且还有一些数据库会限制执行的 SQL 语句长度。
25.MyBatis bind标签
每个数据库的拼接函数或连接符号都不同,例如 MySQL 的 concat 函数、Oracle 的连接符号“||”等。这样 SQL 映射文件就需要根据不同的数据库提供不同的实现,显然比较麻烦,且不利于代码的移植。幸运的是,MyBatis 提供了 bind 标签来解决这一问题。
bind 标签可以通过 OGNL 表达式自定义一个上下文变量。
比如,按照网站名称进行模糊查询,SQL 映射文件如下。
- <select id="selectWebsite" resultType="net.biancheng.po.Website">
- <bind name="pattern" value="'%'+_parameter+'%'" />
- SELECT id,name,url,age,country
- FROM website
- WHERE name like #{pattern}
- </select>
bind 元素属性如下。
- value:对应传入实体类的某个字段,可以进行字符串拼接等特殊处理。
- name:给对应参数取的别名。
以上代码中的“_parameter”代表传递进来的参数,它和通配符连接后,赋给了 pattern,然后就可以在 select 语句中使用这个变量进行模糊查询,不管是 MySQL 数据库还是 Oracle 数据库都可以使用这样的语句,提高了可移植性。
大部分情况下需要传递多个参数,下面为传递多个参数时 bind 的用法示例。
示例
本节示例基于《第一个MyBatis程序》一节的代码实现。
WebsiteMapper 类中方法代码如下。
- public List<Website> selectWebsite(Website site);
SQL 映射文件代码如下。
- <select id="selectWebsite" resultType="net.biancheng.po.Website">
- <bind name="pattern_name" value="'%'+name+'%'" />
- <bind name="pattern_url" value="'%'+url+'%'" />
- SELECT id,name,url,age,country
- FROM website
- WHERE name like #{pattern_name}
- AND url like #{pattern_url}
- </select>
测试代码如下。
- public class Test {
- public static void main(String[] args) 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.setname("编程");
- site.setUrl("http");
- List<Website> siteList = ss.selectList("net.biancheng.mapper.WebsiteMapper.selectWebsite", site);
- for (Website ws : siteList) {
- System.out.println(ws);
- }
- }
- }
运行结果如下。
DEBUG [main] - ==> Preparing: SELECT id,name,url,age,country FROM website WHERE name like ? AND url like ?
DEBUG [main] - ==> Parameters: %编程%(String), %http%(String)
DEBUG [main] - <== Total: 1
Website[id=1,name=编程帮,url=https://www.biancheng.net/,age=10,country=CN]
26.MyBatis trim标签
在 MyBatis 中除了使用 if+where 实现多条件查询,还有一个更为灵活的元素 trim 能够替代之前的做法。
trim 一般用于去除 SQL 语句中多余的 AND 关键字、逗号,
或者给 SQL 语句前拼接 where、set 等后缀,可用于选择性插入、更新、删除或者条件查询等操作。trim 语法格式如下。
- <trim prefix="前缀" suffix="后缀" prefixOverrides="忽略前缀字符" suffixOverrides="忽略后缀字符">
- SQL语句
- </trim>
trim 中属性说明如下。
属性 | 描述 |
---|---|
prefix | 给SQL语句拼接的前缀,为 trim 包含的内容加上前缀 |
suffix | 给SQL语句拼接的后缀,为 trim 包含的内容加上后缀 |
prefixOverrides | 去除 SQL 语句前面的关键字或字符,该关键字或者字符由 prefixOverrides 属性指定。 |
suffixOverrides | 去除 SQL 语句后面的关键字或者字符,该关键字或者字符由 suffixOverrides 属性指定。 |
示例
本节示例基于《第一个MyBatis程序》一节的代码实现。
下面我们利用 trim 实现与 where 元素相同的效果。
要求:根据网站名称或网址对网站进行模糊查询。
WebsiteMapper.xml 代码如下。
- <select id="selectWebsite" resultType="net.biancheng.po.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>
WebsiteMapper 类中方法如下。
- public List<Website> selectWebsite(Website website);
测试类代码如下。
- public class Test {
- public static void main(String[] args) 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.setname("编程");
- site.setUrl("http");
- List<Website> siteList = ss.selectList("net.biancheng.mapper.WebsiteMapper.selectWebsite", site);
- for (Website ws : siteList) {
- System.out.println(ws);
- }
- }
- }
输出结果如下。
DEBUG [main] - ==> Preparing: SELECT id,name,url,age,country FROM website where name LIKE CONCAT ('%',?,'%') AND url like concat ('%',?,'%')
DEBUG [main] - ==> Parameters: 编程(String), http(String)
DEBUG [main] - <== Total: 1
Website[id=1,name=编程帮,url=https://www.biancheng.net/,age=10,country=CN]
27.MyBatis分页功能
MyBatis 的分页功能是基于内存的分页,即先查询出所有记录,再按起始位置和页面容量取出结果。
本节我们为查询网站记录增加分页功能,要求结果列表按照 id 升序排列(本节示例基于《第一个MyBatis程序》一节的代码实现)。
WebsiteMapper 中方法如下。
- /**
- *
- * @param site
- * @param currentPageNo 起始位置
- * @param pageSize 页面容量
- * @return
- */
- public List<Website> selectWebsite(@Param("site") Website site, @Param("from") Integer currentPageNo,
- @Param("pageSize") Integer pageSize);
相比原来的 selectWebsite 方法,增加了两个参数,起始位置(from)和页面容量(pageSize),用于实现分页查询。
修改 WebsiteMapper.xml 的查询语句,增加 limit 关键字,SQL 映射代码如下。
- <select id="selectWebsite" resultType="net.biancheng.po.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>
测试类代码如下。
- public static void main(String[] args) 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.setname("编程");
- site.setUrl("http");
- Integer pageSize = 3;
- Integer currentPageNo = 0;
- List<Website> siteList = new ArrayList<Website>();
- siteList = ss.getMapper(WebsiteMapper.class).selectWebsite(site, currentPageNo, pageSize);
- for (Website ws : siteList) {
- System.out.println(ws);
- }
- }
运行结果如下。
DEBUG [main] - ==> Preparing: SELECT id,name,url,age,country FROM website where url like concat ('%',?,'%') ORDER BY id limit ?,?
DEBUG [main] - ==> Parameters: http(String), 0(Integer), 3(Integer)
DEBUG [main] - <== Total: 3
Website[id=1,name=编程帮,url=https://www.biancheng.net/,age=10,country=CN]
Website[id=2,name=C语言中文网,url=http://c.biancheng.net/,age=12,country=CN]
Website[id=3,name=百度,url=https://www.baidu.com/,age=18,country=CN]
上述代码中,根据传入的起始位置(currentPageNo=0)和页面容量(pageSize=3)进行相应分页,查看第一页的数据列表,运行测试方法,输出正确的分页列表。
注意:MyBatis 实现分页查询属于 DAO 层操作,由于 DAO 层不牵涉任何业务实现,所以实现分页的方法中第一个参数为 limit 的起始位置(下标从 0 开始),而不是用户输入的真正页码(页码从1开始)。在 JSP 教程的《JSP分页》一节已经学习过页码如何转换为 limit 的起始位置下标,即:起始位置下标=(页码-1)*页面容量,那么这个转换操作必然不能在 DAO 层实现,需要在业务层实现。所以我们在测试类中传入的参数为下标,而不是页码。
28.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 到数据库执行,返回结果。
示例
本节示例基于《第一个MyBatis程序》一节的代码实现。
在 WebsiteMapper 类中添加 selectWebsiteById 方法,代码如下。
- public Website selectWebsiteById(int id);
WebsiteMapper.xml 中添加相应的映射 SQL 语句,代码如下。
- <select id="selectWebsiteById"
- resultType="net.biancheng.po.Website">
- SELECT * FROM website
- WHERE id=#{id}
- </select>
测试代码如下。
- package net.biancheng.test;
- import java.io.IOException;
- import java.io.InputStream;
- import java.util.List;
- 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 org.apache.log4j.Logger;
- import net.biancheng.po.Website;
- public class Test {
- public static Logger logger = Logger.getLogger(Test.class);
- public static void main(String[] args) throws IOException {
- InputStream config = Resources.getResourceAsStream("mybatis-config.xml"); // 根据配置文件构建
- SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(config);
- SqlSession ss = ssf.openSession();
- Website site = ss.selectOne("net.biancheng.mapper.WebsiteMapper.selectWebsiteById", 1);
- logger.debug("使用同一个sqlsession再执行一次");
- Website site2 = ss.selectOne("net.biancheng.mapper.WebsiteMapper.selectWebsiteById", 1);
- // 请注意,当我们使用二级缓存的时候,sqlSession调用了 commit方法后才会生效
- ss.commit();
- logger.debug("现在创建一个新的SqlSeesion对象在执行一次");
- SqlSession ss2 = ssf.openSession();
- Website site3 = ss2.selectOne("net.biancheng.mapper.WebsiteMapper.selectWebsiteById", 1);
- // 请注意,当我们使用二级缓存的时候,sqlSession调用了 commit方法后才会生效
- ss2.commit();
- }
- }
运行结果如下。
DEBUG [main] - ==> Preparing: SELECT * FROM website WHERE id=?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
DEBUG [main] - 使用同一个sqlsession再执行一次
DEBUG [main] - 现在创建一个新的SqlSeesion对象在执行一次
DEBUG [main] - ==> Preparing: SELECT * FROM website WHERE id=?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
从运行结果可以看出,第一个 SqlSession 实际只发生过一次查询,而第二次查询就从缓存中取出了,也就是 SqlSession 层面的一级缓存。为了克服这个问题,我们往往需要配置二级缓存,使得缓存在 SqlSessionFactory 层面上能够提供给各个 SqlSession 对象共享。
二级缓存
二级缓存是全局缓存,作用域超出 session 范围之外,可以被所有 SqlSession 共享。
一级缓存缓存的是 SQL 语句,二级缓存缓存的是结果对象。
二级缓存的配置
1)MyBatis 的全局缓存配置需要在 mybatis-config.xml 的 settings 元素中设置,代码如下。
- <settings>
- <setting name="cacheEnabled" value="true" />
- </settings>
2)在 mapper 文件(如 WebMapper.xml)中设置缓存,默认不开启缓存。需要注意的是,二级缓存的作用域是针对 mapper 的 namescape 而言,即只有再次在 namescape 内(net.biancheng.WebsiteMapper)的查询才能共享这个缓存,代码如下。
- <mapper namescape="net.biancheng.WebsiteMapper">
- <!-- cache配置 -->
- <cache
- eviction="FIFO"
- flushInterval="60000"
- size="512"
- readOnly="true" />
- ...
- </mapper>
以上属性说明如下。
属性 | 说明 |
---|---|
eviction | 代表的是缓存回收策略,目前 MyBatis 提供以下策略。
|
flushInterval | 刷新间隔时间,单位为毫秒,这里配置的是 100 秒刷新,如果省略该配置,那么只有当 SQL 被执行的时候才会刷新缓存。 |
size | 引用数目,正整数,代表缓存最多可以存储多少个对象,不宜设置过大。设置过大会导致内存溢出。这里配置的是 1024 个对象。 |
readOnly | 只读,默认值为 false,意味着缓存数据只能读取而不能修改,这样设置的好处是可以快速读取缓存,缺点是没有办法修改缓存。 |
3)在 mapper 文件配置支持 cache 后,如果需要对个别查询进行调整,可以单独设置 cache,代码如下。
- <select id="getWebsiteList" resultType="net.biancheng.po.Website" usecache="true">
- ...
- </select>
对于 MyBatis 缓存仅作了解即可,因为面对一定规模的数据量,内置的 Cache 方式就派不上用场了,并且对查询结果集做缓存并不是 MyBatis 所擅长的,它专心做的应该是 SQL 映射。对于缓存,采用 OSCache、Memcached 等专门的缓存服务器来做更为合理。
29.MyBatis逆向工程
Mybatis 提供了一个逆向工程工具,该工具可以根据数据表自动生成针对单表的 po 类、mapper 映射文件和 mapper 接口。大大缩减了开发时间,可以让开发人员将更多的精力放在繁杂的业务逻辑上。
之所以强调单表两个字,是因为 MyBatis 逆向工程生成的 Mapper 中的操作都是针对单表的。在大型项目中,很少有复杂的多表关联查询,所以该工具作用还是很大的。
1. 下载jar包
jar包下载链接:https://github.com/mybatis/generator/releases。本节下载版本为 mybatis-generator-core-1.4.0。
maven 项目在 pom.xml 文件中添加以下代码。
- <dependency>
- <groupId>org.mybatis.generator</groupId>
- <artifactId>mybatis-generator-core</artifactId>
- <version>1.4.0</version>
- </dependency>
2. 创建数据表
创建 user、student、studentCard 和 website 数据表,SQL 语句如下。
- DROP TABLE IF EXISTS `student`;
- 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;
- DROP TABLE IF EXISTS `studentcard`;
- 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=6 DEFAULT CHARSET=utf8;
- DROP TABLE IF EXISTS `user`;
- 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;
- 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 CURRENT_TIMESTAMP,
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
3. 创建项目
创建 NXProject 项目,导入所需 jar 包。
新建资源文件夹 config,在 config 文件夹下创建 genertorConfig.xml 文件,用于配置及指定数据库及表等。
注意:可以在已有的项目中进行逆向生成,也可以创建一个新项目。建议创建新项目,避免出现意外影响已有的项目。
genertorConfig.xml 配置文件代码如下。
- <?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>
- <context id="DB2Tables" targetRuntime="MyBatis3">
- <commentGenerator>
- <!-- 是否去除自动生成的注释 -->
- <property name="suppressAllComments" value="true" />
- </commentGenerator>
- <!-- Mysql数据库连接的信息:驱动类、连接地址、用户名、密码 -->
- <jdbcConnection driverClass="com.mysql.jdbc.Driver"
- connectionURL="jdbc:mysql://localhost:3306/test" userId="root"
- password="root" />
- <!-- 默认为false,把JDBC DECIMAL 和NUMERIC类型解析为Integer,为true时 把JDBC DECIMAL 和NUMERIC类型解析为java.math.BigDecimal -->
- <javaTypeResolver>
- <property name="forceBigDecimals" value="false" />
- </javaTypeResolver>
- <!-- targetProject:生成POJO类的位置 -->
- <javaModelGenerator
- targetPackage="net.biancheng.pojo" targetProject=".\src">
- <!-- enableSubPackages:是否让schema作为包的后缀 -->
- <property name="enableSubPackages" value="false" />
- <!-- 从数据库返回的值被清理前后的空格 -->
- <property name="trimStrings" value="true" />
- </javaModelGenerator>
- <!-- targetProject:mapper映射文件生成的位置 -->
- <sqlMapGenerator targetPackage="net.biancheng.mapper"
- targetProject=".\src">
- <!-- enableSubPackages:是否让schema作为包的后缀 -->
- <property name="enableSubPackages" value="false" />
- </sqlMapGenerator>
- <!-- targetProject:mapper接口生成的的位置 -->
- <javaClientGenerator type="XMLMAPPER"
- targetPackage="net.biancheng.mapper" targetProject=".\src">
- <!-- enableSubPackages:是否让schema作为包的后缀 -->
- <property name="enableSubPackages" value="false" />
- </javaClientGenerator>
- <!-- 指定数据表 -->
- <table tableName="website"></table>
- <table tableName="student"></table>
- <table tableName="studentcard"></table>
- <table tableName="user"></table>
- </context>
- </generatorConfiguration>
创建 GeneratorSqlmap 类执行生成代码。
- package net.biancheng;
- import java.io.File;
- import java.util.*;
- import org.mybatis.generator.api.MyBatisGenerator;
- import org.mybatis.generator.config.Configuration;
- import org.mybatis.generator.config.xml.ConfigurationParser;
- import org.mybatis.generator.internal.DefaultShellCallback;
- public class GeneratorSqlmap {
- public void generator() throws Exception {
- List<String> warnings = new ArrayList<String>();
- boolean overwrite = true;
- // 指定配置文件
- File configFile = new File("./config/generatorConfig.xml");
- ConfigurationParser cp = new ConfigurationParser(warnings);
- Configuration config = cp.parseConfiguration(configFile);
- DefaultShellCallback callback = new DefaultShellCallback(overwrite);
- MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
- myBatisGenerator.generate(null);
- }
- // 执行main方法以生成代码
- public static void main(String[] args) {
- try {
- GeneratorSqlmap generatorSqlmap = new GeneratorSqlmap();
- generatorSqlmap.generator();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
项目结构如下图所示。
执行 GeneratorSqlmap 后刷新项目,项目结构如下。
可以看到在 pojo 包中,有一部分是名字为 XxxExample 的类。类中包含以下 3 个成员变量,如下。
- protected String orderByClause;
- protected boolean distinct;
- protected List<Criteria> oredCriteria;
以上变量说明如下:
- distinct 字段用于指定 DISTINCT 查询。
- orderByClause 字段用于指定 ORDER BY 条件,这个条件没有构造方法,直接通过传递字符串值指定。
- oredCriteria 字段用于自定义查询条件。