4. Mybatis

229 阅读24分钟

一、简介

1.1 什么是Mybatis

Mybatis是一个优秀的持久层框架,支持定制化SQL、避免了所有JDBC代码和手动设置参数以及获取结果集。

Mybatis可以使用简单的XML或者注解来配置和映射原生类型、接口和Java的POJO

持久化

  • 数据持久化,将程序的数据在持久状态和瞬时状态转化的过程
  • 数据库(JDBC)、io文件持久化

持久层

  • Dao层
  • 完成持久化工作的代码块

1.2 为什么要用Mybatis

  • 帮助程序员将数据存入数据库中
  • 方便,本身小前,没有第三方依赖,最简单只需要两个jar和配置sql映射
  • 传统的JDBC太复杂,简化为框架
  • 解除sql与程序代码的耦合:通过提供Dao层,将业务逻辑和数据访问逻辑分离,易于维护
  • 提供xml标签,支持动态sql

二、第一个MyBatis程序

思路:搭建环境、导入Mybatis、编写代码、测试

2.1 搭建环境

  • 搭建数据库

  • 创建Maven项目

  • 导入依赖

<!--MYSQL驱动-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.11</version>
</dependency>
<!--Mybatis-->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.4.5</version>
</dependency>
<!--单元测试-->
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.8.2</version>
</dependency>

2.2 Mybatis配置文件

resource下创建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>
    <properties resource="db.properties"/>

    <settings>
        <!--设置日志工厂-->
        <!--SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING-->
        <setting name="logImpl" value="STDOUT_LOGGING"/>

    </settings>
    
    <!--可以给实体类起别名-->
    <typeAliases>
        <typeAlias type="com.nick.pojo.User" alias="User"/>
    </typeAliases>
    
    <environments default="development">
        <!--开发环境-->
        <environment id="development">
            <!--默认JDBC事务管理-->
            <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>

    <!--!!!!每一个Mapper.xml都需要在Mybatis核心配置文件中注册!!!!-->
    <mappers>
        <mapper resource="com/nick/dao/UsersMapper.xml"/>
    </mappers>
</configuration>

创建db.properties

driver = com.mysql.cj.jdbc.Driver
url = jdbc:mysql://localhost:3306/jdbc?useSSL=false&useUnicode=true&characterEncoding=utf-8
username = root
password = wang2995

2.3 编写Mybatis的工具类

每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例。

String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = 
    new SqlSessionFactoryBuilder().build(inputStream);

既然有了 SqlSessionFactory,顾名思义,我们可以从中获得 SqlSession 的实例。SqlSession 提供了在数据库执行 SQL 命令所需的所有方法。你可以通过 SqlSession 实例来直接执行已映射的 SQL 语句。

建一个包com.nick.utils,创建一个MybatisUtil.java

