框架 01 - MyBatis 3

1、MyBatis 入门

1.1、介绍

MyBatis 作为一款优秀的持久层框架(ORM 框架),其支持自定义 SQL、存储过程以及高级映射。几乎免除了所有的 JDBC 代码以及设置参数获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

主要特点

  • 简单易学:本身就很小且简单。没有任何第三方依赖,最简单安装只要两个 Jar 文件 + 配置几个 SQL 映射文件。易于学习,易于使用。通过文档和源代码,可以比较完全的掌握他的设计思路和实现。
  • 设计灵活:MyBatis 不会对应用程序或者数据库的现有设计强加任何影响。SQL 写在 XML 里,便于统一管理和优化。通过 SQL 语句可以满足操作数据库的所有需求。
  • 降低耦合:通过提供 DAO 层,将业务逻辑和数据访问逻辑分离,使系统的设计更清晰,更易维护,更易单元测试。SQL 和代码的分离,提高了可维护性。
  • 关系映射:提供映射标签,支持对象与数据库的 ORM 字段关系映射。
  • 动态 SQL:提供 XML 标签,支持编写动态 SQL。

相关网址

  • 官方中文文档:https://mybatis.org/mybatis-3/zh/index.html。
  • GitHub 下载:https://github.com/mybatis/mybatis-3/releases。
    • mybatis-3.5.7.zip:包含 Mybatis 驱动、Log4j 日志等等 Jar 包,以及 MyBatis 官方英文文档。
    • Source code (zip):源码包 Windows 版本。
    • Source code (tar.gz):源码包 Linux 版本(相比 Windows 版本其保留有文件权限)。

如何使用 MyBatis

  • Java 项目:首先导入 Jar 包,然后创建一个接口两个 XML(映射接口、配置文件、映射文件),最后使用相关 API 调用即可。
  • Maven 项目:导入 Maven 依赖即可,其他的与上面相同。

Maven 依赖

  • MyBatis 依赖:MyBatis、PageHelper。
  • JDBC 依赖:MySQL Connector。
  • 其他依赖:Junit、Lombok。
<!--MyBatis 依赖-->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.7</version>
</dependency>
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.2.0</version>
</dependency>

<!--JDBC 依赖-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.26</version>
</dependency>

<!--其他依赖-->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.12</version>
</dependency>

数据库

CREATE DATABASE IF NOT EXISTS `mybatis` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE `mybatis`;
CREATE TABLE IF NOT EXISTS `book` (
	`bid` INT COMMENT '主键',
	`bname` VARCHAR(100) COMMENT '名称',
	`bcount` INT COMMENT '数量',
	PRIMARY KEY (`bid`)
);
INSERT INTO `book` (`bid`, `bname`, `bcount`) VALUES
('1', 'Java 基础', '1'),
('2', 'Java 中级', '2'),
('3', 'Java 高级', '3');

1.2、入门示例

步骤

  • 一、创建 Maven 项目,导入 Maven 依赖。
  • 二、创建数据库、实体类。
  • 三、创建映射接口、映射文件。
  • 四、创建配置文件(jdbc.properteis、mybatis-config.xml)。
  • 五、测试。

入门示例

  • 一、创建 Maven 项目,导入 Maven 依赖。

  • 二、创建数据库、实体类。

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Book {
    private Integer bid;
    private String bname;
    private Integer bcount;
}
  • 三、创建映射接口、映射文件。
public interface BookMapper {
    List<Book> selectAll();
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.feng.dao.BookMapper">
    <select id="selectAll" resultType="com.feng.pojo.Book">
        select * from book
    </select>
</mapper>
  • 四、创建配置文件(jdbc.properteis、mybatis-config.xml)。
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis?serverTimezone=Asia/Shanghai&useSSL=true&useUnicode=true&characterEncoding=UTF-8
username=root
password=test123!
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <properties resource="jdbc.properties"/>
    <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>
    <mappers>
        <mapper resource="com/feng/dao/BookMapper.xml"/>
    </mappers>
</configuration>
  • 五、测试。
public class T01_Start {
    @Test
    public void test01() throws IOException {
        // 1、读取 MyBatis 配置文件,获取 SqlSession 实例
        String resource = "mybatis-config.xml";
        InputStream is = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 2、获取映射接口实例,执行方法
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        List<Book> bookList = mapper.selectAll();
        bookList.forEach(System.out::println);
        // 3、关闭 SqlSession
        sqlSession.close();
    }
}

关于无法找到 MyBatis 映射文件

MyBatis 映射文件放在源码包(例如 com.feng.dao 或者 com.feng.mapper),执行测试时发生异常,提示无法找到 MyBatis 映射文件。

原因分析:MyBatis 映射文件属于 XML 文件,如果放在源码包,执行测试时,其不会被编译到 target 目录。

解决方式:

  • 方式一、将 MyBatis 映射文件放到 resource/com/feng/dao 目录。
  • 方式二、发生异常时手动将将 MyBatis 映射文件复制到 target/com/feng/dao 目录,重新测试。
  • 方式三、配置 Maven 静态资源过滤。
<!--Maven 静态资源过滤-->
<build>
    <resources>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.properties</include>
                <include>**/*.xml</include>
            </includes>
            <filtering>true</filtering>
        </resource>
        <resource>
            <directory>src/main/resources</directory>
            <includes>
                <include>**/*.properties</include>
                <include>**/*.xml</include>
            </includes>
            <filtering>true</filtering>
        </resource>
    </resources>
</build>

1.3、核心 API

MyBatis 相比 JDBC,可以大大简化 Java 代码,容易理解,方便维护。

SqlSession 作为 MyBatis 的核心接口,用于获取 MyBatis 映射接口实例、执行 SQL 语句、提交与回滚事务等等。其由 SqlSessionFactory 实例创建,SqlSessionFactory 由 SqlSessionFactoryBuilder 实例创建。

如果搭配使用依赖注入框架(例如 Spring),SqlSession 会被创建并注入该容器,不需要再使用 SqlSessionFactory 或者 SqlSessionFactoryBuilder。具体可以参考 Mybatis-Spring。

SqlSessionFactoryBuilder

SqlSessionFactoryBuilder 提供多个重载的 build() 方法,用于从不同地方的资源中加载并创建 SqlSessionFactory 实例。

public class SqlSessionFactoryBuilder {
  public SqlSessionFactory build(Reader reader) {}
  public SqlSessionFactory build(Reader reader, String environment) {}
  public SqlSessionFactory build(Reader reader, Properties properties) {}
  public SqlSessionFactory build(Reader reader, String environment, Properties properties) {}
  public SqlSessionFactory build(InputStream inputStream) {}// 常用
  public SqlSessionFactory build(InputStream inputStream, String environment) {}
  public SqlSessionFactory build(InputStream inputStream, Properties properties) {}
  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {}
  public SqlSessionFactory build(Configuration config) {}
}

最常用的 build(InputStream inputStream),用于从 MyBatis 配置文件中加载 SqlSessionFactory 实例。只需将 MyBatis 配置文件转换为 InputStream 传入即可。这里可以使用 MyBatis 提供的 org.apache.ibatis.io.Resources 工具类,其专门用于加载配置文件。

<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="{root}"/>
            <property name="password" value="${password}"/>
        </dataSource>
    </environment>
</environments>

上面提到 SqlSessionFactoryBuilder 可以从不同地方的资源中加载并创建 SqlSessionFactory 实例。注意,如果这些信息同时存在多个地方,加载顺序如下:

  • 首先,读取 properties 的子标签 property 中的信息。
  • 其次,读取 properties 引入外部数据源文件中的信息(resource/url),并覆盖之前的信息。
  • 然后,读取 environments 的子标签 property 中的信息,并覆盖之前的信息。
  • 最后,读取方法参数中的信息,并覆盖之前的信息。

因此方法参数优先级最高,environments 优先级较高,resource/url 优先级中等,properties 优先级最低。

最后一个 build(Configuration config),用于直接从 Java 代码配置中加载并创建 SqlSessionFactory 实例,可以完全取代 MyBatis 配置文件(后面细说)。

SqlSessionFactory

SqlSessionFactory 提供多个重载的 openSession() 方法,用于创建 SqlSession 实例。并且需要考虑以下内容:

  • 事务处理:自动提交还是手动提交。
  • 数据库连接:如何获取数据库连接。
  • 执行 SQL 语句:PreparedStatement 或者批量更新。
  • 运行时检查 MyBatis 配置。
public interface SqlSessionFactory {
  SqlSession openSession();// 手动提交事务
  SqlSession openSession(boolean autoCommit);// true 自动提交
  SqlSession openSession(Connection connection);// 使用自定义 Connection 获取 SqlSession
  SqlSession openSession(TransactionIsolationLevel level);// 事务隔离级别
  SqlSession openSession(ExecutorType execType);// ExecutorType 枚举:SIMPLE 为默认创建新的预处理语句,REUSE 为复用预处理语句,BATCH 为批量执行所有更新语句
  SqlSession openSession(ExecutorType execType, boolean autoCommit);
  SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level);
  SqlSession openSession(ExecutorType execType, Connection connection);
  Configuration getConfiguration();// 运行时检查 MyBatis 配置
}

SqlSession

SqlSession 作为 MyBatis 的核心接口。方法分类如下:

  • 获取 MyBatis 映射接口实例:getMapper()。
  • 执行 SQL:select()、delete()、update()、delete()。
  • 提交回滚事务:commit()、commit(boolean force)、void rollback()、rollback(boolean force)。
  • 关闭连接:close()。
  • 缓存管理。
  • 映射注解。

1.4、生命周期

如果使用依赖注入容器(例如 Spring),作用域与生命周期直接交给容器管理。

SqlSessionFactoryBuilder

仅仅用于创建 SqlSessionFactory,创建后便不再需要。因此其最佳作用域是方法作用域(局部变量)

SqlSessionFactory

用于创建 SqlSession,而且每次请求时都需要创建一个 SqlSession,程序运行期间应该一直存在,并且不需要重新创建。因此其最佳作用域是应用作用域。建议使用单例模式或者静态单例模式

SqlSession

用于执行 SQL 语句,线程不安全,不能被共享。因此其最佳作用域是请求作用域。每次请求之后创建一个 SqlSession,返回响应之后关闭这个 SqlSession。关闭 SqlSession 非常重要,为了确保始终能够执行,可以将其放到 finally 块中。

1.5、MyBatis 工具类

如何获取 SqlSession

String resource = "mybatis-config.xml";
InputStream is = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = sqlSessionFactory.openSession();

MybatisUtil 类(标准单例模式)

public class MybatisUtil {
    private static final SqlSessionFactory sqlSessionFactory;

    private MybatisUtil() {
    }

    static {
        String resource = "mybatis-config.xml";
        InputStream is = null;
        try {
            is = Resources.getResourceAsStream(resource);
        } catch (IOException e) {
            e.printStackTrace();
        }
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
    }

    public static SqlSession getSqlSession() {
        return sqlSessionFactory.openSession();
    }
}

示例

  • 测试
public class T02_MyBatisUtil {
    @Test
    public void test01() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        List<Book> bookList = mapper.selectAll();
        bookList.forEach(System.out::println);
        sqlSession.close();
    }
}

2、MyBatis 配置文件

2.1、介绍

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> 数据源
    <databaseIdProvider> 数据库厂商标识
    <mappers> 映射器
</configuration>

注意:这些顶级标签具有先后顺序,并且只能存在一个。

2.2、属性(properties)

属性用于配置数据源信息,或者引入外部数据源文件动态替换,以供环境配置使用。

标签说明

  • resource/url 属性:引入外部 Properties 数据源文件。resource 为类路径,url 为网络路径。
  • property 子标签:数据源属性键值对。name 为名称,value 为值并且可以使用 ${} 动态替换。
<properties resource="jdbc.properties"/>
<properties url="jdbc.properties"/>
<properties>
    <property name="driver" value="${driver}"/>
    <property name="url" value="${url}"/>
    <property name="username" value="${username}"/>
    <property name="password" value="${password}"/>
</properties>
<properties>
    <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=Asia/Shanghai&amp;useSSL=true&amp;useUnicode=true&amp;characterEncoding=UTF-8"/>
    <property name="username" value="root"/>
    <property name="password" value="test123!"/>
</properties>
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis?serverTimezone=Asia/Shanghai&useSSL=true&useUnicode=true&characterEncoding=UTF-8
username=root
password=test123!

2.3、设置(settings)

设置用于调整设置 MyBatis,可以改变 MyBatis 的运行时行为。(可选,详见官网中文文档)

标签说明

  • setting 子标签:设置信息键值对。name 为名称,value 为值。
<settings>
    <setting name="logImpl" value="STDOUT_LOGGING"/>
    <setting name="mapUnderscoreToCamelCase" value="true"/>
    <setting name="cacheEnabled" value="true"/>
</settings>

2.4、类型别名(typeAliases)

类型别名可以为 Java 实体类型设置一个缩写名字,仅仅用于 MyBatis 映射文件中的参数类型与返回值类型,以便替换冗余的全限定类名。

标签说明

  • typeAlias 子标签(单个设置):根据全限定类名设置类型别名。type 为全限定类名,Alias 为类型别名。
  • package 子标签(批量设置):根据包名设置类型别名。name 为包名,默认别名为类名小写(如果同时存在 @Alias 注解,别名为注解值)。
<typeAliases>
    <typeAlias type="com.feng.pojo.Book" alias="book"/>
    <package name="com.feng.pojo"/><!--如果同时存在 @Alias 注解,别名为注解值-->
</typeAliases>
@Data
@AllArgsConstructor
@NoArgsConstructor
// @Alias("myBook")// 别名为 myBook
public class Book {
    private int bid;
    private String bname;
    private int bcount;
}

内置 Java 类型别名

基本数据类型的类型别名为前面加“_”。

引用数据类型的类型别名为类名小写。主要包括:包装类、String 类、Date 类、Object 类及其数组、集合类(List、ArrayList、Map、HashMap、Collection、Iterator)、大数据类及其数组(BigDecimal、BigInteger、BigDecimal[]、BigInteger[])。

注意:这些内置 Java 类型别名不区分大小写,并且基本数据类型与包装类的类型别名可以替换使用。例如:映射接口的返回值类型为 int,对应映射文件的 resultType 存在多种写法。

public interface BookMapper {
    int selectCount();
}
<select id="selectCount" resultType="int">
    select count(*) from book
</select>
<select id="selectCount" resultType="_int">
    select count(*) from book
</select>
<select id="selectCount" resultType="INT">
    select count(*) from book
</select>
<select id="selectCount" resultType="Integer">
    select count(*) from book
</select>
<select id="selectCount" resultType="_integer">
    select count(*) from book
</select>
<select id="selectCount" resultType="integer">
    select count(*) from book
</select>
<select id="selectCount" resultType="INTEGER">
    select count(*) from book
</select>

2.5、类型处理器(typeHandlers)

类型处理器用于实现 Java 类型与 JDBC 类型按照合适的方式互相转换。详细说明:

  • MyBatis 执行 SQL 语句前,设置预处理语句(PreparedStatement)的参数,会使用类型处理器将 Java 类型转换为合适的 JDBC 类型。底层调用的是 PreparedStatement 的 set() 方法。注意:如果在映射语句中使用 ${} 替换 #{},或者明确指定 statementType=“[STATEMENT|CALLABLE]”,则不支持。
  • MyBatis 执行 SQL 语句后,处理结果集(ResultSet)的结果,会使用类型处理器将 JDBC 类型转换为合适的 Java 类型。底层调用的是 ResultSet 的 get() 方法。

MyBatis 内置很多类型处理器用于类型处理(详见官网中文文档,源码见 org.apache.ibatis.type 包)。如果需要处理非标准的、或者不支持的数据类型可以重写已有的类型处理器,或者自定义类型处理器。

重写已有的类型处理器步骤(不太建议)

  • 首先继承已有的类型处理器,重写四个方法,泛型不变,只需更改方法实现。
  • 其次配置这个类型处理器。

自定义类型处理器步骤

  • 首先继承 org.apache.ibatis.type.BaseTypeHandler 抽象类,重写四个抽象方法,泛型为需要处理的 Java 类型。一个 setXXXX() 方法用于设置预处理语句(PreparedStatement)的参数类型,三个 getXXXX() 抽象方法用于处理结果集(ResultSet)的结果类型。
  • 其次配置这个类型处理器。

标签说明

  • typeHandler 子标签(单个设置):配置类型处理器。handler 为类型处理器的全限定类名,javaType 为 Java 类型,jdbcType 为 JDBC 类型。需要注意:这种写法如果同时使用 @MappedTypes 与 @MappedJdbcTypes 注解,javaType/jdbcType 会被覆盖。
  • package 子标签(批量设置):配置类型处理器。name 为类型处理器所在包名。注意:这种写法必须配合 @MappedTypes 与 @MappedJdbcTypes 注解。
<typeHandlers>
    <typeHandler handler="com.feng.typeHandler.DIYDateTypeHandler" javaType="date" jdbcType="VARCHAR"/>
    <package name="com.feng.typeHandler"/><!--这种写法必须配合 @MappedTypes 与 @MappedJdbcTypes 注解-->
</typeHandlers>

注解说明

  • @MappedTypes:注解在类型处理器类,表示处理的 Java 类型,参数同类型处理器的泛型。
  • @MappedJdbcTypes:注解在类型处理器类,表示处理的 JDBC 类型。

自定义类型处理器步骤(要求 Java 类型为 Date,JDBC 类型为 VARCHAR)

  • 步骤一、数据库添加字段(类型为 VARCHAR),实体类添加字段(类型为 Java.Util.Date)。
  • 步骤二、映射接口添加方法,映射文件添加映射语句。注意:使用类型处理器,必须指定具体生效字段,否则 MyBatis 无法识别。设置参数时配置在 SQL 语句中,处理结果时配置在 resultMap 结果映射中。
  • 步骤三、创建自定义类型处理器类,并在配置文件中进行配置。
  • 步骤四、测试。

示例

  • 数据库、实体类
ALTER TABLE `book` ADD `bdate` VARCHAR(100) COMMENT '添加时间';
private Date bdate;
  • 映射接口、映射文件
// 类型处理器测试
int insert(Book book);
Book selectById(int id);
<!--类型处理器测试-->
<insert id="insert" parameterType="book">
    insert into book (bid, bname, bcount, bdate) values (#{bid}, #{bname}, #{bcount}, #{bdate, typeHandler = com.feng.typeHandler.DIYDateTypeHandler})
</insert>
<resultMap id="selectById" type="book">
    <id property="bid" column="bid"/>
    <result property="bname" column="bname"/>
    <result property="bcount" column="bcount"/>
    <result property="bdate" column="bdate" typeHandler="com.feng.typeHandler.DIYDateTypeHandler"/>
</resultMap>
<select id="selectById" parameterType="int" resultMap="selectById">
    select * from book where bid = #{bid};
</select>
  • 自定义类型处理器类、MyBatis 配置文件
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(Date.class)
public class DIYDateTypeHandler extends BaseTypeHandler<Date> {
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Date parameter, JdbcType jdbcType) throws SQLException {
        ps.setString(i, new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(parameter));
    }

    @Override
    public Date getNullableResult(ResultSet rs, String columnName) throws SQLException {
        try {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(rs.getString(columnName));
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public Date getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        try {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(rs.getString(columnIndex));
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public Date getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        try {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(cs.getString(columnIndex));
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return null;
    }
}
<typeHandlers>
    <package name="com.feng.typeHandler"/><!--这种写法必须配合 @MappedTypes 与 @MappedJdbcTypes 注解-->
</typeHandlers>
  • 测试
public class T02_DIYDateTypeHandler {
    // 插入
    @Test
    public void test01() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        int count = mapper.insert(new Book(4, "Java 高级", 4, new Date()));
        System.out.println("count = " + count);
        sqlSession.commit();
        sqlSession.close();
    }

    // 查询
    @Test
    public void test02() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        Book book = mapper.selectById(4);
        System.out.println(book);
        sqlSession.close();
    }
}
# 插入:对象属性为 Date,参数类型为 String
==>  Preparing: insert into book (bid, bname, bcount, bdate) values (?, ?, ?, ?)
==> Parameters: 4(Integer), Java 高级(String), 4(Integer), 2022-11-11 21:25:13(String)
<==    Updates: 1
count = 1

# 查询:结果集类型为 String,对象属性为 Date
==>  Preparing: select * from book where bid = ?;
==> Parameters: 4(Integer)
<==    Columns: bid, bname, bcount, bdate
<==        Row: 4, Java 高级, 4, 2022-11-11 21:08:37
<==      Total: 1
Book(bid=4, bname=Java 高级, bcount=4, bdate=Fri Nov 11 21:08:37 CST 2022)

2.6、对象工厂(objectFactory)

MyBatis 将结果集中的数据封装为对象之前,先会使用一个对象工厂实例创建一个对象。默认对象工厂仅仅用于实例化目标类,可以调用无参、或者有参构造方法。如果需要返回特定对象,可以自定义对象工厂。

自定义对象工厂实现步骤

  • 步骤一、继承 DefaultObjectFactory 类,重写四个方法,这些方法全部来自 ObjectFactory 接口。
    • 两个 create():创建实例(一个处理无参构造,一个处理有参构造)。
    • isCollection():判断实例对象是否为集合。
    • setProperties():初始化对象工厂实例时调用该方法。
  • 步骤二、配置对象工厂。
  • 步骤三、测试。

objectFactory 标签说明

  • type 属性:自定义对象工厂的全限定类名。
  • property 子标签(可选):配置自定义对象工厂的初始化属性。键值对,name 为名称 value 为值。
<objectFactory type="com.feng.objectFactory.DIYObjectFactory">
    <property name="username" value="张三"/>
</objectFactory>

示例

  • 步骤一、继承 DefaultObjectFactory 类,重写四个方法,这些方法全部来自 ObjectFactory 接口。
public class DIYObjectFactory extends DefaultObjectFactory {
    private Properties properties;

    @Override
    public <T> T create(Class<T> type) {
        System.out.println("调用了无参构造");
        System.out.println("this.getProperties() = " + this.getProperties());
        return super.create(type);
    }

    @Override
    public <T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
        System.out.println("调用了有参构造");
        System.out.println("this.getProperties() = " + this.getProperties());
        return super.create(type, constructorArgTypes, constructorArgs);
    }

    @Override
    public <T> boolean isCollection(Class<T> type) {
        return super.isCollection(type);
    }

    @Override
    public void setProperties(Properties properties) {
        System.out.println(this.getProperties());
        this.properties = properties;
    }

    public Properties getProperties() {
        return properties;
    }
}
  • 步骤二、配置对象工厂。
<objectFactory type="com.feng.objectFactory.DIYObjectFactory">
    <property name="username" value="张三"/>
</objectFactory>
  • 步骤三、测试。
public class T03_DIYObjectFactory {
    @Test
    public void test01() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        Book book = mapper.selectById02(1);
        System.out.println(book);
        sqlSession.close();
    }
}
Opening JDBC Connection
Created connection 1207608476.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@47faa49c]
==>  Preparing: select * from book where bid = ?;
==> Parameters: 1(Integer)
调用了无参构造
this.getProperties() = {username=张三}
调用了有参构造
this.getProperties() = {username=张三}
<==    Columns: bid, bname, bcount, bdate
<==        Row: 1, Java 基础, 1, 2022-11-11 08:08:08
调用了无参构造
this.getProperties() = {username=张三}
调用了有参构造
this.getProperties() = {username=张三}
<==      Total: 1
Book(bid=1, bname=Java 基础, bcount=1, bdate=Fri Nov 11 08:08:08 CST 2022)
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@47faa49c]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@47faa49c]
Returned connection 1207608476 to pool.

2.7、插件拦截器(plugins)

MyBatis 允许在执行映射文件中的映射语句时,进行拦截调用。允许插件拦截调用以下方法:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

拦截器使用步骤

  • 首先实现 Interceptor 接口,至少重写核心方法 intercept()。
  • 然后配置插件拦截器。

标签说明

  • plugin 子标签:配置插件拦截器,可以自定义或者使用第三方。interceptor 为拦截器类。
    • property 子标签(可选):配置插件拦截器的初始化属性。键值对,name 为名称 value 为值。
<plugins>
    <plugin interceptor="com.feng.interceptor.DIYInterceptor">
        <property name="username" value="李四"/>
    </plugin>
</plugins>

1、自定义插件拦截器

// 拦截方法签名
@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}))
public class DIYInterceptor implements Interceptor {
    private Properties properties = new Properties();

    // 拦截器核心方法
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("执行目标方法之前");
        System.out.println("当前用户是:" + properties.get("username"));
        Object proceed = invocation.proceed();
        System.out.println("执行目标方法之后");
        return proceed;
    }

    // 获取 MyBatis 配置文件中的参数
    @Override
    public void setProperties(Properties properties) {
        this.properties = properties;
    }
}
<plugins>
    <plugin interceptor="com.feng.interceptor.DIYInterceptor">
        <property name="username" value="李四"/>
    </plugin>
</plugins>
  • 测试
public class T04_DIYInterceptor {
    @Test
    public void test01() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        List<Book> bookList = mapper.selectAll();
        bookList.forEach(System.out::println);
        sqlSession.close();
    }
}

2、使用第三方插件拦截器示例(PageHelper 分页)

导入 Maven 依赖,配置拦截器插件即可。

<!--PageHelper 分页助手插件-->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.2.0</version>
</dependency>
<plugins>
    <plugin interceptor="com.github.pagehelper.PageInterceptor"/>
</plugins>
  • 测试
public class T05_PageHelper {
    @Test
    public void test01() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);

        // 设置分页参数,然后查询所有
        Page<Object> page = PageHelper.startPage(1, 2);
        List<Book> books = mapper.selectAll();

        // 获取 PageInfo
        PageInfo<Book> pageInfo = new PageInfo<>(books);
        System.out.println("Total 总条数:" + pageInfo.getTotal());
        System.out.println("Size 总页数:" + pageInfo.getSize());
        System.out.println("PageNum 第几页:" + pageInfo.getPageNum());
        System.out.println("PageSize 页大小 :" + pageInfo.getPageSize());
        System.out.println("PrePage 上一页:" + pageInfo.getPrePage());
        System.out.println("NextPage 下一页:" + pageInfo.getNextPage());
        System.out.println("IsFirstPage 是否是第一页:" + pageInfo.isIsFirstPage());
        System.out.println("IsLastPage 是否是最后一页:"+ pageInfo.isIsLastPage());

        sqlSession.close();
    }
}

2.8、环境配置(environments)

MyBatis 允许同时配置多套环境,可以将映射文件中的映射语句应用到多个、或者多种数据库中。例如:开发、测试、生产环境需要不同的数据库配置,等等类似场景。注意:虽然可以同时配置多套环境,但是一个 SqlSessionFactory 实例只能选择其中一个。如果需要连接多个数据库,则需要创建多个 SqlSessionFactory 实例,一个数据库对应一个 SqlSessionFactory 实例。

标签说明

  • default 属性:默认使用的环境 ID。
  • environment 子标签:环境。id 为当前环境 ID。
    • transactionManager 子标签:事务管理器。type 为事务管理器类型。
    • dataSource 子标签:数据源。type 为数据源类型。
      • properties 属性:数据源属性键值对。name 为名称,value 为值并且可以使用 ${} 动态替换。
<environments default="development"><!--引用一个环境 ID-->
    <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>
    <environment id="test">
        <transactionManager type="JDBC"/>
        <dataSource type="UNPOOLED">
            <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
            <property name="url" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=Asia/Shanghai&amp;useSSL=true&amp;useUnicode=true&amp;characterEncoding=UTF-8"/>
            <property name="username" value="root"/>
            <property name="password" value="test123!"/>
        </dataSource>
    </environment>
</environments>

事务管理器类型(transactionManager)

MyBatis 提供两种事务管理器类型 type=“[JDBC|MANAGED]”(源码见 org.apache.ibatis.transaction 包):

  • JDBC:使用 JDBC 提交与回滚事务。
  • MANAGED:MyBatis 不负责提交与回滚事务,而是将其交给其他容器进行管理,例如 Web 服务器的应用上下文对象、Spring 容器等等。

数据源类型(dataSource)

MyBatis 提供三种内置数据源类型 type=“[UNPOOLED|POOLED|JNDI]”(源码见 org.apache.ibatis.dataSource 包):

  • UNPOOLED:每次请求打开与关闭数据库连接。UNPOOLED 需要配置以下内容:
    • 必选配置:driver、url、username、password。
    • 可选配置:autoCommit、defaultTransactionIsolationLevel、defaultNetworkTimeout。
  • POOLED:使用数据库连接池,每次请求结束会将数据库连接返还到”池“中。POOLED 需要配置以下内容:
    • 包含以上 UNPOOLED 中的配置。
    • 其他与“池”相关的配置,可选并提供默认值。
  • JNDI:使用 JNDI。MyBatis 使用 JNDI 中配置好的数据源获取数据库连接。例如 EJB(企业级 JavaBean)或者应用服务器 Tomcat 等等(不做介绍)。

数据源类型源码

  • UnpooledDataSource
public class UnpooledDataSource implements DataSource {
	// 必选配置
    private String driver;// JDBC 驱动的 Java 全限定类名
    private String url;// JDBC 驱动连接数据库的 URL 地址
    private String username;// 登录数据库的用户名
    private String password;// 登录数据库的密码
	// 可选配置
    private Boolean autoCommit;// 是否自动提交事务
    private Integer defaultTransactionIsolationLevel;// 默认事务隔离级别
    private Integer defaultNetworkTimeout;// 默认网络连接超时时间(单位:毫秒)
    ...
}
  • PooledDataSource
public class PooledDataSource implements DataSource {
    // 包含 UNPOOLED 中的配置
    private final UnpooledDataSource dataSource;
    // 其他与“池”相关的配置,可选并提供默认值
    protected int poolMaximumActiveConnections = 10;
    protected int poolMaximumIdleConnections = 5;
    protected int poolMaximumCheckoutTime = 20000;
    protected int poolTimeToWait = 20000;
    protected int poolMaximumLocalBadConnectionTolerance = 3;
    protected String poolPingQuery = "NO PING QUERY SET";
    protected boolean poolPingEnabled;
    protected int poolPingConnectionsNotUsedFor;
    ...
}

UNPOOLED 与 POOLED 日志对比

  • UNPOOLED:每次请求打开与关闭数据库连接。
  • POOLED:每次请求结束会将数据库连接返还到”池“中。
Opening JDBC Connection
......
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@69e1dd28]
Opening JDBC Connection
Created connection 1926096844.
......
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@72cde7cc]
Returned connection 1926096844 to pool.

2.9、数据库厂商标识(databaseIdProvider)

数据库厂商标识允许 MyBatis 根据不同的数据库厂商执行不同的映射语句。使用步骤如下:

  • 首先在 MyBatis 配置文件中配置数据库厂商。
  • 然后在映射文件的映射语句中配置 databaseId 即可。

注意:如果映射文件的映射语句配置 databaseId,则 id 相同的映射语句可以存在多个。带有 databaseId 的为专用语句,不带 databaseId 的为通用语句。MyBatis 会同时加载所有语句,并且优先执行专用语句。

标签说明

  • type:数据库厂商标识类型。提供一种 type=“DB_VENDOR”。用于处理数据库厂商标识,详见 org.apache.ibatis.mapping.VendorDatabaseIdProvider 类。
  • property 子标签:配置数据库厂商别名键值对。name 为厂商名称,value 为别名。
<databaseIdProvider type="DB_VENDOR">
    <property name="MySQL" value="mysql"/>
    <property name="SQL Server" value="sqlserver"/>
    <property name="DB2" value="db2"/>
    <property name="Oracle" value="oracle"/>
</databaseIdProvider>

示例

  • 配置文件、映射文件
<databaseIdProvider type="DB_VENDOR">
    <property name="MySQL" value="mysql"/>
    <property name="SQL Server" value="sqlserver"/>
    <property name="DB2" value="db2"/>
    <property name="Oracle" value="oracle"/>
</databaseIdProvider>
<!--数据库厂商标识测试-->
<select id="selectAll" resultType="book" databaseId="mysql">
    -- 数据库厂商标识测试
    select * from book
</select>
  • 测试
public class T06_DataBaseId {
    @Test
    public void test01() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper bookMapper = sqlSession.getMapper(BookMapper.class);
        List<Book> bookList = bookMapper.selectAll();
        bookList.forEach(System.out::println);
        sqlSession.close();
    }
}

2.10、映射器(mappers)

MyBatis 配置完成之后,需要告诉 MyBatis 去哪里执行 SQL 语句,需要使用映射器关联 MyBatis 映射文件/接口

标签说明

  • mapper 子标签(单个设置):设置映射文件的位置。resource 为类路径,url 为网络路径,class 为全限定类名。
  • package 子标签(批量设置):设置映射接口的位置。name 为映射文件所在包名。

注意:如果使用注解,映射器只能使用下面两种。

<mappers>
    <mapper resource="com/feng/dao/BookMapper.xml"/>
    <mapper url="file:///com/feng/dao/BookMapper.xml"/>
    <mapper class="com.feng.dao.BookMapper"/>
    <package name="com.feng.dao"/>
</mappers>

3、MyBatis 映射文件 - 单表

3.1、介绍

命名空间(namespace)

一个 MyBatis 映射文件对应一个的 MyBatis 映射接口。命名空间用于关联对应的映射接口。

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

顶级标签

<select> 映射查询语句
<insert> 映射插入语句
<delete> 映射删除语句
<update> 映射更新语句

<resultMap> 结果映射,封装处理数据库结果集中的对象

<cache> 该命名空间的二级缓存配置
<cache-ref> 引用其他命名空间的二级缓存配置

<sql> SQL 片段抽取复用

select、insert、delete、update 标签说明

  • select、insert、delete、update 通用属性:
    • id:唯一标识符,可被其他地方引用(嵌套)。
    • parameterType:参数类型,可以是全限定了类名或者别名。可选,MyBatis 可以通过类型处理器(TypeHandler)进行类型推断。
    • flushCache:如果设置为 true,只要调用当前语句,本地缓存与二级缓存都会被清空。对 select 默认为 false。对 insert、update、delete 默认为 true。
    • timeout:请求结果超时时间,单位为秒。默认为未设置(依赖数据库驱动)。
    • statementType:使用哪种语句类型。可选 STATEMENT、PREPARED、CALLABLE。默认为 PREPARED。
    • databaseId:如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有不带 databaseId 或匹配当前 databaseId 的语句;如果带和不带的语句都有,则不带的会被忽略。
  • select 特有属性(结果集相关):
    • resultType:结果类型,可以是全限定类名或者别名。注意:如果返回集合,应该设置为集合包含的类型,而不是集合本身的类型。resultType 与 resultMap 只能二选一。
    • resultMap:结果映射,引用一个 resultMap 标签。最强大最复杂。resultType 与 resultMap 只能二选一。
    • useCache:如果设置为 true,只要调用当前语句,二级缓存就会将结果存储起来。默认为 true。
    • fetchSize:请求结果返回行数。默认为未设置(依赖数据库驱动)。
    • resultSetType:使用哪种结果集类型。可选 FORWARD_ONLY、SCROLL_SENSITIVE、SCROLL_INSENSITIVE、DEFAULT(未设置)。默认为未设置(依赖数据库驱动)。
    • resultOrdered:仅仅用于嵌套查询。默认为 false。
    • resultSets:仅仅用于多结果集。
  • insert、update 特有属性(主键相关):
    • useGeneratedKeys:使用主键自增。需要 MySQL、SQL Server 等数据库设置 ID 自增。默认为 false。
    • keyProperty:生成键值对应的数据库列名。
    • keyColumn:设置生成键值在表中的列名,在某些数据库(像 PostgreSQL)中,当主键列不是表中的第一列的时候,是必须设置的。如果生成列不止一个,可以用逗号分隔多个属性名称。

3.2、基本 CRUD、分页、模糊

MyBatis 配置完成以后,仅需创建映射接口,映射文件,就可进行测试。注意:更新操作(insert、delete、update)需要提交回滚事务,查询操作(select)则不需要。

  • 手动提交回滚事务:sqlSession.commit()、sqlSession.rollback()。
  • 自动提交回滚事务:获取 SqlSession 实例时设置自动提交即可。例如 return sqlSessionFactory.openSession(true)。

基本 CURD

  • selectAll、insert、delete、update、select。

分页查询三种方式

  • 方式一、使用 PageHelper 分页助手插件(详见插件拦截器)。
  • 方式二、使用 MySql 的 Limit 关键字分页。
  • 方式三、使用 MyBatis 自带的 RowBounds 工具分页。

模糊查询四种方式

  • 方式一、在 Java 代码中拼接通配符。
  • 方式二、在 SQL 语句中拼接通配符,注意:需要使用 ${} 代替 #{}。

示例

  • 映射接口、映射文件
public interface BookMapper {
    // 基本 CURD
    List<Book> selectAll();// 查询所有
    void insert(Book book);// 插入
    void delete(int id);// 删除
    void update(Book book);// 更新
    Book selectById(int id);// 根据 ID 查询

    // 分页查询
    List<Book> selectPageByLimit(Map<String, Integer> map);// Limit
    List<Book> selectPageByRowBounds();// RowBounds

    // 模糊查询
    List<Book> selectLike01(String name);
    List<Book> selectLike02(String name);
    List<Book> selectLike03(String name);
    List<Book> selectLike04(String name);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.feng.dao.BookMapper">
    <!--基本 CRUD-->
    <select id="selectAll" resultType="book">
        select * from book
    </select>
    <insert id="insert" parameterType="book">
        insert into book (bid, bname, bcount) values (#{bid}, #{bname}, #{bcount})
    </insert>
    <delete id="delete" parameterType="int">
        delete from book where bid = #{bid}
    </delete>
    <update id="update" parameterType="book">
        update book set bname = #{bname}, bcount = #{bcount} where bid = #{bid}
    </update>
    <select id="selectById" parameterType="int" resultType="book">
        select * from book where bid = #{bid}
    </select>

    <!--分页查询-->
    <select id="selectPageByLimit" parameterType="map" resultType="book">
        select * from book limit #{offset}, #{limit}
    </select>
    <select id="selectPageByRowBounds" resultType="book">
        select * from book
    </select>

    <!--模糊查询-->
    <select id="selectLike01" parameterType="string" resultType="book">
        select * from book where bname like #{bname}
    </select>
    <select id="selectLike02" parameterType="string" resultType="book">
        select * from book where bname like '%${bname}%'
    </select>
    <select id="selectLike03" parameterType="string" resultType="book">
        select * from book where bname like concat('%', #{bname}, '%')
    </select>
    <select id="selectLike04" parameterType="string" resultType="book">
        select * from book where bname like concat('%', '${bname}', '%')
    </select>
</mapper>
  • 测试
// 基本 CRUD
public class T01_BaseCRUD {
    @Test
    public void test01() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        List<Book> bookList = mapper.selectAll();
        bookList.forEach(System.out::println);
        sqlSession.close();
    }

    @Test
    public void test02() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        mapper.insert(new Book(4, "Java 高级", 4));
        sqlSession.commit();
        sqlSession.close();
    }

    @Test
    public void test03() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        mapper.delete(4);
        sqlSession.commit();
        sqlSession.close();
    }

    @Test
    public void test04() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        Book book = mapper.selectById(1);
        book.setBcount(100);
        mapper.update(book);
        sqlSession.commit();
        sqlSession.close();
    }

    @Test
    public void test05() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        Book book = mapper.selectById(1);
        System.out.println(book);
        sqlSession.close();
    }
}
// 分页查询
public class T02_SelectPage {
    @Test
    public void test01() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        Map<String, Integer> map = new HashMap<>();
        map.put("offset", 0);
        map.put("limit", 2);
        List<Book> bookList = mapper.selectPageByLimit(map);
        bookList.forEach(System.out::println);
        sqlSession.close();
    }

    @Test
    public void test02() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        List<Book> bookList = sqlSession.selectList("com.feng.dao.BookMapper.selectPageByRowBounds", null, new RowBounds(0, 2));
        bookList.forEach(System.out::println);
        sqlSession.close();
    }
}
// 模糊查询
public class T03_SelectLike {
    @Test
    public void test01() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        List<Book> bookList = mapper.selectLike01("%级%");
        bookList.forEach(System.out::println);
        sqlSession.close();
    }

    @Test
    public void test02() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        List<Book> bookList = mapper.selectLike02("级");
        bookList.forEach(System.out::println);
        sqlSession.close();
    }

    @Test
    public void test03() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        List<Book> bookList = mapper.selectLike03("级");
        bookList.forEach(System.out::println);
        sqlSession.close();
    }

    @Test
    public void test04() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        List<Book> bookList = mapper.selectLike04("级");
        bookList.forEach(System.out::println);
        sqlSession.close();
    }
}

3.3、使用注解

使用 MyBatis 注解配置 SQL,可以完全代替 MyBatis 映射文件,甚至不写。

注解说明

MyBatis 注解位于 org.apache.ibatis.annotations 包下。

@Select 源码:

  • String[] value():SQL 语句。
  • String databaseId() default “”:数据库 ID。
  • @interface List {}:包含 Select 注解。
public @interface Select {
  String[] value();// SQL 语句
  String databaseId() default "";// 数据库 ID
  @Documented
  @Retention(RetentionPolicy.RUNTIME)
  @Target(ElementType.METHOD)
  @interface List {// 包含  Select 注解。
    Select[] value();
  }
}

需要注意,如果使用注解,映射器只能使用注册映射接口的形式

<mappers>
    <!--注册映射文件-->
    <mapper resource="com/feng/dao/BookMapper.xml"/>
    <mapper url="file:///com/feng/mapper/BookMapper.xml"/>
    <!--注册映射接口-->
    <mapper class="com.feng.dao.BookMapper"/>
    <package name="com.feng.dao"/>
</mappers>

示例

  • 映射接口
public interface BookMapper {
    // 基本 CURD
    @Select("select * from book")
    List<Book> selectAll();// 查询所有
    @Insert("insert into book (bid, bname, bcount) values (#{bid}, #{bname}, #{bcount})")
    void insert(Book book);// 插入
    @Delete("delete from book where bid = #{bid}")
    void delete(int id);// 删除
    @Update("update book set bname = #{bname}, bcount = #{bcount} where bid = #{bid}")
    void update(Book book);// 更新
    @Select("select * from book where bid = #{bid}")
    Book selectById(int id);// 根据 ID 查询

    // 分页查询
    @Select("select * from book limit #{offset}, #{limit}")
    List<Book> selectPageByLimit(Map<String, Integer> map);// Limit
    @Select("select * from book")
    List<Book> selectPageByRowBounds();// RowBounds

    // 模糊查询
    @Select("select * from book where bname like #{bname}")
    List<Book> selectLike01(String name);
    @Select("select * from book where bname like '%${bname}%'")
    List<Book> selectLike02(String name);
    @Select("select * from book where bname like concat('%', #{bname}, '%')")
    List<Book> selectLike03(String name);
    @Select("select * from book where bname like concat('%', '${bname}', '%')")
    List<Book> selectLike04(String name);
}
  • 配置文件
<mappers>
    <!--<mapper class="com.feng.dao.BookMapper"/>-->
    <package name="com.feng.dao"/>
</mappers>

3.4、#{} 与 ${} 的区别

#{} 称为参数占位符,MyBatis 会先创建 PreparedStatement 再传入参数,相当于 ?。通常用于传入 SQL 语句的参数。使用较多,非常安全。

${} 称为字符串拼接,MyBatis 会先传入参数拼接为完整 SQL 语句,再执行。通常用于传入 SQL 语句的表名、列名等等(分表)。特殊场景使用,会导致 SQL 注入风险,除非对这些数据加以校验。

3.5、参数映射

对于简单类型或者原始类型(int、Integer、String),MyBatis 直接使用他们的值作为参数。因此如果只有一个参数,可以不用指定 parameterType,并且这个参数名称可以随意命名。例如:

<select id="selectById" parameterType="int" resultType="book">
    select * from book where bid = #{bid}
</select>

<select id="selectById" resultType="book">
    select * from book where bid = #{id}
</select>

如果参数类型为 Java 对象(例如 Book),必须指定 parameterType,参数名称对应属性名称。例如:

<insert id="insert" parameterType="book">
    insert into book (bid, bname, bcount) values (#{bid}, #{bname}, #{bcount})
</insert>

其次,如果使用类型处理器,参数需要指定 javaType、jdbcType、typeHandler,具体参考类型处理器,很少使用,使用上面两种简写方式即可。

#{bdate, javaType = date, jdbcType = VARCHAR, typeHandler = com.feng.typeHandler.DIYDateTypeHandler}

3.6、结果映射(resultMap)

结果映射用于复杂结果(具体参考多表查询),或者使用类型处理器处理 MyBaits 不支持的数据类型(具体参考类型处理器)。

关于实体类属性名与数据库列名不一致问题。有三种解决方式:

  • 使用结果映射
  • 开启驼峰命名
  • 使用 SQL 语句别名

示例

  • 使用结果映射
<!--结果映射-->
<select id="selectResultMap" resultMap="resultMap">
    select * from book
</select>
<resultMap id="resultMap" type="book">
    <result property="id" column="bid"/>
    <result property="name" column="bname"/>
    <result property="count" column="bcount"/>
</resultMap>
  • 开启驼峰命名
<setting name="mapUnderscoreToCamelCase" value="true"/>
  • 使用 SQL 语句别名
select bid id, bname name, bcount count from book;
select bid as id, bname as name, bcount as count from book;

3.7、缓存(了解即可)

缓存是存储在内存中的临时数据。通常会将用户经常查询的数据存放到缓存(内存)中,减少用户直接查询数据库(磁盘),从而提高效率,解决高并发系统的性能问题。

经常查询且不经常改变的数据,适合使用缓存。反之,不经常查询且经常改变的数据,不适合使用缓存。

MyBatis 缓存特性:

  • 映射文件中的所有 select 语句结果会被缓存。
  • 映射文件中的所有 insert、update 和 delete 语句会刷新缓存。
  • 根据缓存清理策略,自动清理缓存。
  • 调用 clearCache() 方法,手动清理缓存。
  • 调用 commit()、roolback() 方法,会刷新缓存。

MyBatis 提供两种缓存,一级缓存与二级缓存。用户访问顺序如下:首先是二级缓存,其次是一级缓存,最后是数据库。

一级缓存

也叫本地会话缓存(SqlSession),默认开启因此无需配置(没什么用)。

二级缓存

属于事务性的缓存,默认关闭并且需要手动配置。可以是不同的 SqlSession,由于其针对的是同一个命名空间(命名空间),因此可能存在问题:一个命名空间更改了某些数据,另一个命名空间缓存没有更新的情况(不建议使用)。

二级缓存标签说明

  • cache:该命名空间的二级缓存配置。属性:
    • eviction:二级缓存清除策略。提供四种 eviction = “[LRU|FIFO|SOFT|WEAK]”,LRU 为按照最近最少使用移除,FIFO 为按照先进先出移除,SOFT 为软引用(基于垃圾回收器与软引用规则移除),WEAK 为弱引用(更积极地基于垃圾回收器与弱引用规则移除)。默认为 LRU。
    • flushInterval:二级缓存刷新间隔时间。单位:毫秒。默认为不设置,也就是不刷新,只在调用该语句时刷新。
    • size:二级缓存引用对象数量。需要注意运行环境或者服务器的内存大小。默认为 1024。
    • readOnly:是否只读。提供两种 readOnly= “[true|false]”,true 为只读,返回用户相同的缓存对象实例,提升性能, false 为可读可写,返回用户不同的缓存对象实例(通过序列化拷贝),速度慢更安全。默认为 false。
  • cache-ref:引用其他命名空间的二级缓存配置。
    • namespace:引用命名空间。
<cache
       eviction="FIFO"
       flushInterval="60000"
       size="512"
       readOnly="true"/>
<cache-ref namespace=""/>

一级缓存示例

  • 测试
public class T04_Cache {
    // 一级缓存
    @Test
    public void test01() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        List<Book> bookList = mapper.selectAll();
        bookList.forEach(System.out::println);
        System.out.println("========");

        // 同一个 SqlSession
        BookMapper mapper02 = sqlSession.getMapper(BookMapper.class);
        sqlSession.getMapper(BookMapper.class);
        List<Book> bookList02 = mapper02.selectAll();
        bookList02.forEach(System.out::println);
        sqlSession.close();
    }
}
  • 结果:第二次查询没有执行 SQL,数据来自缓存。
Opening JDBC Connection
Created connection 137460818.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@8317c52]
==>  Preparing: select * from book
==> Parameters: 
<==    Columns: bid, bname, bcount, bdate
<==        Row: 1, Java 基础, 1, null
<==        Row: 2, Java 中级, 2, null
<==        Row: 3, Java 高级, 3, null
<==      Total: 3
Book(bid=1, bname=Java 基础, bcount=1)
Book(bid=2, bname=Java 中级, bcount=2)
Book(bid=3, bname=Java 高级, bcount=3)
========
Book(bid=1, bname=Java 基础, bcount=1)
Book(bid=2, bname=Java 中级, bcount=2)
Book(bid=3, bname=Java 高级, bcount=3)
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@8317c52]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@8317c52]
Returned connection 137460818 to pool.

二级缓存示例

  • 首先在配置文件中开启二级缓存。
  • 其次在映射文件中配置二级缓存。
<setting name="cacheEnabled" value="true"/>
// 二级缓存测试方法
List<Book> select();
<!--二级缓存配置-->
<cache
       eviction="FIFO"
       flushInterval="60000"
       size="512"
       readOnly="true"/>
<select id="select" resultType="book" useCache="true">
    select * from book
</select>
  • 测试
// 二级缓存
@Test
public void test02() {
    SqlSession sqlSession = MybatisUtil.getSqlSession();
    BookMapper mapper = sqlSession.getMapper(BookMapper.class);
    List<Book> bookList = mapper.select();
    bookList.forEach(System.out::println);
    sqlSession.close();
    System.out.println("========");

    // 重新获取 SqlSession
    SqlSession sqlSession02 = MybatisUtil.getSqlSession();
    BookMapper mapper02 = sqlSession02.getMapper(BookMapper.class);
    sqlSession02.getMapper(BookMapper.class);
    List<Book> bookList02 = mapper02.select();
    bookList02.forEach(System.out::println);
    sqlSession02.close();
}
  • 结果:第二次查询没有执行 SQL,因为不是同一个 SqlSession,所以数据来自二级缓存。(缓存命中率 0.5:Cache Hit Ratio [com.feng.dao.BookMapper]: 0.5)。
Cache Hit Ratio [com.feng.dao.BookMapper]: 0.0
Opening JDBC Connection
Created connection 285133380.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@10feca44]
==>  Preparing: select * from book
==> Parameters: 
<==    Columns: bid, bname, bcount, bdate
<==        Row: 1, Java 基础, 1, null
<==        Row: 2, Java 中级, 2, null
<==        Row: 3, Java 高级, 3, null
<==      Total: 3
Book(bid=1, bname=Java 基础, bcount=1)
Book(bid=2, bname=Java 中级, bcount=2)
Book(bid=3, bname=Java 高级, bcount=3)
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@10feca44]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@10feca44]
Returned connection 285133380 to pool.
========
Cache Hit Ratio [com.feng.dao.BookMapper]: 0.5
Book(bid=1, bname=Java 基础, bcount=1)
Book(bid=2, bname=Java 中级, bcount=2)
Book(bid=3, bname=Java 高级, bcount=3)

4、MyBatis 映射文件 - 多表

4.1、多表模型介绍

多表模型

  • 一对一(例如:一个 Person 对应一个 Address):
    • 数据库:选择其中一张为主表,另一张为从表(外键)。例如:PERSON 为主表,ADDRESS 为从表(外键)。
    • 实体类:两个实体类中分别包含一个对象。例如:Person 包含一个 Address 对象,Address 包含一个 Person 对象。
  • 一对多/多对一(例如:一个 Customer 对应多个 Order):
    • 数据库:选择“一的”表为主表,“多的”表为从表(外键)。例如:CUSTOMER 为主表,ORDER 为从表(外键)。
    • 实体类:“一的”实体类中包含一个集合,“多的”实体类中包含一个对象。例如:Customer 包含一个 Order 集合,Order 包含一个 Customer 对象。
  • 多对多(例如:多个 User 对应多个 Role):
    • 数据库:使用中间表作为从表,至少包含两列外键,关联另外两张主表。例如 USERROLE 为从表(两列外键)。
    • 实体类:两个实体类中分别包含一个集合。

使用外键注意事项

传统情况使用数据库级别的外键确定表与表之间的关系。优点是可以确保数据的完成性,缺点是级联操作非常麻烦。

实际开发中要求不能使用数据库级别的外键,表与表之间使用 ID 关联即可但不设置为外键,所有情况使用代码解决

级联操作

  • 级联添加:先添加主表,再添加从表(外键)。
  • 级联删除:先删除从表(外键),再删除主表。
  • 级联修改:因为主键通常不会修改,外键可以随意修改。因此级联修改不受影响。

多表查询

  • 可以使用关联查询、嵌套查询。

4.2、一对一

一对一(例如:一个 Person 对应一个 Address)

  • 数据库:选择其中一张为主表,另一张为从表(外键)。例如:PERSON 为主表,ADDRESS 为从表(外键)。
  • 实体类:两个实体类中分别包含一个对象。例如:Person 包含一个 Address 对象,Address 包含一个 Person 对象。

示例

  • 数据库、实体类
USE `mybatis`;

-- 主表
CREATE TABLE `person` (
	`person_id` INT,
	`person_name` VARCHAR(50),
	PRIMARY KEY (`person_id`)
);
INSERT INTO `person` (`person_id`, `person_name`) VALUES
(1, '张三'),
(2, '李四'),
(3, '王五');

-- 从表
CREATE TABLE `address` (
	`address_id` INT,
	`address_name` VARCHAR(50),
	`person_id` INT,
	PRIMARY KEY (`address_id`),
	CONSTRAINT `address_person_id` FOREIGN KEY (`person_id`) REFERENCES `person` (`person_id`)
);
INSERT INTO `address` (`address_id`, `address_name`, `person_id`) VALUES
(1, '陕西西安', 1),
(2, '河南郑州', 2),
(3, '四川成都', 3);
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {
    private Integer personId;
    private String presonName;
    private Address address;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Address {
    private Integer addressId;
    private String addressName;
    private Person person;
}
  • 映射接口、映射文件
public interface AddressDao {
    List<Address> selectAll();

    List<Address> selectAll02();
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.feng.dao.AddressDao">
    <!--标准查询-->
    <select id="selectAll" resultMap="selectAll" >
        select * from address, person where address.person_id = person.person_id
    </select>
    <resultMap id="selectAll" type="address">
        <result property="addressId" column="address_id"/>
        <result property="addressName" column="address_name"/>
        <association property="person" javaType="person">
            <result property="personId" column="person_id"/>
            <result property="personName" column="person_name"/>
        </association>
    </resultMap>

    <!--嵌套查询-->
    <select id="selectAll02" resultMap="selectAll02" >
        select * from address
    </select>
    <resultMap id="selectAll02" type="address">
        <result property="addressId" column="address_id"/>
        <result property="addressName" column="address_name"/>
        <association property="person" javaType="person" column="person_id" select="selectPerson"/>
    </resultMap>
    <select id="selectPerson" parameterType="int" resultType="person">
        select * from person where person_id = #{personId}
    </select>
</mapper>

4.3、一对多/多对一

一对多/多对一(例如:一个 Customer 对应多个 Order):

  • 数据库:选择“一的”表为主表,“多的”表为从表(外键)。例如:CUSTOMER 为主表,ORDER 为从表(外键)。
  • 实体类:“一的”实体类中包含一个集合,“多的”实体类中包含一个对象。例如:Customer 包含一个 Order 集合,Order 包含一个 Customer 对象。

示例

  • 数据库、实体类
USE `mybatis`;

-- 主表
CREATE TABLE `customer` (
	`customer_id` INT,
	`customer_name` VARCHAR(50),
	PRIMARY KEY (`customer_id`)
);
INSERT INTO `customer` (`customer_id`, `customer_name`) VALUES
(1, '刘德华'),
(2, '张学友'),
(3, '周润发');

-- 从表
CREATE TABLE `order` (
	`order_id` INT,
	`order_name` VARCHAR(50),
	`customer_id` INT,
	PRIMARY KEY (`order_id`),
	CONSTRAINT `order_customer_id` FOREIGN KEY (`customer_id`) REFERENCES `customer` (`customer_id`)
);
INSERT INTO `order` (`order_id`, `order_name`, `customer_id`) VALUES
(1, '小米14', 1),
(2, '小米15', 2),
(3, '小米16', 3),
(4, '苹果11', 1),
(5, '苹果12', 2),
(6, '苹果13', 3);
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Customer {
    private Integer customerId;
    private String customerName;
    List<Order> orderList;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
    private Integer orderId;
    private String orderName;
    private Customer customer;
}
  • 映射接口、映射文件
public interface CustomerDao {
    List<Customer> selectAll();

    List<Customer> selectAll02();
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.feng.dao.CustomerDao">
    <select id="selectAll" resultMap="selectAll">
        select * from customer, mybatis.order where customer.customer_id = order.customer_id
    </select>
    <resultMap id="selectAll" type="customer">
        <result property="customerId" column="customer_id"/>
        <result property="customerName" column="customer_name"/>
        <collection property="orderList" javaType="list" ofType="order">
            <result property="orderId" column="order_id"/>
            <result property="orderName" column="order_name"/>
        </collection>
    </resultMap>

    <select id="selectAll02" resultMap="selectAll02">
        select * from customer
    </select>
    <resultMap id="selectAll02" type="customer">
        <result property="customerId" column="customer_id"/>
        <result property="customerName" column="customer_name"/>
        <collection property="orderList" javaType="list" ofType="order" column="customer_id" select="selectOrder"/>
    </resultMap>
    <select id="selectOrder" parameterType="int" resultType="order">
        select * from mybatis.order where customer_id = #{customerId}
    </select>
</mapper>

4.4、多对多

多对多(例如:多个 User 对应多个 Role):

  • 数据库:使用中间表作为从表,至少包含两列外键,关联另外两张主表。例如 USERROLE 为从表(两列外键)。
  • 实体类:两个实体类中分别包含一个集合。

示例

  • 数据库、实体类
USE `mybatis`;

-- 主表
CREATE TABLE `user` (
	`user_id` INT,
	`user_name` VARCHAR(50),
	`user_password` VARCHAR(50),
	PRIMARY KEY (`user_id`)
);
INSERT INTO `user` (`user_id`, `user_name`, `user_password`) VALUE
(1, '张三', '111'),
(2, '李四', '222'),
(3, '王五', '333' );

-- 主表
CREATE TABLE `role` (
	`role_id` INT,
	`role_name` VARCHAR(50),
	PRIMARY KEY (`role_id`)
);
INSERT INTO `role` (`role_id`, `role_name`) VALUE
(1, '超级管理员'),
(2, '管理员'),
(3, '普通用户');

-- 中间表
CREATE TABLE `userrole` (
	`user_id` INT,
	`role_id` INT,
	CONSTRAINT `userrole_id_01` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`),
	CONSTRAINT `userrole_id_02` FOREIGN KEY (`role_id`) REFERENCES `role` (`role_id`)
);
INSERT INTO `userrole` (`user_id`, `role_id`) VALUES
(1, 1),
(1, 2),
(1, 3),
(2, 2),
(2, 3),
(3, 3);
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Role {
    private Integer roleId;
    private String roleName;
    private List<User> userList;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer userId;
    private String userName;
    private String userPassword;
    private List<Role> roleList;
}
  • 映射接口、映射文件
public interface UserDao {
    List<User> selectAll();

    List<User> selectAll02();
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.feng.dao.UserDao">
    <!--多对多标准查询-->
    <select id="selectAll" resultMap="selectAll">
        select * from user, role, userrole where user.user_id = userrole.user_id and role.role_id = userrole.role_id
    </select>
    <resultMap id="selectAll" type="user">
        <result property="userId" column="user_id"/>
        <result property="userName" column="user_name"/>
        <result property="userPassword" column="user_password"/>
        <collection property="roleList" javaType="list" ofType="role">
            <result property="roleId" column="role_id"/>
            <result property="roleName" column="role_name"/>
        </collection>
    </resultMap>

    <!--多对多嵌套查询-->
    <select id="selectAll02" resultMap="selectAll02">
        select * from user
    </select>
    <resultMap id="selectAll02" type="user">
        <result property="userId" column="user_id"/>
        <result property="userName" column="user_name"/>
        <result property="userPassword" column="user_password"/>
        <collection property="roleList" javaType="list" ofType="role" column="user_id" select="selectUserRole"/>
    </resultMap>
    <select id="selectUserRole" parameterType="int" resultType="role">
        select * from role, userrole where role.role_id = userrole.role_id and user_id = #{userId}
    </select>
</mapper>

4.5、注解一对一

public interface PersonDao {
    @Select("select * from person")
    @Results({
            @Result(property = "personId", column = "person_id"),
            @Result(property = "personName", column = "person_name"),
            @Result(
                    property = "address",
                    javaType = Address.class,
                    column = "person_id",
                    one = @One(select = "com.feng.dao.AddressDao.select")
            )
    })
    List<Person> selectAll();
}
public interface AddressDao {
    @Select("select * from address where person_id = #{personId}")
    Address select(int id);
}

4.6、注解一对多/多对一

public interface CustomerDao {
    @Select("select * from customer")
    @Results({
            @Result(property = "customerId", column = "customer_id"),
            @Result(property = "customerName", column = "customer_name"),
            @Result(
                    property = "orderList",
                    javaType = List.class,
                    column = "customer_id",
                    many = @Many(select = "com.feng.dao.OrderDao.selectByCustomerId")
            )
    })
    List<Customer> selectAll();
}
public interface OrderDao {
    @Select("select * from mybatis.order where customer_id = #{customerId}")
    List<Order> selectByCustomerId(int id);
}

4.7、注解多对多

public interface UserDao {
    @Select("select * from user")
    @Results({
            @Result(property = "userId", column = "user_id"),
            @Result(property = "userName", column = "user_name"),
            @Result(property = "userPassword", column = "user_password"),
            @Result(
                    property = "roleList",
                    javaType = List.class,
                    column = "user_id",
                    many = @Many(select = "com.feng.dao.RoleDao.selectByUserId")
            )
    })
    List<User> selectAll();
}
public interface RoleDao {
    @Select("select * from role, userrole where role.role_id = userrole.role_id and user_id = #{userId}")
    List<Role> selectByUserId(int id);
}

5、MyBatis 映射文件 - 动态 SQL

5.1、介绍

MyBatis 可以根据不同的条件,动态生成不同的 SQL 语句。通常用在两个地方:

  • 多条件查询:如果某些字段为 null,希望 SQL 语句中不出现该字段,而不是将 null 值作为查询条件。
  • 多字段更新:如果某些字段为 null,希望 SQL 语句中不出现该字段,而不是将 null 值作为数据插入。
-- 多条件查询
select * from book where bid = ? bname = ? and bcount = ?;
-- 多字段更新
update book set bname = ?, bcount = ? where bid = ?

动态 SQL 标签说明

  • 条件语句 if:如果条件成立则拼接该语句。
  • 条件语句 choose when otherwise:如果条件成立则拼接该语句,所有条件都不成立拼接 otherwise 中的语句。类似 Java 中的 switch case default 语句。
  • 关键字 where:至少一个条件成立,则会生成 where 关键字,并且会去除多余的 and/or 关键字。通常搭配 if、choose when otherwise 使用。
  • 关键字 set:至少一个条件成立,则会生成 set 关键字,并且会去除多余的逗号。通常搭配 if、choose when otherwise 使用。
  • 通用语句 trim(代替 where、set):prefix 为前缀、suffix 为后缀、prefixOverrides 为前缀重写(自动去除不需要的符号)、suffixOverrides 为后缀重写(自动去除不需要的符号)。
  • 循环语句 foreach:用来简化 SQL 语句中多次重复的内容。通常用于批量插入、删除、修改。例如 where in/or id = (?, ?, ?)、value (?, ?, ?), (?, ?, ?), (?, ?, ?) 等等。
  • SQL 片段抽取复用
    • sql:定义 SQL 片段。id 为唯一标识符。
    • include:引用 SQL 片段。refid 为引用 SQL 片段的 ID。

这些标签可以自由搭配,非常灵活。

5.2、条件语句 if

// if
List<Book> selectIf(Book book);
<!--if 标签-->
<!--select * from book where 1 = 1 and bid = ? and bname = ? and bcount = ?-->
<select id="" parameterType="book" resultType="book">
    select * from book where 1 = 1
    <if test="bid != null">
        and bid = #{bid}
    </if>
    <if test="bname != null">
        and bname = #{bname}
    </if>
    <if test="bcount != null">
        and bcount = #{bcount}
    </if>
</select>
public class T01_If {
    @Test
    public void test01() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        Book book = new Book();
        book.setBid(2);
        List<Book> bookList = mapper.selectIf(book);
        bookList.forEach(System.out::println);
        sqlSession.close();
    }
}

5.3、条件语句 choose when otherwise

// choose
List<Book> selectChoose(Book book);
<!--choose when other 标签-->
<!--select * from book where bid = ?-->
<select id="selectChoose" parameterType="book" resultType="book">
    select * from book
    <where>
        <choose>
            <when test="bid != null">
                and bid = #{bid}
            </when>
            <when test="bname != null">
                and bname = #{bname}
            </when>
            <otherwise>
                and bcount = #{bcount}
            </otherwise>
        </choose>
    </where>
</select>
public class T02_Choose {
    @Test
    public void test01() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        Book book = new Book();
        book.setBid(2);
        List<Book> bookList = mapper.selectChoose(book);
        bookList.forEach(System.out::println);
        sqlSession.close();
    }
}

5.4、关键字 where

// where
List<Book> selectByCondition(Book book);
<!--where 标签-->
<!--select * from book where bid = ? and bname = ? and bcount = ?-->
<select id="selectByCondition" parameterType="book" resultType="book">
    select * from book
    <where>
        <if test="bid != null">
            and bid = #{bid}
        </if>
        <if test="bname != null">
            and bname = #{bname}
        </if>
        <if test="bcount != null">
            and bcount = #{bcount}
        </if>
    </where>
</select>
public class T03_Where {
    @Test
    public void test01() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        Book book = new Book();
        book.setBid(2);
        List<Book> bookList = mapper.selectByCondition(book);
        bookList.forEach(System.out::println);
        sqlSession.close();
    }
}

5.5、关键字 set

// set
void update(Book book);
<!--set 标签-->
<!--update book set bname = ?, bcount = ? where bid = ?-->
<update id="update" parameterType="book">
    update book
    <set>
        <if test="bname != null">
            bname = #{bname},
        </if>
        <if test="bcount != null">
            bcount = #{bcount},
        </if>
    </set>
    where bid = #{bid}
</update>
public class T04_Set {
    @Test
    public void test01() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        Book book = new Book(3, "Java", 3);
        mapper.update(book);
        sqlSession.commit();
        sqlSession.close();
    }
}

5.6、通用语句 trim

// trim
List<Book> selectByCondition02(Book book);
void update02(Book book);
<!--trim 标签-->
<!--select * from book where bid = ? and bname = ? and bcount = ?-->
<select id="selectByCondition02" parameterType="book" resultType="book">
    select * from book
    <trim prefix="where" prefixOverrides="and">
        <if test="bname != null">
            and bname = #{bname}
        </if>
        <if test="bcount != null">
            and bcount = #{bcount}
        </if>
    </trim>
</select>
<!--update book set bname = ?, bcount = ? where bid = ?-->
<update id="update02" parameterType="book">
    update book
    <trim prefix="set" suffixOverrides="," suffix="where">
        <if test="bname != null">
            bname = #{bname},
        </if>
        <if test="bcount != null">
            bcount = #{bcount},
        </if>
    </trim>
    bid = #{bid}
</update>
public class T05_Trim {
    @Test
    public void test01() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        Book book = new Book(null, null, 3);
        List<Book> bookList = mapper.selectByCondition02(book);
        bookList.forEach(System.out::println);
        sqlSession.close();
    }

    @Test
    public void test02() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        Book book = new Book(3, "Java", 3);
        mapper.update02(book);
        sqlSession.commit();
        sqlSession.close();
    }
}

5.7、循环语句 foreach

遍历 Array、List、Map

  • 遍历 List 对象, collection 属性的值为 list。
  • 遍历 Map 对象,collection 属性的值为 Map 集合中元素的键。

foreach 标签

  • collection 属性:需要遍历的对象,Array 为 array,List 为 list,Map 没有。
  • item 属性:集合迭代时元素的别名。
  • open 属性:开始符号。
  • separator 属性:分割符号。
  • close 属性:结束符号。
  • index 属性(可选):迭代 Array、List 时 index 为元素序号,迭代 Map 时 index 为元素的键。

批量操作

  • 批量添加。
  • 按照 ID 批量删除(注意:建议直接写明 where 关键字,不要使用 where 标签,因为 List 为空时则不会生成 where 关键字,导致删除所有数据)。
  • 按照 ID 批量查询。
// foreach
void insertBatch(List<Book> bookList);
void deleteByIds(List<Integer> ids);
void deleteByIds02(List<Integer> ids);
List<Book> selectByIds(List<Integer> ids);
<!--foreach 标签-->
<!--insert into book (bid, bname, bcount) value (?, ?, ?), (?, ?, ?), (?, ?, ?)-->
<insert id="insertBatch" parameterType="list">
    insert into book (bid, bname, bcount) value
    <foreach collection="list" item="book" open="(" separator="), (" close=")">
        #{book.bid}, #{book.bname}, #{book.bcount}
    </foreach>
</insert>
<!--delete from book where bid in (? , ? , ?)-->
<delete id="deleteByIds" parameterType="list">
    delete from book where
    <foreach collection="list" item="id" open="bid in (" separator="," close=")">
        #{id}
    </foreach>
</delete>
<!--delete from book where bid in (? , ? , ?)-->
<delete id="deleteByIds02" parameterType="list">
    delete from book
    <where>
        <foreach collection="list" item="id" open="bid in (" separator="," close=")">
            #{id}
        </foreach>
    </where>
</delete>
<!--select * from book where bid in (? , ? , ?)-->
<select id="selectByIds" parameterType="list" resultType="book">
    select * from book
    <where>
        <foreach collection="list" item="id" open="bid in (" separator="," close=")">
            #{id}
        </foreach>
    </where>
</select>
public class T06_Foreach {
    @Test
    public void insertBatch() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        List<Book> bookList = new ArrayList<>();
        bookList.add(new Book(4, "设计模式 I", 99));
        bookList.add(new Book(5, "设计模式 II", 99));
        bookList.add(new Book(6, "设计模式 III", 99));
        mapper.insertBatch(bookList);
        sqlSession.commit();
        sqlSession.close();
    }

    @Test
    public void deleteByIds() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        List<Integer> ids = Arrays.asList(4, 5, 6);
        mapper.deleteByIds(ids);
        sqlSession.commit();
        sqlSession.close();
    }

    @Test
    public void deleteByIds02() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        List<Integer> ids = Arrays.asList(4, 5, 6);
        mapper.deleteByIds(ids);
        sqlSession.commit();
        sqlSession.close();
    }

    @Test
    public void SelectByIds() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        List<Integer> ids = Arrays.asList(1, 2, 3);
        List<Book> bookList = mapper.selectByIds(ids);
        bookList.forEach(System.out::println);
        sqlSession.close();
    }
}

5.8、SQL 片段抽取复用

// sql include
List<Book> selectAll();
<!--sql include 标签-->
<sql id="selectAllSQL">
    select *
</sql>
<select id="selectAll" resultType="book">
    <include refid="selectAllSQL"/>  from book
</select>
public class T07_SQLInclude {
    @Test
    public void test() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mpper = sqlSession.getMapper(BookMapper.class);
        List<Book> bookList = mpper.selectAll();
        bookList.forEach(System.out::println);
        sqlSession.close();
    }
}

6、MyBatis SQL 语句构建器(了解)

虽然 MyBatis 映射文件支持映射动态的 SQL 语句,但是这种写法非常麻烦。因此 MyBatis 也提供了使用 Java 代码生成 SQL 语句的特性。

public class BookSqlBuilder {
    // Builder 写法
    public String selectById() {
        return new SQL()
                .SELECT("*")
                .FROM("book")
                .WHERE("bid = #{bid}")
                .toString();
    }

    // 匿名内部类写法
    public String deleteById() {
        return new SQL() {{
            DELETE_FROM("book");
            WHERE("bid = #{bid}");
        }}.toString();
    }

    // 条件查询
    public String selectByCondition(Book book) {
        return new SQL() {{
            SELECT("*");
            FROM("book");
            if (book.getBname() != null) {
                WHERE("bname = #{bname}");
            }
            if (book.getBcount() != null) {
                WHERE("bcount = #{bcount}");
            }
        }}.toString();
    }
}

如何使用 SQL 语句构建器

  • 首先,使用 SQL 类生成 SQL 语句(SQL 类继承 AbstractSQL 抽象类,位于 org.apache.ibatis.jdbc 包下)。
  • 然后,定义 SqlBuilder 类,用于返回 SQL 语句。
  • 最后,在映射接口中使用 @SelectProvider 注解即可。

注意:虽然 MyBatis 支持生成所有 SQL 语句,但是使用 LIMIT、OFFSET 等等方法生成 SQL 语句时,需要数据库本身的支持,否则会报错。

示例

  • SqlBuilder
public class BookSqlBuilder {
    // Builder 写法
    public String selectById() {
        return new SQL()
                .SELECT("*")
                .FROM("book")
                .WHERE("bid = #{bid}")
                .toString();
    }

    // 匿名内部类写法
    public String deleteById() {
        return new SQL() {{
            DELETE_FROM("book");
            WHERE("bid = #{bid}");
        }}.toString();
    }

    // 条件查询
    public String selectByCondition(Book book) {
        return new SQL() {{
            SELECT("*");
            FROM("book");
            if (book.getBname() != null) {
                WHERE("bname = #{bname}");
            }
            if (book.getBcount() != null) {
                WHERE("bcount = #{bcount}");
            }
        }}.toString();
    }
}
  • 映射接口
public interface BookMapper {
    @SelectProvider(type = BookSqlBuilder.class, method = "selectById")
    Book selectById(int id);
}
  • 测试
public class T01_SqlBuilder {
    @Test
    public void test01() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        Book book = mapper.selectById(1);
        System.out.println(book);
        sqlSession.close();
    }

    @Test
    public void test02() {
        SqlSession sqlSession = MybatisUtil.getSqlSession();
        BookMapper mapper = sqlSession.getMapper(BookMapper.class);
        List<Book> bookList = mapper.selectByCondition(new Book(null, null, null));
        bookList.forEach(System.out::println);
        sqlSession.close();
    }
}

7、MyBatis 原理(了解)

8、MyBatis 总结

8.1、项目说明

mybatis(全部基于 Maven)

  • mybatis01_start
  • mybatis02_config
  • mybatis03_mapper01
  • mybatis04_mapper02
  • mybatis05_dynamicsql
  • mybatis06_sqlbuilder

8.2、项目资料

数据库

CREATE DATABASE IF NOT EXISTS `mybatis` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE `mybatis`;
CREATE TABLE IF NOT EXISTS `book` (
	`bid` INT COMMENT '主键',
	`bname` VARCHAR(100) COMMENT '书名',
	`bcount` INT COMMENT '数量',
	PRIMARY KEY (`bid`)
);
INSERT INTO `book` (`bid`, `bname`, `bcount`) VALUES
('1', 'Java 基础', '1'),
('2', 'Java 中级', '2'),
('3', 'Java 高级', '3');

-- 添加字段
ALTER TABLE `book` ADD `bdate` VARCHAR(100) COMMENT '添加时间';

Maven 依赖

  • MyBatis 依赖:MyBatis、PageHelper。
  • JDBC 依赖:MySQL Connector。
  • 其他依赖:Junit、Lombok。
<!--MyBatis 依赖-->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.7</version>
</dependency>
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.2.0</version>
</dependency>

<!--JDBC 依赖-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.26</version>
</dependency>

<!--其他依赖-->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.12</version>
</dependency>

8.3、常见问题

MySQL6 与 MySQL5 的区别

  • MySQL6 驱动名称为 com.mysql.cj.jdbc.Driver,URL 需要添加时区。
  • MySQL5 驱动名称为 com.mysql.jdbc.Driver,URL 不需要添加时区。
# MySQL6
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis?serverTimezone=Asia/Shanghai&useSSL=true&useUnicode=true&characterEncoding=UTF-8
username=root
password=test123!

# MySQL5
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis?useSSL=true&useUnicode=true&characterEncoding=UTF-8
username=root
password=test123!
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot 是一个快速开发的框架,可以轻松集成 MyBatis 持久层框架Spring Boot 和 MyBatis 的结合可以让我们更加方便地开发应用程序,提高开发效率。 下面是一个简单的 Spring Boot + MyBatis 的示例: 1. 在 pom.xml 中添加 MyBatis 和 MySQL 驱动的依赖: ```xml <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> ``` 2. 创建一个 Spring Boot 应用程序,并在 application.properties 中添加数据库配置: ``` spring.datasource.url=jdbc:mysql://localhost:3306/test spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.jdbc.Driver ``` 3. 创建一个 User 实体类: ```java public class User { private Long id; private String name; private Integer age; // 省略 getter 和 setter } ``` 4. 创建一个 UserMapper 接口: ```java @Mapper public interface UserMapper { @Select("SELECT * FROM user WHERE id = #{id}") User findById(Long id); @Insert("INSERT INTO user(name, age) VALUES(#{name}, #{age})") @Options(useGeneratedKeys = true, keyProperty = "id") void save(User user); } ``` 5. 在 Spring Boot 应用程序中使用 UserMapper: ```java @RestController public class UserController { @Autowired private UserMapper userMapper; @GetMapping("/users/{id}") public User findById(@PathVariable Long id) { return userMapper.findById(id); } @PostMapping("/users") public void save(@RequestBody User user) { userMapper.save(user); } } ``` 这样,我们就可以使用 Spring Boot + MyBatis 快速开发应用程序了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值