public class MybatisUtils {
    private static SqlSessionFactory sqlSessionFactory;
    /**
     * TODO: 1. 获取SqlSessionFactory对象
     */
    static {
        try {
            String resource = "mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);
            sqlSessionFactory = 
                new SqlSessionFactoryBuilder().build(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * TODO: 2. 获取SqlSession对象
     */
    public static SqlSession getSqlSession() {
        return sqlSessionFactory.openSession();
    }
}

2.4 编写代码

  • 实体类
@Data
@Builder
public class User {
    private int id;
    private String name;
    /**
     * 在数据库中该字段为password
     */
    private String pwd;
    private String email;
    private Date birthday;

    @Tolerate
    public User() {
    }
}
  • Dao接口
public interface UsersMapper {
    /**
     * 查询全表
     * @return
     */
    List<User> getUserList();
}
  • Dao配置文件UserDao.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">
<!--namespace, to bind a dao interface-->
<mapper namespace="com.nick.dao.UsersMapper">
    <!--结果集映射-->
    <resultMap id="UserMap" type="User">
        <result column="password" property="pwd"/>
    </resultMap>
    <select id="getUserList" resultMap="UserMap">
        SELECT * from Users
    </select>
</mapper>
  • 测试类

注意:UserMapper is not known to the MapperRegistry

是Mapper没有在核心配置文件中注册

注意:Could not find resource com/nick/dao/UserMapper.xml

是因为xml不在resource下,资源过滤问题,要在pom中配置<build>

public class UserMapperTest {
    @Test
    public void test() {
        /**
         * TODO: 1. 获取SqlSession对象
         */
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        /**
         * TODO: 2. 执行
         */
        UsersMapper mapper = sqlSession.getMapper(UsersMapper.class);
        List<User> userList = mapper.getUserList();
        /**
         * TODO: 3. 关闭SqlSession
         */
        sqlSession.close();
    }
}

资源过滤问题解决:

<build>
    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <includes>
                <include>**/*.properties</include>
                <include>**/*.xml</include>
            </includes>
            <filtering>true</filtering>
        </resource>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.properties</include>
                <include>**/*.xml</include>
            </includes>
            <filtering>true</filtering>
        </resource>
    </resources>
</build>
  • 结果
Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
Opening JDBC Connection
Created connection 477289012.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1c72da34]
==>  Preparing: SELECT * from Users 
==> Parameters: 
<==    Columns: id, name, password, email, birthday
<==        Row: 1, 张三, 123456, zs@qq.com, 1994-12-30
<==        Row: 2, 李四, 123456, ls@qq.com, 1994-12-01
<==        Row: 3, 王五, 123456, ww@qq.com, 1994-12-02
<==        Row: 4, 老刘, 123456, ll@qq.com, 1994-12-03
<==      Total: 4
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1c72da34]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1c72da34]
Returned connection 477289012 to pool.

三、Map和模糊查询拓展

3.1 万能的Map

对于insert方法,如果实体类User有一百个字段,sql中就要写一百个属性么?

由于参数是User,如果不写,就不能使用这个对象

可以使用Map来简化实现,参数为Map类型,好处是不需要这个对象中有什么,只需要查对应的字段

int addUser(Map<String, Object> map);
<insert id="addUser" paramterType="Map">
    insert into user(id, name, pwd)
    values(#{userId}, #{userName}, #{password})
</insert>

这里#{userId}#{userName}#{password}不必和对象一一对应,只需要和Map中的Key对应。修改的时候只需要将要修改的属性放入Map即可,不用将所有的属性都放进去,比对象灵活。

3.2 模糊查询

  • Java代码在执行的时候,传递参数可以使用通配符"%"
<select id="getUserLike" resultType="com.nick.pojo.User">
    select * from user where name LIKE #{value}
</select>

传入的参数value王%

  • 在SQL中直接写通配符(不推荐,可能SQL注入)
<select id="getUserLike" resultType="com.nick.pojo.User">
    select * from user where name LIKE "%"#{value}"%"
</select>

传入的参数value

四、配置属性优化

4.1 核心配置文件

mybatis-config.xml

4.2 环境配置(environments)

MyBatis 可以配置成适应多种环境,这种机制有助于将 SQL 映射应用于多种数据库之中, 现实情况下有多种理由需要这么做。例如,开发、测试和生产环境需要有不同的配置;或者想在具有相同 Schema 的多个生产数据库中使用相同的 SQL 映射。还有许多类似的使用场景。

不过要记住:尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择一种环境。

所以,如果你想连接两个数据库,就需要创建两个 SqlSessionFactory 实例,每个数据库对应一个。而如果是三个数据库,就需要三个实例,依此类推,记起来很简单:

  • 每个数据库对应一个 SqlSessionFactory 实例

为了指定创建哪种环境,只要将它作为可选的参数传递给 SqlSessionFactoryBuilder 即可。可以接受环境配置的两个方法签名是:

SqlSessionFactory factory = 
    new SqlSessionFactoryBuilder().build(reader, environment);
SqlSessionFactory factory = 
    new SqlSessionFactoryBuilder().build(reader, environment, properties);

environments 元素定义了如何配置环境。

<environments default="development">
  <environment id="development">
    <transactionManager type="JDBC">
      <property name="..." value="..."/>
    </transactionManager>
    <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>

注意一些关键点:

  • 默认使用的环境 ID(比如:default="development")。
  • 每个 environment 元素定义的环境 ID(比如:id="development")。
  • 事务管理器的配置(比如:type="JDBC")。
  • 数据源的配置(比如:type="POOLED")。

默认环境和环境 ID 顾名思义。 环境可以随意命名,但务必保证默认的环境 ID 要匹配其中一个环境 ID。

4.2.1事务管理器(transactionManager)

在 MyBatis 中有两种类型的事务管理器(也就是 type="[JDBC|MANAGED]"):

  • JDBC – 这个配置直接使用了 JDBC 的提交和回滚设施,它依赖从数据源获得的连接来管理事务作用域。
  • MANAGED – 这个配置几乎没做什么。它从不提交或回滚一个连接,而是让容器来管理事务的整个生命周期(比如 JEE 应用服务器的上下文)。 默认情况下它会关闭连接。然而一些容器并不希望连接被关闭,因此需要将 closeConnection 属性设置为 false 来阻止默认的关闭行为。例如:
<transactionManager type="MANAGED">
  <property name="closeConnection" value="false"/>
</transactionManager>

提示 如果你正在使用 Spring + MyBatis,则没有必要配置事务管理器,因为 Spring 模块会使用自带的管理器来覆盖前面的配置。

这两种事务管理器类型都不需要设置任何属性。它们其实是类型别名,换句话说,你可以用 TransactionFactory 接口实现类的全限定名或类型别名代替它们。

public interface TransactionFactory {
  default void setProperties(Properties props) { // 从 3.5.2 开始,该方法为默认方法
    // 空实现
  }
  Transaction newTransaction(Connection conn);
  Transaction newTransaction(DataSource dataSource, 	
                             TransactionIsolationLevel level, 
                             boolean autoCommit);
}

在事务管理器实例化后,所有在 XML 中配置的属性将会被传递给 setProperties() 方法。你的实现还需要创建一个 Transaction 接口的实现类,这个接口也很简单:

public interface Transaction {
  Connection getConnection() throws SQLException;
  void commit() throws SQLException;
  void rollback() throws SQLException;
  void close() throws SQLException;
  Integer getTimeout() throws SQLException;
}

使用这两个接口,你可以完全自定义 MyBatis 对事务的处理。

4.2.2 数据源(dataSource)

dataSource 元素使用标准的 JDBC 数据源接口来配置 JDBC 连接对象的资源。

  • 大多数 MyBatis 应用程序会按示例中的例子来配置数据源。虽然数据源配置是可选的,但如果要启用延迟加载特性,就必须配置数据源。

有三种内建的数据源类型(也就是 type="[UNPOOLED|POOLED|JNDI]"),第三方(dbcpc3p0druid):

(1)UNPOOLED

这个数据源的实现会每次请求时打开和关闭连接。虽然有点慢,但对那些数据库连接可用性要求不高的简单应用程序来说,是一个很好的选择。 性能表现则依赖于使用的数据库,对某些数据库来说,使用连接池并不重要,这个配置就很适合这种情形。UNPOOLED 类型的数据源仅仅需要配置以下 5 种属性:

  • driver – 这是 JDBC 驱动的 Java 类全限定名(并不是 JDBC 驱动中可能包含的数据源类)。
  • url – 这是数据库的 JDBC URL 地址。
  • username – 登录数据库的用户名。
  • password – 登录数据库的密码。
  • defaultTransactionIsolationLevel – 默认的连接事务隔离级别。
  • defaultNetworkTimeout – 等待数据库操作完成的默认网络超时时间(单位:毫秒)。查看 java.sql.Connection#setNetworkTimeout() 的 API 文档以获取更多信息。

作为可选项,你也可以传递属性给数据库驱动。只需在属性名加上“driver.”前缀即可,例如:

  • driver.encoding=UTF8

这将通过 DriverManager.getConnection(url, driverProperties) 方法传递值为 UTF8encoding 属性给数据库驱动。

(2)POOLED(用完可以回收)

这种数据源的实现利用“”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。 这种处理方式很流行,能使并发 Web 应用快速响应请求

除了上述提到 UNPOOLED 下的属性外,还有更多属性用来配置 POOLED 的数据源:

  • poolMaximumActiveConnections – 在任意时间可存在的活动(正在使用)连接数量,默认值:10
  • poolMaximumIdleConnections – 任意时间可能存在的空闲连接数。
  • poolMaximumCheckoutTime – 在被强制返回之前,池中连接被检出(checked out)时间,默认值:20000 毫秒(即 20 秒)
  • poolTimeToWait – 这是一个底层设置,如果获取连接花费了相当长的时间,连接池会打印状态日志并重新尝试获取一个连接(避免在误配置的情况下一直失败且不打印日志),默认值:20000 毫秒(即 20 秒)。
  • poolMaximumLocalBadConnectionTolerance – 这是一个关于坏连接容忍度的底层设置, 作用于每一个尝试从缓存池获取连接的线程。 如果这个线程获取到的是一个坏的连接,那么这个数据源允许这个线程尝试重新获取一个新的连接,但是这个重新尝试的次数不应该超过 poolMaximumIdleConnectionspoolMaximumLocalBadConnectionTolerance 之和。 默认值:3(新增于 3.4.5)
  • poolPingQuery – 发送到数据库的侦测查询,用来检验连接是否正常工作并准备接受请求。默认是“NO PING QUERY SET”,这会导致多数数据库驱动出错时返回恰当的错误消息。
  • poolPingEnabled – 是否启用侦测查询。若开启,需要设置 poolPingQuery 属性为一个可执行的 SQL 语句(最好是一个速度非常快的 SQL 语句),默认值:false。
  • poolPingConnectionsNotUsedFor – 配置 poolPingQuery 的频率。可以被设置为和数据库连接超时时间一样,来避免不必要的侦测,默认值:0(即所有连接每一时刻都被侦测 — 当然仅当 poolPingEnabled 为 true 时适用)。

(3)JNDI(用的比较少)

这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用。这种数据源配置只需要两个属性:

  • initial_context – 这个属性用来在 InitialContext 中寻找上下文(即,initialContext.lookup(initial_context))。这是个可选属性,如果忽略,那么将会直接从 InitialContext 中寻找 data_source 属性。
  • data_source – 这是引用数据源实例位置的上下文路径。提供了 initial_context 配置时会在其返回的上下文中进行查找,没有提供时则直接在 InitialContext 中查找。

和其他数据源配置类似,可以通过添加前缀“env.”直接把属性传递给 InitialContext。比如:

  • env.encoding=UTF8

这就会在 InitialContext 实例化时往它的构造方法传递值为 UTF8encoding 属性。

你可以通过实现接口 org.apache.ibatis.datasource.DataSourceFactory 来使用第三方数据源实现:

public interface DataSourceFactory {
  void setProperties(Properties props);
  DataSource getDataSource();
}

org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory 可被用作父类来构建新的数据源适配器,比如下面这段插入 C3P0 数据源所必需的代码:

import org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory;
import com.mchange.v2.c3p0.ComboPooledDataSource;

public class C3P0DataSourceFactory extends UnpooledDataSourceFactory {

  public C3P0DataSourceFactory() {
    this.dataSource = new ComboPooledDataSource();
  }
}

为了令其工作,记得在配置文件中为每个希望 MyBatis 调用的 setter 方法增加对应的属性。 下面是一个可以连接至 PostgreSQL 数据库的例子:

<dataSource type="org.myproject.C3P0DataSourceFactory">
  <property name="driver" value="org.postgresql.Driver"/>
  <property name="url" value="jdbc:postgresql:mydb"/>
  <property name="username" value="postgres"/>
  <property name="password" value="root"/>
</dataSource>

4.3 映射器(mappers)

MapperRegistry:注册绑定我们的Mapper文件;

既然 MyBatis 的行为已经由上述元素配置完了,我们现在就要来定义 SQL 映射语句了。 但首先,我们需要告诉 MyBatis 到哪里去找到这些语句。 在自动查找资源方面,Java 并没有提供一个很好的解决方案,所以最好的办法是直接告诉 MyBatis 到哪里去找映射文件。 你可以使用相对于类路径的资源引用,或完全限定资源定位符(包括 file:/// 形式的 URL),或类名和包名等。(一般使用resource

例如:

<!-- 使用相对于类路径的资源引用 -->
<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
  <mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<!-- 使用完全限定资源定位符(URL) -->
<mappers>
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
  <mapper url="file:///var/mappers/BlogMapper.xml"/>
  <mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<!-- 使用映射器接口实现类的完全限定类名 -->
<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
  <mapper class="org.mybatis.builder.BlogMapper"/>
  <mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
<!-- 将包内的映射器接口实现全部注册为映射器 -->
<mappers>
  <package name="org.mybatis.builder"/>
</mappers>

使用classpackage所产生的的一些问题:

  • 接口和Mapper.xml必须同名
  • 接口和Mapper.xml必须在同一个目录下

4.4 属性(properties)

这些属性可以在外部进行配置,并可以进行动态替换。你既可以在典型的 Java 属性文件中配置这些属性,也可以在 properties 元素的子元素中设置。例如:

<properties resource="org/mybatis/example/config.properties">
  <property name="username" value="dev_user"/>
  <property name="password" value="F2Fa3!33TYyg"/>
</properties>

设置好的属性可以在整个配置文件中用来替换需要动态配置的属性值。比如:

<dataSource type="POOLED">
  <property name="driver" value="${driver}"/>
  <property name="url" value="${url}"/>
  <property name="username" value="${username}"/>
  <property name="password" value="${password}"/>
</dataSource>

这个例子中的 username 和 password 将会由 properties 元素中设置的相应值来替换。 driver 和 url 属性将会由 config.properties 文件中对应的值来替换。这样就为配置提供了诸多灵活选择。

也可以在 SqlSessionFactoryBuilder.build() 方法中传入属性值。例如:

SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(
    reader, environment, props);

如果一个属性在不只一个地方进行了配置,那么,MyBatis 将按照下面的顺序来加载:

  • 首先读取在 properties 元素体内指定的属性。
  • 然后根据 properties 元素中的 resource 属性读取类路径下属性文件,或根据 url 属性指定的路径读取属性文件,并覆盖之前读取过的同名属性。
  • 最后读取作为方法参数传递的属性,并覆盖之前读取过的同名属性。

因此,通过方法参数传递的属性具有最高优先级,resource/url 属性中指定的配置文件次之,最低优先级的则是 properties 元素中指定的属性。

从 MyBatis 3.4.2 开始,你可以为占位符指定一个默认值。例如:

<dataSource type="POOLED">
  <property name="username" value="${username:ut_user}"/> 
  <!-- 如果属性 'username' 没有被配置,'username' 属性的值将为 'ut_user' -->
</dataSource>

这个特性默认是关闭的。要启用这个特性,需要添加一个特定的属性来开启这个特性。例如:

<properties resource="org/mybatis/example/config.properties">
  <property name="org.apache.ibatis.parsing.PropertyParser
                  .enable-default-value" 
            value="true"/> 
  <!-- 启用默认值特性 -->
</properties>

提示 如果你在属性名中使用了 ":" 字符(如:db:username),或者在 SQL 映射中使用了 OGNL 表达式的三元运算符(如: ${tableName != null ? tableName : 'global_constants'}),就需要设置特定的属性来修改分隔属性名和默认值的字符。例如:

<properties resource="org/mybatis/example/config.properties">
  <property name="org.apache.ibatis.parsing.PropertyParser
                  .default-value-separator" 
            value="?:"/> 
  <!-- 修改默认值的分隔符 -->
</properties>
<dataSource type="POOLED">
  <property name="username" value="${db:username?:ut_user}"/>
</dataSource>

4.5 设置(settings)

这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为。 下表描述了设置中各项设置的含义、默认值等。

设置名描述有效值默认值
cacheEnabled全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。true | falsetrue
lazyLoadingEnabled延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态。true | falsefalse
aggressiveLazyLoading开启时,任一方法的调用都会加载该对象的所有延迟加载属性。 否则,每个延迟加载属性会按需加载(参考 lazyLoadTriggerMethods)。true | falsefalse
multipleResultSetsEnabled是否允许单个语句返回多结果集(需要数据库驱动支持)。true | falsetrue
useColumnLabel使用列标签代替列名。实际表现依赖于数据库驱动,具体可参考数据库驱动的相关文档,或通过对比测试来观察。true | falsetrue
useGeneratedKeys允许 JDBC 支持自动生成主键,需要数据库驱动支持。如果设置为 true,将强制使用自动生成主键。尽管一些数据库驱动不支持此特性,但仍可正常工作(如 Derby)。true | falseFalse
autoMappingBehavior指定 MyBatis 应如何自动映射列到字段或属性。
NONE 表示关闭自动映射;
PARTIAL 只会自动映射没有定义嵌套结果映射的字段。
FULL 会自动映射任何复杂的结果集(无论是否嵌套)。
NONE
PARTIAL
FULL
PARTIAL
autoMappingUnknownColumnBehavior指定发现自动映射目标未知列(或未知属性类型)的行为。
NONE: 不做任何反应
WARNING: 输出警告日志
(org.apache.ibatis.session.AutoMappingUnknownColumnBehavior 的日志等级必须设置为 WARN)
FAILING: 映射失败 (抛出 SqlSessionException)
NONE
WARNING
FAILING
NONE
defaultExecutorType配置默认的执行器。
SIMPLE 就是普通的执行器;
REUSE 执行器会重用预处理语句(PreparedStatement);
BATCH 执行器不仅重用语句还会执行批量更新。
SIMPLE
REUSE
BATCH
SIMPLE
defaultStatementTimeout设置超时时间,它决定数据库驱动等待数据库响应的秒数。任意正整数未设置 (null)
defaultFetchSize为驱动的结果集获取数量(fetchSize)设置一个建议值。此参数只可以在查询设置中被覆盖。任意正整数未设置 (null)
defaultResultSetType指定语句默认的滚动策略。(新增于 3.5.2)FORWARD_ONLY
SCROLL_SENSITIVE
SCROLL_INSENSITIVE
DEFAULT(等同于未设置)
未设置 (null)
safeRowBoundsEnabled是否允许在嵌套语句中使用分页(RowBounds)。
如果允许使用则设置为 false。
true | falseFalse
safeResultHandlerEnabled是否允许在嵌套语句中使用结果处理器(ResultHandler)。
如果允许使用则设置为 false。
true | falseTrue
mapUnderscoreToCamelCase是否开启驼峰命名自动映射,
即从经典数据库列名 A_COLUMN 映射到经典 Java 属性名 aColumn。
true | falseFalse
localCacheScopeMyBatis 利用本地缓存机制(Local Cache)防止循环引用和加速重复的嵌套查询。
默认值为 SESSION,会缓存一个会话中执行的所有查询。
若设置值为 STATEMENT,本地缓存将仅用于执行语句,对相同 SqlSession 的不同查询将不会进行缓存。
SESSION
STATEMENT
SESSION
jdbcTypeForNull当没有为参数指定特定的 JDBC 类型时,空值的默认 JDBC 类型。
某些数据库驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。
JdbcType 常量,常用值:NULL、VARCHAR 或 OTHER。OTHER
lazyLoadTriggerMethods指定对象的哪些方法触发一次延迟加载。用逗号分隔的方法列表。equals,clone,hashCode,toString
defaultScriptingLanguage指定动态 SQL 生成使用的默认脚本语言。一个类型别名或全限定类名。org.apache.ibatis.scripting
.xmltags.XMLLanguageDriver
defaultEnumTypeHandler指定 Enum 使用的默认 TypeHandler 。(新增于 3.4.5)一个类型别名或全限定类名。org.apache.ibatis.type
.EnumTypeHandler
callSettersOnNulls指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法,这在依赖于 Map.keySet() 或 null 值进行初始化时比较有用。
注意基本类型(int、boolean 等)是不能设置成 null 的。
true | falsefalse
returnInstanceForEmptyRow当返回行的所有列都是空时,MyBatis默认返回 null。 当开启这个设置时,MyBatis会返回一个空实例。
请注意,它也适用于嵌套的结果集(如集合或关联)。(新增于 3.4.2)
true | falsefalse
logPrefix指定 MyBatis 增加到日志名称的前缀。任何字符串未设置
logImpl指定 MyBatis 所用日志的具体实现,未指定时将自动查找。SLF4J
LOG4J
LOG4J2
JDK_LOGGING
COMMONS_LOGGING
STDOUT_LOGGING
NO_LOGGING
未设置
proxyFactory指定 Mybatis 创建可延迟加载对象所用到的代理工具。CGLIB
JAVASSIST
JAVASSIST
vfsImpl指定 VFS 的实现自定义 VFS 的实现的类全限定名,以逗号分隔。未设置
useActualParamName允许使用方法签名中的名称作为语句参数名称。 为了使用该特性,你的项目必须采用 Java 8 编译,并且加上 -parameters选项。(新增于 3.4.1)true | falsetrue
configurationFactory指定一个提供 Configuration 实例的类。 这个被返回的 Configuration 实例用来加载被反序列化对象的延迟加载属性值。 这个类必须包含一个签名为static Configuration getConfiguration() 的方法。(新增于 3.2.3)一个类型别名或完全限定类名。未设置
shrinkWhitespacesInSql从SQL中删除多余的空格字符。请注意,这也会影响SQL中的文字字符串。 (新增于 3.5.5)true | falsefalse
defaultSqlProviderTypeSpecifies an sql provider class that holds provider method (Since 3.5.6). This class apply to the type(or value) attribute on sql provider annotation(e.g. @SelectProvider), when these attribute was omitted.A type alias or fully qualified class nameNot set
nullableOnForEachSpecifies the default value of 'nullable' attribute on 'foreach' tag. (Since 3.5.9)true | false

一个配置完整的 settings 元素的示例如下:

<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>

4.6 类型别名(typeAliases)

类型别名可为 Java 类型设置一个缩写名字。 它仅用于 XML 配置,意在降低冗余的全限定类名书写。例如:

<typeAliases>
  <typeAlias alias="Author" type="domain.blog.Author"/>
  <typeAlias alias="Blog" type="domain.blog.Blog"/>
  <typeAlias alias="Comment" type="domain.blog.Comment"/>
  <typeAlias alias="Post" type="domain.blog.Post"/>
  <typeAlias alias="Section" type="domain.blog.Section"/>
  <typeAlias alias="Tag" type="domain.blog.Tag"/>
</typeAliases>

当这样配置时,Blog 可以用在任何使用 domain.blog.Blog 的地方。

也可以指定一个包名,MyBatis 会在包名下面搜索需要的 Java Bean,比如:

<typeAliases>
  <package name="domain.blog"/>
</typeAliases>

每一个在包 domain.blog 中的 Java Bean,在没有注解的情况下,会使用 Bean 的首字母小写的非限定类名来作为它的别名。 比如 domain.blog.Author 的别名为 author;若有注解,则别名为其注解值。见下面的例子:

@Alias("author")
public class Author {
    ...
}

下面是一些为常见的 Java 类型内建的类型别名。它们都是不区分大小写的,注意,为了应对原始类型的命名重复,采取了特殊的命名风格。

别名映射的类型
_bytebyte
_longlong
_shortshort
_intint
_integerint
_doubledouble
_floatfloat
_booleanboolean
stringString
byteByte
longLong
shortShort
intInteger
integerInteger
doubleDouble
floatFloat
booleanBoolean
dateDate
decimalBigDecimal
bigdecimalBigDecimal
objectObject
mapMap
hashmapHashMap
listList
arraylistArrayList
collectionCollection
iteratorIterator

五、生命周期和作用域

理解我们之前讨论过的不同作用域和生命周期类别是至关重要的,因为错误的使用会导致非常严重的并发问题

提示 对象生命周期和依赖注入框架

依赖注入框架可以创建线程安全的、基于事务的 SqlSession 和映射器,并将它们直接注入到你的 bean 中,因此可以直接忽略它们的生命周期。 如果对如何通过依赖注入框架使用 MyBatis 感兴趣,可以研究一下 MyBatis-Spring 或 MyBatis-Guice 两个子项目。

image.png

5.1 SqlSessionFactoryBuilder

  • 方法作用域(局部变量)

这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。

因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。 你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。

5.2 SqlSessionFactory

  • 应用作用域(想象为数据库连接池,单例,全局)

SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。

5.3 SqlSession

  • 请求或方法作用域(理解为连接到连接池的请求,用完就关闭)

每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。 绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。 也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession。 如果你现在正在使用一种 Web 框架,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中。

换句话说,每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它。 这个关闭操作很重要,为了确保每次都能执行关闭操作,你应该把这个关闭操作放到 finally 块中。 下面的示例就是一个确保 SqlSession 关闭的标准模式:

try (SqlSession session = sqlSessionFactory.openSession()) {
  // 你的应用逻辑代码
}

5.4 映射器实例

  • 方法作用域

映射器是一些绑定映射语句的接口。映射器接口的实例是从 SqlSession 中获得的。虽然从技术层面上来讲,任何映射器实例的最大作用域与请求它们的 SqlSession 相同。但方法作用域才是映射器实例的最合适的作用域。

也就是说,映射器实例应该在调用它们的方法中被获取使用完毕之后即可丢弃。 映射器实例并不需要被显式地关闭。尽管在整个请求作用域保留映射器实例不会有什么问题,但是你很快会发现,在这个作用域上管理太多像 SqlSession 的资源会让你忙不过来。 因此,最好将映射器放在方法作用域内。就像下面的例子一样:

try (SqlSession session = sqlSessionFactory.openSession()) {
  BlogMapper mapper = session.getMapper(BlogMapper.class);
  // 你的应用逻辑代码
}

image.png

六、ResultMap

ResultMap:解决属性名和字段名不一致的问题

数据库中的字段和实体类的字段不一致,例如user表中字段为pwd,实体类中属性为password,则查询的时候返回数据中password属性为null。因为类型处理器通过字段pwd去实体类中找对应的set方法,然后发现找不到,就设置为空。

解决方法:

  • 起别名(需要改SQL)
<select id="getUserById" resultType="com.nick.pojo.User">
    select id, name, pwd as password 
    from user 
    where id = #{id}
</select>
  • ResultMap

结果集映射,将pwd映射为password

<resultMap id="userMap" type="com.nick.pojo.User">
    <result colum="pwd" property="password"/>
</resultMap>
<select id="getUserById" resultMap="userMap">
    select id, name, pwd
    from user 
    where id = #{id}
</select>

七、日志工厂

7.1 为什么需要

如果一个数据库操作,出现了异常,需要进行排错,日志就是最好的助手

  • 原来的实现方式可以将SQL打印出来,进行检查(sout、debug)
  • 现在使用Mapper后,无法将SQL直接通过代码打印出来,因此需要使用日志工厂来实现类似的功能

7.2 具体使用

setting中可以配置**logImpl** 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。

setting的位置必须要在propertiestypeAliases之间

  • SLF4J
  • LOG4J 【掌握】
  • LOG4J2
  • JDK_LOGGING
  • COMMONS_LOGGING
  • STDOUT_LOGGING 【掌握】
  • NO_LOGGING
<setting>
	<setting name="logImpl" value="STDOUT_LOGGING`"/>
</setting>
Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
Opening JDBC Connection
Created connection 1634132079.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6166e06f]
==>  Preparing: SELECT * from users 
==> Parameters: 
<==    Columns: id, name, password, email, birthday
<==        Row: 1, 张三, 123456, zs@qq.com, 1994-12-30
<==        Row: 2, 李四, 123456, ls@qq.com, 1994-12-01
<==        Row: 3, 王五, 123456, ww@qq.com, 1994-12-02
<==        Row: 4, 老刘, 123456, ll@qq.com, 1994-12-03
<==      Total: 4
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6166e06f]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6166e06f]
Returned connection 1634132079 to pool.

Process finished with exit code 0

八、实现分页

为什么要分页?

数据量太大,减少数据的处理量

8.1 使用Limit分页

select *
from user
limit startIndex, pageSize
  • limit 0, 2

每页显示2个,从第0个开始查询

  • limit 2

查询前两个

// interface
List<USer> getUserByLimit(Map<String, Object> map);
<select id="getUserByLimit" parameterType="map" resultType="User">
    select *
    from user
    limit #{startIndex}, #{pageSize}
</select>
// 调用
HashMap<String, Integer> map = new HashMap<String, Integer>();
map.put("startIndex", 0);
map.put("pageSize", 2);
// ...

8.2 RowBounds分页

不推荐使用

// interface
List<USer> getUserByRowBounds();
<select id="getUserByRowBounds" parameterType="map" resultType="User">
    select *
    from user
</select>
// 调用
SqlSession sqlSession = sqlSessionFactory.getSqlSession();
RowBounds rowBounds = new RowBounds(1, 2);
// 通过Java代码层面实现分页
List<User> userList = sqlSession.selectList(
    "com.nick.dao.UserMapper.getUserByRowBounds", null, rowBounds);

8.3 PageHelper分页插件

了解即可

九、Mybatis详细执行流程

image.png

  • Resource获取加载全局配置文件

根据配置文件路径,加载流

image.png

  • 实例化SqlSessionFactoryBuilder、解析配置文件流XMLConfigBuilder

根据配置文件的输入流,去构建XMLConfigBuilder

image.png

  • Configuration所有配置信息

使用XMLConfigBuilder去解析配置文件中的每一项

image.png

  • SqlSessionFactory实例化

解析完成后,会返回configuration,根据configuration去进行SqlSessionFactory的创建

image.png

  • 创建sqlSessionFactory结束后

SqlSessionFactory对象会包含configuration,而configuration中包含配置文件的各个项

image.png

  • TransactionFactory事务管理器、创建Executor(OpenSession的时候创建)

SqlSessionFactory通过openSession来返回一个sqlSession对象,在openSession的时候会创建事务管理器和执行器

image.png

  • 获取到sqlSession

sqlSession中会包含configuration、事务管理器、执行器

image.png

image.png

  • 获取Mapper

接下来通过SqlSession来获取Mapper,具体是通过接口名称和反射在mapperRegistry中找

image.png

  • Mapper执行具体的方法

找到Mapper后,可以通过反射找到Mapper中的公共方法

image.png

Mapper是根据接口名称通过反射获取的方法。

image.png

十、复杂查询环境

10.1 环境搭建

  • SQL
CREATE TABLE `teacher` (
    `id` INT(10) NOT NULL,
    `name` VARCHAR(30) DEFAULT NULL,
    PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

INSERT INTO teacher(`id`, `name`)
VALUES (1, '王老师');

CREATE TABLE `student` (
   `id` INT(10) NOT NULL,
   `name` VARCHAR(30) DEFAULT NULL,
   `tid` INT(10) DEFAULT NULL,
   PRIMARY KEY (`id`),
   KEY `fktid` (`tid`),
   CONSTRAINT `fktid` FOREIGN KEY (`tid`) REFERENCES `teacher` (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('1', '小明', '1');
INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('2', '小红', '1');
INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('3', '小张', '1');
INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('4', '小李', '1');
INSERT INTO `student` (`id`, `name`, `tid`) VALUES ('5', '小王', '1');
  • POJO
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Teacher {
    private int id;
    private String name;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
    private int id;
    private String name;
    /**
     * 使用组合,学生关联一个老师
     */
    private Teacher teacher;
}
  • Mapper接口
public interface TeacherMapper {
    /**
     * 查询老师
     * @param id
     * @return
     */
    @Select("select * from teacher where id = #{tid}")
    Teacher getTeacherById(@Param("tid") int id);
}
  • Mapper.xml

写在resource下com.nick.dao,与接口同包,这样最终会target到一个目录,mybatis-config中的mappers配置就可以使用class

<?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.nick.dao.TeacherMapper">
</mapper>
  • 配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<!--核心配置文件-->
<configuration>
    <properties resource="db.properties"/>

    <settings>
        <!--设置日志工厂-->
        <!--SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING-->
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>
    
    <!--可以给实体类起别名-->
    <typeAliases>
        <package name="com.nick.pojo"/>
    </typeAliases>
    
    <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>

    <!--每一个Mapper.xml都需要在Mybatis核心配置文件中注册-->
    <mappers>
        <mapper class="com.nick.dao.TeacherMapper"/>
        <mapper class="com.nick.dao.StudentMapper"/>
    </mappers>
</configuration>
  • 测试
public class MyTest {
    @Test
    public void test() {
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        TeacherMapper mapper = sqlSession.getMapper(TeacherMapper.class);
        Teacher teacher = mapper.getTeacherById(1);
        System.out.println(teacher);
        sqlSession.close();
    }
}
  • 结果
Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
Opening JDBC Connection
Created connection 793315160.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2f490758]
==>  Preparing: select * from teacher where id = ? 
==> Parameters: 1(Integer)
<==    Columns: id, name
<==        Row: 1, 王老师
<==      Total: 1
Teacher(id=1, name=王老师)
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2f490758]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2f490758]
Returned connection 793315160 to pool.

Process finished with exit code 0

10.2 多对一处理

按照查询嵌套处理

查询所有的学生信息,和学生对应的老师信息

  • StudentMapper
public interface StudentMapper {
    /**
     * 查询学生以及对应的老师的信息
     * @return
     */
    List<Student> getStudentAndTeacherInfo();

    /**
     * 找老师
     * @param tid
     * @return
     */
    Teacher getTeacherById(@Param("tid") int tid);
}
  • xml
<select id="getStudentAndTeacherInfo" resultMap="StudentTeacher">
        select * from student
    </select>
    <resultMap id="StudentTeacher" type="Student">
        <result column="id" property="id"/>
        <result column="name" property="name"/>
        <!--复杂的属性 对象 association 集合 collection -->
        <association property="teacher" column="tid" 
                     javaType="Teacher" select="getTeacherById"/>
    </resultMap>
    <select id="getTeacherById" resultType="Teacher">
        select * from teacher where id = #{tid}
    </select>

resultMap用于处理查询结果和实体类对不上的问题

  • result:只能对简单类型的数据进行映射
  • association:对象
  • collection:集合

==<association>这句话的意思是在Student类中有一个对象是Teacher(JavaType)类型的,名为teacher(property),需要从getStudentAndTeacherInfo的返回结果中,通过tid(column)来进行子查询getTeacherById(select)==

  • 测试
@Test
public void test2() {
    SqlSession sqlSession = MybatisUtils.getSqlSession();
    StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
    List<Student> studentList = mapper.getStudentAndTeacherInfo();
    studentList.stream().forEach(item-> System.out.println(item));
    sqlSession.close();
}
  • 结果
Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
Opening JDBC Connection
Created connection 1796371666.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6b1274d2]
==>  Preparing: select * from student 
==> Parameters: 
<==    Columns: id, name, tid
<==        Row: 1, 小明, 1
====>  Preparing: select * from teacher where id = ? 
====> Parameters: 1(Integer)
<====    Columns: id, name
<====        Row: 1, 王老师
<====      Total: 1
<==        Row: 2, 小红, 1
<==        Row: 3, 小张, 1
<==        Row: 4, 小李, 1
<==        Row: 5, 小王, 1
<==      Total: 5
Student(id=1, name=小明, teacher=Teacher(id=1, name=王老师))
Student(id=2, name=小红, teacher=Teacher(id=1, name=王老师))
Student(id=3, name=小张, teacher=Teacher(id=1, name=王老师))
Student(id=4, name=小李, teacher=Teacher(id=1, name=王老师))
Student(id=5, name=小王, teacher=Teacher(id=1, name=王老师))
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6b1274d2]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6b1274d2]
Returned connection 1796371666 to pool.

按照结果嵌套处理

<resultMap id="StudentTeacher2" type="Student">
    <result column="sid" property="id"/>
    <result column="sname" property="name"/>
    <association property="teacher" javaType="Teacher">
        <result column="tname" property="name"/>
    </association>
</resultMap>
<select id="getStudentAndTeacherInfo2" resultMap="StudentTeacher2">
    select s.id sid, s.name sname, t.name tname
    from student s, teacher t
    where s.tid = t.id;
</select>
  • 结果
Logging initialized using 'class org.apache.ibatis.logging.stdout.StdOutImpl' adapter.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
PooledDataSource forcefully closed/removed all connections.
Opening JDBC Connection
Created connection 1796371666.
Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6b1274d2]
==>  Preparing: select s.id sid, s.name sname, t.name tname from student s, teacher t where s.tid = t.id; 
==> Parameters: 
<==    Columns: sid, sname, tname
<==        Row: 1, 小明, 王老师
<==        Row: 2, 小红, 王老师
<==        Row: 3, 小张, 王老师
<==        Row: 4, 小李, 王老师
<==        Row: 5, 小王, 王老师
<==      Total: 5
Student(id=1, name=小明, teacher=Teacher(id=0, name=王老师))
Student(id=2, name=小红, teacher=Teacher(id=0, name=王老师))
Student(id=3, name=小张, teacher=Teacher(id=0, name=王老师))
Student(id=4, name=小李, teacher=Teacher(id=0, name=王老师))
Student(id=5, name=小王, teacher=Teacher(id=0, name=王老师))
Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6b1274d2]
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6b1274d2]
Returned connection 1796371666 to pool.

Process finished with exit code 0

10.3 一对多处理

比如:一个老师对多个学生

  • 老师实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Teacher {
    private int id;
    private String name;
    private List<Student> students;
}
  • xml

按照结果嵌套处理

<resultMap id="teacherAndStuList" type="Teacher">
    <result column="tname" property="name"/>
    <result column="tid" property="id"/>
    <!--注意collection中使用ofType-->
    <collection property="students" ofType="Student">
        <result column="sid" property="id"/>
        <result column="sname" property="name"/>
    </collection>
</resultMap>
<select id="getTeacherAndStuList" resultMap="teacherAndStuList">
    select s.id sid, s.name sname, t.name tname, t.id tid
    from student s, teacher t
    where s.tid = t.id and tid = #{tid}
</select>
  • 结果
Teacher(id=1, name=王老师, students=[Student(id=1, name=小明, teacher=null), Student(id=2, name=小红, teacher=null), Student(id=3, name=小张, teacher=null), Student(id=4, name=小李, teacher=null), Student(id=5, name=小王, teacher=null)])

十一、动态SQL

11.1 什么是动态SQL

动态SQL就是指根据不同的条件生成不同的SQL语句

  • if
  • choose (when, otherwise)
  • trim (where, set)
  • foreach

11.2 if

使用动态 SQL 最常见情景是根据条件包含 where 子句的一部分。比如:

<select id="findActiveBlogWithTitleLike" resultType="Blog">
  SELECT * FROM BLOG
  WHERE state = ‘ACTIVE’
  <if test="title != null">
    AND title like #{title}
  </if>
</select>

这条语句提供了可选的查找文本功能。如果不传入 “title”,那么所有处于 “ACTIVE” 状态的 BLOG 都会返回;如果传入了 “title” 参数,那么就会对 “title” 一列进行模糊查找并返回对应的 BLOG 结果(细心的读者可能会发现,“title” 的参数值需要包含查找掩码通配符字符)。

如果希望通过 “title” 和 “author” 两个参数进行可选搜索该怎么办呢?首先,我想先将语句名称修改成更名副其实的名称;接下来,只需要加入另一个条件即可。

<select id="findActiveBlogLike" resultType="Blog">
  SELECT * FROM BLOG WHERE state = ‘ACTIVE’
  <if test="title != null">
    AND title like #{title}
  </if>
  <if test="author != null and author.name != null">
    AND author_name like #{author.name}
  </if>
</select>

11.3 choose、when、otherwise

有时候,我们不想使用所有的条件,而只是想从多个条件中选择一个使用。针对这种情况,MyBatis 提供了 choose 元素,它有点像 Java 中的 switch 语句。

还是上面的例子,但是策略变为:传入了 “title” 就按 “title” 查找,传入了 “author” 就按 “author” 查找的情形。若两者都没有传入,就返回标记为 featured 的 BLOG(这可能是管理员认为,与其返回大量的无意义随机 Blog,还不如返回一些由管理员精选的 Blog)。

<select id="findActiveBlogLike" resultType="Blog">
  SELECT * FROM BLOG WHERE state = ‘ACTIVE’
  <choose>
    <when test="title != null">
      AND title like #{title}
    </when>
    <when test="author != null and author.name != null">
      AND author_name like #{author.name}
    </when>
    <otherwise>
      AND featured = 1
    </otherwise>
  </choose>
</select>

11.4 trim、where、set

前面几个例子已经方便地解决了一个臭名昭著的动态 SQL 问题。现在回到之前的 “if” 示例,这次我们将 “state = ‘ACTIVE’” 设置成动态条件,看看会发生什么。

<select id="findActiveBlogLike" resultType="Blog">
  SELECT * FROM BLOG
  WHERE
  <if test="state != null">
    state = #{state}
  </if>
  <if test="title != null">
    AND title like #{title}
  </if>
  <if test="author != null and author.name != null">
    AND author_name like #{author.name}
  </if>
</select>

如果没有匹配的条件会怎么样?最终这条 SQL 会变成这样:

SELECT * FROM BLOG
WHERE

这会导致查询失败。如果匹配的只是第二个条件又会怎样?这条 SQL 会是这样:

SELECT * FROM BLOG
WHERE
AND title like ‘someTitle’

这个查询也会失败。这个问题不能简单地用条件元素来解决。这个问题是如此的难以解决,以至于解决过的人不会再想碰到这种问题。

MyBatis 有一个简单且适合大多数场景的解决办法。而在其他场景中,可以对其进行自定义以符合需求。而这,只需要一处简单的改动:

<select id="findActiveBlogLike" resultType="Blog">
  SELECT * FROM BLOG
  <where>
    <if test="state != null">
         state = #{state}
    </if>
    <if test="title != null">
        AND title like #{title}
    </if>
    <if test="author != null and author.name != null">
        AND author_name like #{author.name}
    </if>
  </where>
</select>
  • where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。

如果 where 元素与你期望的不太一样,你也可以通过自定义 trim 元素来定制 where 元素的功能。比如,和 where 元素等价的自定义 trim 元素为:

<trim prefix="WHERE" prefixOverrides="AND |OR ">
  ...
</trim>
  • prefixOverrides 属性会忽略通过管道符分隔的文本序列(注意此例中的空格是必要的)。上述例子会移除所有 prefixOverrides 属性中指定的内容,并且插入 prefix 属性中指定的内容。

用于动态更新语句的类似解决方案叫做 setset 元素可以用于动态包含需要更新的列,忽略其它不更新的列。比如:

<update id="updateAuthorIfNecessary">
  update Author
    <set>
      <if test="username != null">username=#{username},</if>
      <if test="password != null">password=#{password},</if>
      <if test="email != null">email=#{email},</if>
      <if test="bio != null">bio=#{bio}</if>
    </set>
  where id=#{id}
</update>

这个例子中,set 元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号(这些逗号是在使用条件语句给列赋值时引入的)。

来看看与 set 元素等价的自定义 trim 元素吧:

<trim prefix="SET" suffixOverrides=",">
  ...
</trim>

注意,我们覆盖了后缀值设置,并且自定义了前缀值。

11.5 foreach

动态 SQL 的另一个常见使用场景是对集合进行遍历(尤其是在构建 IN 条件语句的时候)。比如:

<select id="selectPostIn" resultType="domain.blog.Post">
  SELECT *
  FROM POST P
  <where>
    <foreach item="item" index="index" collection="list"
        open="ID in (" separator="," close=")" nullable="true">
          #{item}
    </foreach>
  </where>
</select>
  • foreach 元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以及集合项迭代之间的分隔符。这个元素也不会错误地添加多余的分隔符,看它多智能!

在foreach中,key的值含义:

  • collection:值为集合的名称
  • index:值在集合中的索引,如果是Map则为key
  • item:具体的值,如果是Map则为value
  • open:集合以什么开始(
  • seperator:集合的分隔符,
  • close:集合以什么结束)

在SQL中拼接集合后的效果(item1, item2, item3)

提示 你可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数传递给 foreach。当使用可迭代对象或者数组时,index 是当前迭代的序号,item 的值是本次迭代获取到的元素。==当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值==。

至此,我们已经完成了与 XML 配置及映射文件相关的讨论。下一章将详细探讨 Java API,以便你能充分利用已经创建的映射配置。

11.6 script

要在带注解的映射器接口类中使用动态 SQL,可以使用 script 元素。比如:

@Update({"<script>",
      "update Author",
      "  <set>",
      "    <if test='username != null'>username=#{username},</if>",
      "    <if test='password != null'>password=#{password},</if>",
      "    <if test='email != null'>email=#{email},</if>",
      "    <if test='bio != null'>bio=#{bio}</if>",
      "  </set>",
      "where id=#{id}",
      "</script>"})
void updateAuthorValues(Author author);

11.7 bind

bind 元素允许你在 OGNL 表达式以外创建一个变量,并将其绑定到当前的上下文。比如:

<select id="selectBlogsLike" resultType="Blog">
  <bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
  SELECT * FROM BLOG
  WHERE title LIKE #{pattern}
</select>

11.8 sql

可以将公用的SQL代码抽取出来,放到<sql>标签中,并指定一个id值。在使用的时候可以使用<include>进行导入,指定refid即可。

十二、缓存

12.1 什么是缓存

所有的数据库查询,都要了解数据库,而连接是很耗费资源的,要想办法进行优化。

一次查询的结果暂存在可直接取到的地方(内存),下次查询相同数据的时候,直接走内存,不用连接数据库、开连接、拿连接池、关闭连接

  1. 什么是缓存
  • 存在内存中的临时数据
  • 将用户经常查询的数据放在缓存中,用户去查询数据就不用从磁盘上查询,而是直接从缓存中找,从而提高效率,解决了高并发系统的性能问题
  1. 为什么使用缓存
  • 减少和数据库的交互次数,减少系统开销,提高系统效率
  1. 什么样的数据库能使用缓存
  • 经常查询且不经常改变的数据

12.2 Mybatis缓存

MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便地配置和定制。 为了使它更加强大而且易于配置,我们对 MyBatis 3 中的缓存实现进行了许多改进。

Mybatis中的缓存

  • 系统默认定义了两级缓存:一级缓存二级缓存
  • 默认只有一级会开启,sqlSession级别的缓存,也称为本地缓存(在sqlSession开启和关闭之间有效)
  • 二级缓存需要手动开启和配置,基于namespace级别的缓存
  • 为了提高扩展性,Mybatis定义了缓存接口cache,可以通过实现该接口自定义二级缓存

12.3 一级缓存

  • 测试

image.png

  • 结果

image.png

缓存失效的情况

  • 查询不一样的数据
  • insert、update 和 delete数据会刷新缓存(==查两次1号用户之间修改2号用户,也会刷新缓存,会在数据库中查询两次1号用户,及时修改的不是1号==)
  • 查询不同的Mapper
  • 手动清除缓存sqlSession.clearCache

12.4 二级缓存

默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。 要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:

<cache/>

开启二级缓存步骤

  • 在mybatis-config的setting中设置cacheEnabled来开启全局缓存(默认是开启的)
  • 在mapper.xml中添加<cache/>

基本上就是这样。这个简单语句的效果如下:

  • 映射语句文件中的所有 select 语句的结果将会被缓存
  • 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存
  • 缓存会使用==最近最少使用==算法(LRU, Least Recently Used)算法来清除不需要的缓存。
  • 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
  • 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用
  • 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。

提示 缓存只作用于 cache 标签所在的映射文件中的语句。如果你混合使用 Java API 和 XML 映射文件,在共用接口中的语句将不会被默认缓存。你需要使用 @CacheNamespaceRef 注解指定缓存作用域。

这些属性可以通过 cache 元素的属性来修改。比如:

<cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="true"/>

这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。

可用的清除策略有如下几个,默认的清除策略是 LRU:

  • LRU – 最近最少使用:移除最长时间不被使用的对象。
  • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
  • SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
  • WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。

flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。

size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。

readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。

提示 二级缓存是事务性的。这意味着,当 SqlSession 完成并提交时,或是完成并回滚,但没有执行 flushCache=true 的 insert/delete/update 语句时,缓存会获得更新。

二级缓存的工作机制

  • 一个会话查询一条数据,这个数据就会被放在当前会话的一级缓存中
  • 如果当前会话关闭,一级缓存就没了,数据就会被保存在二级缓存中
  • 新的会话查询信息,就可以从二级缓存中获取
  • 不同的mapper查出的数据会放在自己对应的缓存中

对于单个查询语句,可以设置是否使用、刷新缓存

image.png

  • 测试

image.png

  • 结果

image.png

12.5 缓存原理

image.png

12.6 自定义缓存

Ehcache:是一个纯Java进程的缓存框架,具有快速、精干的特点,是Hibernate中默认的CacheProvider

  • 要在程序中使用,先要导包:
 <dependency>
     <groupId>org.mybatis.caches</groupId>
     <artifactId>mybatis-ehcache</artifactId>
     <version>1.2.1</version>
 </dependency>
  • 配置使用第三方cache

image.png

  • Ehcache文件

image.png

image.png