不安分的黑娃
踏踏实实,坚持学习,慢慢就懂了~

参考资料

什么是 MyBatis ?

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

Java 如何操作数据库?

JDBC

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.21</version>
</dependency>
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

/**
 * JDBC连接数据库实例
 * 
 * @author black
 * @date 2020年10月25日 21:40:28
 */
public class JDBCDemo {
    public static final String url = "jdbc:mysql://localhost:3306/blackdb?characterEncoding=UTF8&connectTimeout=6000&socketTimeout=6000&connectionTimeZone=+8:00";
    public static final String user = "black";
    public static final String password = "black";

    /**
     * 使用Statement查询数据库
     * 
     * @param sqlStr sql语句
     */
    public static void query(String sqlStr) {
        try {
            // 加载驱动 mysql8 使用com.mysql.cj.jdbc.Driver;myql5.7仍然用com.mysql.jdbc.Driver
            Class.forName("com.mysql.cj.jdbc.Driver");// mysql
            // 获取数据连接
            Connection conn = DriverManager.getConnection(url, user, password);
            Statement statement = conn.createStatement();
            System.out.println("statement.getFetchSize(): " + statement.getFetchSize());
            // 执行sql
            statement.execute(sqlStr);
            // 获取结果
            ResultSet rs = statement.getResultSet();
            while (rs.next()) {
                String id = rs.getString("id");
                String name = rs.getString("name");
                String age = rs.getString("age");
                System.out.println("id : " + id + ", name : " + name + ", age : " + age);
            }
            // 关闭statement
            statement.close();
            // 关闭数据库连接
            conn.close();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    /**
     * 使用PreparedStatement查询数据库
     * 
     * @param sqlStr        sql语句
     * @param paramterArray 替换参数列表
     */
    public static void query(String sqlStr, String[] paramterArray) {
        try {
            // 1.加载驱动
            Class.forName("com.mysql.cj.jdbc.Driver");
            // 2.获取连接
            Connection conn = DriverManager.getConnection(url, user, password);
            // 3.获取PreparedStatement
            PreparedStatement prepareStatement = conn.prepareStatement(sqlStr);
            System.out.println(" prepareStatement.getFetchSize() = " + prepareStatement.getFetchSize());

            // 4.替换参数
            prepareStatement.setString(1, paramterArray[0]);
            prepareStatement.setString(2, paramterArray[1]);
            prepareStatement.setString(3, paramterArray[2]);
            // 5.执行sql,获取结果
            ResultSet rs = prepareStatement.executeQuery();
            while (rs.next()) {
                // 6.循环获取查询结果
                String id = rs.getString("id");
                String name = rs.getString("name");
                String age = rs.getString("age");
                System.out.println("id : " + id + ", name : " + name + ", age : " + age);
            }
            prepareStatement.close();
            conn.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 批量插入:
     * 使用prepareStatement.addBatch()实现批量插入,理论无数量限制,但是在提交的过程中突然失去了数据库连接或连接被关闭会抛异常。
     */
    public static void insertBatch(String sqlStr) {
        try {
            // 1.加载驱动
            Class.forName("com.mysql.cj.jdbc.Driver");
            // 2.获取连接
            Connection conn = DriverManager.getConnection(url, user, password);
            // 3.获取PreparedStatement
            PreparedStatement prepareStatement = conn.prepareStatement(sqlStr);
            // 4.替换参数
            for (int i = 10; i < 15; i++) {
                prepareStatement.setString(1, "" + i);
                prepareStatement.setString(2, "l" + i);
                prepareStatement.setString(3, "1" + i);
                prepareStatement.addBatch();
            }
            // 5.执行sql,获取结果
            int[] rs = prepareStatement.executeBatch();
            System.out.println("一共插入:" + rs.length + "条记录。");
            prepareStatement.close();
            conn.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 测试
     * 
     * @param args
     */
    public static void main(String[] args) {
        String sqlStr = "select * from person";
        query(sqlStr);
        String sqlStr2 = "select * from person where name in (?,?,?) ";
        String[] paramterArray = new String[] { "bl", "To", "lufi" };
        query(sqlStr2, paramterArray);

//	    String insertSql = "insert into person(id, name, age) values(?,?,?)";
//	    insertBatch(insertSql);
    }
}

Apache Dbutils

<dependency>
    <groupId>commons-dbutils</groupId>
    <artifactId>commons-dbutils</artifactId>
    <version>1.7</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.1</version>
</dependency>
import java.io.Serializable;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

import javax.sql.DataSource;

import org.apache.commons.dbutils.DbUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.ResultSetHandler;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.util.DruidDataSourceUtils;
import com.mysql.cj.jdbc.MysqlDataSource;

/**
 * Apache DBUtils demo
 * 
 * @author black
 *
 */
public class DBUtilsDemo {
    public static String driver = "com.mysql.cj.jdbc.Driver";
    public static final String url = "jdbc:mysql://localhost:3306/blackdb?characterEncoding=UTF8&connectTimeout=6000&socketTimeout=6000&connectionTimeZone=+8:00";
    public static final String user = "root";
    public static final String password = "root";

    /**
     * 查询方法
     */
    public void query(String sql, String paramter) {

        // 这里使用的是阿里巴巴的Druid数据源
        DruidDataSource datasource = new DruidDataSource();
        datasource.setDriverClassName(driver);// 驱动
        datasource.setUrl(url);// jdbc url
        datasource.setUsername(user);// 用户名字
        datasource.setPassword(password);// 密码
        QueryRunner runner = new QueryRunner(datasource);
        try {
            Person person = runner.query(sql, paramter, new BeanHandler<>(Person.class));
            System.out.println(person.toString());
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    /**
     * 查询集合
     * 
     * @param sql
     * @param paramter
     */
    public void queryList(String sql) {
        // 这里使用的是阿里巴巴的Druid数据源
        DruidDataSource datasource = new DruidDataSource();
        datasource.setDriverClassName(driver);// 驱动
        datasource.setUrl(url);// jdbc url
        datasource.setUsername(user);// 用户名字
        datasource.setPassword(password);// 密码
        QueryRunner runner = new QueryRunner(datasource);
        try {
            List<Person> person = runner.query(sql, new BeanListHandler<>(Person.class));
            System.out.println(person.toString());
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        DBUtilsDemo dbUtils = new DBUtilsDemo();
        dbUtils.query("select * from person where name= ?", "black");

        dbUtils.queryList("select * from person;");
    }

}

Spring JDBC Template

引入 spring-jdbc :

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.2.9.RELEASE</version>
</dependency>
<dependency>
	    <groupId>com.alibaba</groupId>
	    <artifactId>druid</artifactId>
	    <version>1.2.1</version>
	</dependency>

示例

import java.util.List;

import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;

import com.alibaba.druid.pool.DruidDataSource;

/**
 * Spring JDBC demo
 * 
 * @author black
 *
 */
public class SpringJdbcDemo {
    // 数据源配置参数
    public static String driver = "com.mysql.cj.jdbc.Driver";
    public static final String url = "jdbc:mysql://localhost:3306/demo";
    public static final String user = "root";
    public static final String password = "root";

    public static void main(String[] args) {

        // 这里使用的是阿里巴巴的Druid数据源
        DruidDataSource datasource = new DruidDataSource();
        datasource.setDriverClassName(driver);// 驱动
        datasource.setUrl(url);// jdbc url
        datasource.setUsername(user);// 用户名字
        datasource.setPassword(password);// 密码

        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(datasource);

        String sql = "select * from person";
        List<Person> person = jdbcTemplate.query(sql, new BeanPropertyRowMapper(Person.class));
        System.out.println(person);

        String queryBlackStr = "select * from person where name = 'black'";
        List<Person> black = jdbcTemplate.query(queryBlackStr, new BeanPropertyRowMapper(Person.class));
        System.out.println(black);
    }
}

快速使用

1. 引入 mybatis jar

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.21</version>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.6</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.30</version>
</dependency>
<!-- logback -->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.28</version>
</dependency>

2. 建表并生成实体类

CREATE TABLE `person` (
  `id` bigint NOT NULL,
  `name` varchar(30) DEFAULT NULL,
  `age` int DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
package com.black.mybatis.app.entity;

import java.io.Serializable;

public class Person implements Serializable {
    private static final long serialVersionUID = -1311367206328692917L;
    private Integer id;
    private String name;
    private Integer age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String toString() {
        StringBuffer str = new StringBuffer();
        str.append("Person: { ");
        str.append("id: ").append(this.id).append(", ");
        str.append("name: ").append(this.name).append(", ");
        str.append("age: ").append(this.age).append(" ");
        str.append("}");
        return str.toString();
    }
}

3. 编写映射器

在 src/main/resources/下新建 mapper/PersonMapper.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 必须是 Mapper 接口类的全限定名 -->
<mapper namespace="com.black.mybatis.app.mapper.PersonMapper">
    <!-- cache: 开启二级缓存 -->
    <cache></cache>
    
    <!-- cache-ref: 引用上一个mapper的二级缓存
        <cache-ref namespace="XXXX"/>
     -->
    
    <resultMap type="person" id="personMap">
        <!-- 主键 -->
        <id column="id" jdbcType="BIGINT" property="id" javaType="Integer"/>
        <!-- result: 数据库列名与属性值映射 -->
        <result column="name" jdbcType="VARCHAR" property="name" javaType="String"/>
        <result column="age" jdbcType="INTEGER" property="age" javaType="Integer"/>
    </resultMap>
    
	<!-- id 必须和 PersonMapper接口的方法名一致 -->
    <select id="selectPersonById" parameterType="map"  resultMap="personMap" >
        select id, name, age from person where id = #{id}
    </select>
    
    <insert id="addPerson" parameterType="person" flushCache="true">
      insert into person (id, name, age)
      values (#{id},#{name, typeHandler=com.black.mybatis.app.typehandler.BlackTypeHandler},#{age})
    </insert>
</mapper>

新建 Mapper接口类:

package com.black.mybatis.app.mapper;

import java.util.List;
import java.util.Map;

import com.black.mybatis.app.entity.Person;

public interface PersonMapper {
    // 方法名必须和 mapper.xml 的<select>的 id 相同
    public List<Person> selectPersonById(Map param);
    // 
    public int addPerson(Person person);
}

4. 编写 mybatis-config.xml 配置文件

在 src/main/resources/下新建 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>

<!-- 此配置文件里的configuration下的一级标签必须按顺序配置!!!否则解析会报错!! -->
<!-- 顺序:properties?,settings?,typeAliases?,typeHandlers?,objectFactory?,objectWrapperFactory?,reflectorFactory?,plugins?,environments?,databaseIdProvider?,mappers? -->
  <!-- properties 配置的属性会替换resource指定配置文件里的相同变量的值 -->
  <properties resource="mybatis.properties">
      <!-- 启用默认值特性 -->
      <property name="org.apache.ibatis.parsing.PropertyParser.enable-default-value" value="true"/>
      <!-- 默认值的分隔符默认为':', 现在配置为'?:'-->
      <property name="org.apache.ibatis.parsing.PropertyParser.default-value-separator" value=":"/>
      
      <property name="driver" value="${driver:com.mysql.jdbc.Driver}"/>
  </properties>

    <!-- 类型别名可为 Java 类型设置一个缩写名字 -->
    <typeAliases>
        <typeAlias type="com.black.mybatis.app.entity.Person" alias="person"/>
    </typeAliases>
    
    <!-- typeHandlers: 类型处理器 -->
    <!-- MyBatis 在设置预处理语句(PreparedStatement)中的参数或从结果集中取出一个值时, 都会用类型处理器将获取到的值以合适的方式转换成 Java 类型。下表描述了一些默认的类型处理器。
                         提示 从 3.4.5 开始,MyBatis 默认支持 JSR-310(日期和时间 API) 。 
    -->
    <typeHandlers>
    
        <!-- 提供了很多类型处理器比如BooleanTypeHandler 对应java类型(java.lang.Boolean, boolean ) 对应数据库兼容的 BOOLEAN-->  
        <typeHandler handler="BooleanTypeHandler"/>
               
        <!-- 自定义typehandler 具体做法为:
                                             实现 org.apache.ibatis.type.TypeHandler 接口,
                                           或继承一个很便利的类 org.apache.ibatis.type.BaseTypeHandler, 并且可以(可选地)将它映射到一个 JDBC 类型。
        
         <typeHandler handler="com.black.mybatis.app.typehandler.BlackTypeHandler"/>
		 -->
    </typeHandlers>
    
    <!-- 对象工厂:每次 MyBatis 创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成实例化工作。-->
    <!-- 默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认无参构造方法,要么通过存在的参数映射来调用带有参数的构造方法。 如果想覆盖对象工厂的默认行为,可以通过创建自己的对象工厂来实现。 -->
    <objectFactory type="com.black.mybatis.app.factory.BlackObjectFactory">
        <!-- <property name="name" value=""/> -->
    </objectFactory>
    
    
    <!-- 插件 -->
    <!-- MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。
                                                     默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
					Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
					ParameterHandler (getParameterObject, setParameters)
					ResultSetHandler (handleResultSets, handleOutputParameters)
					StatementHandler (prepare, parameterize, batch, update, query) 
	-->
    
    <!-- 环境配置 -->
  <environments default="development">
    <environment id="development">
       <!-- 事务管理器 : -->
	    <transactionManager type="JDBC">
	    <!-- 在 MyBatis 中有两种类型的事务管理器(也就是 type="[JDBC|MANAGED]"):
	            JDBC – 这个配置直接使用了 JDBC 的提交和回滚设施,它依赖从数据源获得的连接来管理事务作用域。
	            MANAGED – 这个配置几乎没做什么。它从不提交或回滚一个连接,而是让容器来管理事务的整个生命周期(比如 JEE 应用服务器的上下文)。
	                                                                    默认情况下它会关闭连接。然而一些容器并不希望连接被关闭,因此需要将 closeConnection 属性设置为 false 来阻止默认的关闭行为。 -->
	        <property name="closeConnection" value="false"/>
	        
	        <!-- 如果你正在使用 Spring + MyBatis,则没有必要配置事务管理器,因为 Spring 模块会使用自带的管理器来覆盖前面的配置。 -->
	        
	    </transactionManager>
      
      <!-- dataSource 元素使用标准的 JDBC 数据源接口来配置 JDBC 连接对象的资源。 -->
      <!-- 
      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) 方法传递值为 UTF8 的 encoding 属性给数据库驱动。
      
       -->
       
       <!-- 
      POOLED– 这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。 这种处理方式很流行,能使并发 Web 应用快速响应请求。
		除了上述提到 UNPOOLED 下的属性外,还有更多属性用来配置 POOLED 的数据源:
		poolMaximumActiveConnections – 在任意时间可存在的活动(正在使用)连接数量,默认值:10
		poolMaximumIdleConnections – 任意时间可能存在的空闲连接数。
		poolMaximumCheckoutTime – 在被强制返回之前,池中连接被检出(checked out)时间,默认值:20000 毫秒(即 20 秒)
		poolTimeToWait – 这是一个底层设置,如果获取连接花费了相当长的时间,连接池会打印状态日志并重新尝试获取一个连接(避免在误配置的情况下一直失败且不打印日志),默认值:20000 毫秒(即 20 秒)。
		poolMaximumLocalBadConnectionTolerance – 这是一个关于坏连接容忍度的底层设置, 作用于每一个尝试从缓存池获取连接的线程。 如果这个线程获取到的是一个坏的连接,那么这个数据源允许这个线程尝试重新获取一个新的连接,但是这个重新尝试的次数不应该超过 poolMaximumIdleConnections 与 poolMaximumLocalBadConnectionTolerance 之和。 默认值:3(新增于 3.4.5)
		poolPingQuery – 发送到数据库的侦测查询,用来检验连接是否正常工作并准备接受请求。默认是“NO PING QUERY SET”,这会导致多数数据库驱动出错时返回恰当的错误消息。
		poolPingEnabled – 是否启用侦测查询。若开启,需要设置 poolPingQuery 属性为一个可执行的 SQL 语句(最好是一个速度非常快的 SQL 语句),默认值:false。
		poolPingConnectionsNotUsedFor – 配置 poolPingQuery 的频率。可以被设置为和数据库连接超时时间一样,来避免不必要的侦测,默认值:0(即所有连接每一时刻都被侦测 — 当然仅当 poolPingEnabled 为 true 时适用)。
       -->
       
       <!-- 
       JNDI – 这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用。这种数据源配置只需要两个属性:
			initial_context – 这个属性用来在 InitialContext 中寻找上下文(即,initialContext.lookup(initial_context))。这是个可选属性,如果忽略,那么将会直接从 InitialContext 中寻找 data_source 属性。
			data_source – 这是引用数据源实例位置的上下文路径。提供了 initial_context 配置时会在其返回的上下文中进行查找,没有提供时则直接在 InitialContext 中查找。
			和其他数据源配置类似,可以通过添加前缀“env.”直接把属性传递给 InitialContext。比如:
                env.encoding=UTF8
                                    这就会在 InitialContext 实例化时往它的构造方法传递值为 UTF8 的 encoding 属性。 -->
      <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): 这些配置会告诉 MyBatis 去哪里找映射文件 -->
    <!-- 使用相对于类路径的资源引用  -->
	<mappers>
	  <mapper resource="mapper/PersonMapper.xml"/>
	</mappers> 
</configuration>

在 src/main/resources/下新建 mybatis.properties ,内容如下:

#数据源配置
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/blackdb?characterEncoding=UTF8&connectTimeout=6000&socketTimeout=6000&connectionTimeZone=+8:00
username=black
password=black

5. 编写测试代码

package com.black.mybatis.app;

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

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import com.black.mybatis.app.entity.Person;
import com.black.mybatis.app.mapper.PersonMapper;

/**
 * 应用启动类
 * 
 * @author black
 *
 */
public class MyBatisStandaloneApp {

    public static void main(String[] args) throws IOException {
        query();
    }

    /**
     * mybatis 单实例应用-查询方法
     * 
     * @throws IOException
     */
    public static void query() throws IOException {
        // 1.获取配置文件流
        System.out.println("1.获取配置文件流...");
        String configXMLFile = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(configXMLFile);
        // 2. 创建 SqlSessionFactory
        System.out.println("2.创建SqlSessionFactory...");
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        SqlSessionFactory sqlSessionFactory = builder.build(inputStream);
        // 3. 开启一个 SqlSession(=>得到的是DefaultSqlSession)
        System.out.println("3.创建SqlSession...");
        SqlSession sqlSession = sqlSessionFactory.openSession();//注: sqlSession 可以直接执行 sql
        // 4. 获取 Mapper 接口类
        PersonMapper personMapper = sqlSession.getMapper(PersonMapper.class);
        // 5. 调用 Mapper 接口类方法查询数据
        Map param = new HashMap();
        param.put("id", "1");
        List<Person> resultList = personMapper.selectPersonById(param);
        // 6. 输出查询结果
        System.out.println(resultList.toString());
        // 7. 关闭SqlSession(查询完成后,必须关闭 SqlSession)
        sqlSession.close();
    }

    /**
     * 新增方法
     * 
     * @param person
     * @throws IOException
     */
    public static void insert(Person person) throws IOException {
        // 1. 获取配置文件流
        System.out.println("1.获取配置文件流...");
        String configXMLFile = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(configXMLFile);
        // 2. 创建SqlSessionFactory
        System.out.println("2.创建SqlSessionFactory...");
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        SqlSessionFactory sqlSessionFactory = builder.build(inputStream);
        // 3. 开启一个 SqlSession(=>得到的是DefaultSqlSession)
        System.out.println("3.创建 SqlSession...");
        // try 会自动关闭 sqlSession(JDK 1.7 提供的新特性)
        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
            // 4. 获取接口类
            PersonMapper personMapper = sqlSession.getMapper(PersonMapper.class);
            // 5. 使用 Mapper 插入一条记录
            int count = personMapper.addPerson(person);
            // 6. 打印插入成功数量
            System.out.println("" + count);

            // 7. 查询数据,验证结果
            Map param = new HashMap();
            param.put("id", person.getId());
            List<Person> resultList = personMapper.selectPersonById(param);
            System.out.println(resultList.toString());
        }
    }
}

6. 代码结构

mybatis-app
    |
    |-- src/main/java
                  |
                  |-- com/black/mybatis/app
                                  |-- MyBatisStandaloneApp.java
                                  |-- entity/Person.java
                                  |-- mapper/PersonMapper.java
    |-- src/main/resources
                  |-- mybatis-config.xml
                  |-- mybatis.properties
                  |-- mapper/PersonMapper.xml

MyBatis 核心 API

使用 MyBatis 操作数据库基本步骤:

  1. 获取 mybatis-config.xml 文件流
  2. SqlSessionFactoryBuilder 使用 mybatis-config.xml 文件流构造 SqlSessionFactory 实例
  3. SqlSessionFactory 开启一个生成 SqlSession
  4. 从 SqlSession 获取 Mapper 接口类的实现
  5. 调用 Mapper 接口方法进行数据库操作
  6. 关闭 SqlSession

由以上步骤可以看出 mybatis-config.xml,SqlSessionFactory,SqlSession 是 mybatis的核心 API。

1. mybatis-config.xml

在使用 SqlSessionFactoryBuilder 构造 SqlSessionFactory 时,要么要求传入 mybatis-config.xml 文件流,要么传入自行构建 Configuration 实例。

mybatis-config.xml 文件是定义能够影响 MyBatis 行为的设置和属性信息,即 MyBatis 配置文件。

MyBatis 配置文件标签结构如下:

<configuration>(配置)
    |-- <properties/>(属性)
    |-- <settings/>(设置)
    |-- <typeAliases/>(类型别名)
    |-- <typeHandlers/>(类型处理器)
    |-- <objectFactory/>(对象工厂)
    |-- <plugins/>(插件)
    |-- <environments/>(环境配置)
         |-- <environment/>(环境变量)
             |-- <transactionManager/>(事务管理器)
             |-- <dataSource/>(数据源)
    |-- <databaseIdProvider/>(数据库厂商标识)
    |-- <mappers/>(映射器)

configuration下的一级标签必须按顺序配置!否则解析会报错!!标签顺序:properties?,settings?,typeAliases?,typeHandlers?,objectFactory?,objectWrapperFactory?,reflectorFactory?,plugins?,environments?,databaseIdProvider?,mappers?

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>
    ...
<configuration/>

1.1 properties

属性可以在外部进行配置,并可以进行动态替换。属性既可以

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

配置文件中其他地方如果使用 properties 标签定义的属性,那么使用 ${propertyName} 进行引用,比如数据源:

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

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

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

如果一个属性存在于下面的多个位置,那么 MyBatis 将按照以下顺序来加载它们:
首先,读取在 properties 元素体中指定的属性;
其次,读取在 properties 元素的类路径 resource 或 url 指定的属性,且会覆盖已经指定了的重复属性;
最后,读取作为方法参数传递的属性,且会覆盖已经从 properties 元素体和 resource 或 url 属性中加载了的重复属性。
因此,通过方法参数传递的属性的优先级最高,resource 或 url 指定的属性优先级中等,在 properties 元素体中指定的属性优先级最低。

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

<properties resource="org/mybatis/example/config.properties">
  <!-- 先启用默认值特性,然后才能使用 -->
  <property name="org.apache.ibatis.parsing.PropertyParser.enable-default-value" value="true"/> 
   <!-- 如果属性 'username' 没有被配置,'username' 属性的值将为 'ut_user' -->
  <property name="username" value="${username:ut_user}"/>
</properties>

上面的默认值与属性名的分隔符是 :,那么如果想改变分隔符可进行如下设置:

<properties resource="org/mybatis/example/config.properties">
  <!-- 修改默认值的分隔符为"?:" -->
  <property name="org.apache.ibatis.parsing.PropertyParser.default-value-separator" value="?:"/> 
</properties>

1.2 settings(设置)

这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为。

设置名 描述 有效值 默认值
cacheEnabled 全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。 true
lazyLoadingEnabled 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态。 false
aggressiveLazyLoading 开启时,任一方法的调用都会加载该对象的所有延迟加载属性。 否则,每个延迟加载属性会按需加载(参考 lazyLoadTriggerMethods)。 false (在 3.4.1 及之前的版本中默认为 true)
multipleResultSetsEnabled 是否允许单个语句返回多结果集(需要数据库驱动支持)。 true
useColumnLabel 使用列标签代替列名。实际表现依赖于数据库驱动,具体可参考数据库驱动的相关文档,或通过对比测试来观察。 true
useGeneratedKeys 允许 JDBC 支持自动生成主键,需要数据库驱动支持。如果设置为 true,将强制使用自动生成主键。尽管一些数据库驱动不支持此特性,但仍可正常工作(如 Derby)。 false
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。 false
safeResultHandlerEnabled 是否允许在嵌套语句中使用结果处理器(ResultHandler)。如果允许使用则设置为 false。 true
mapUnderscoreToCamelCase 是否开启驼峰命名自动映射,即从经典数据库列名 A_COLUMN 映射到经典 Java 属性名 aColumn。 false
localCacheScope(一级缓存) MyBatis 利用本地缓存机制(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 的。 false
returnInstanceForEmptyRow 当返回行的所有列都是空时,MyBatis默认返回 null。 当开启这个设置时,MyBatis会返回一个空实例。 请注意,它也适用于嵌套的结果集(如集合或关联)。(新增于 3.4.2) false
logPrefix 指定 MyBatis 增加到日志名称的前缀。 任何字符串 未设置
logImpl 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。 SLF4J
LOG4J(deprecated since 3.5.9)
LOG4J2
JDK_LOGGING
COMMONS_LOGGING
STDOUT_LOGGING
NO_LOGGING
未设置
proxyFactory 指定 Mybatis 创建可延迟加载对象所用到的代理工具。 CGLIB JAVASSIST JAVASSIST (MyBatis 3.3 以上)
vfsImpl 指定 VFS(虚拟文件系统;主要是通过程序能够方便读取本地文件系统、FTP文件系统等系统中的文件资源) 的实现 。mybatis内置了2个实现类: JBoss6VFS.class, DefaultVFS.class 自定义 VFS 的实现的类全限定名,以逗号分隔。 未设置
useActualParamName 允许使用方法签名中的名称作为语句参数名称。 为了使用该特性,你的项目必须采用 Java 8 编译,并且加上 -parameters 选项。(新增于 3.4.1) true
configurationFactory 指定一个提供 Configuration 实例的类。 这个被返回的 Configuration 实例用来加载被反序列化对象的延迟加载属性值。 这个类必须包含一个签名为static Configuration getConfiguration() 的方法。(新增于 3.2.3) 一个类型别名或完全限定类名。 未设置
shrinkWhitespacesInSql 从SQL中删除多余的空格字符。请注意,这也会影响SQL中的文字字符串。 (新增于 3.5.5) false
defaultSqlProviderType Specifies 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. 一个类型别名或完全限定类名。 未设置
nullableOnForEach Specifies the default value of 'nullable' attribute on 'foreach' tag. (Since 3.5.9) 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>

1.3 typeAliases (类型别名)

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>

typeAliases 也可以指定具体的包,为包下所有的类添加别名()

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

// 如果包下的类没有 @Alias 那么就以类名首字母小写为别名;否则使用 @Alias
@Alias("author")
public class Author {
    ...
}

MyBatis 预定义的类型别名:

别名 映射的类型
_byte byte
_long long
_short short
_int int
_integer int
_double double
_float float
_boolean boolean
string String
byte Byte
long Long
short Short
int Integer
integer Integer
double Double
float Float
boolean Boolean
date Date
decimal BigDecimal
bigdecimal BigDecimal
object Object
map Map
hashmap HashMap
list List
arraylist ArrayList
collection Collection
iterator Iterator

1.4 typeHandlers(类型处理器)

MyBatis 在设置预处理语句(PreparedStatement)中的参数或从结果集中取出一个值时, 都会用类型处理器将获取到的值以合适的方式转换成 Java 类型。

从 3.4.5 开始,MyBatis 默认支持 JSR-310(日期和时间 API) 。

MyBatis 提供的类型处理器

类型处理器 Java 类型 JDBC 类型
BooleanTypeHandler java.lang.Boolean, boolean 数据库兼容的 BOOLEAN
ByteTypeHandler java.lang.Byte, byte 数据库兼容的 NUMERIC 或 BYTE
ShortTypeHandler java.lang.Short, short 数据库兼容的 NUMERIC 或 SMALLINT
IntegerTypeHandler java.lang.Integer, int 数据库兼容的 NUMERIC 或 INTEGER
LongTypeHandler java.lang.Long, long 数据库兼容的 NUMERIC 或 BIGINT
FloatTypeHandler java.lang.Float, float 数据库兼容的 NUMERIC 或 FLOAT
DoubleTypeHandler java.lang.Double, double 数据库兼容的 NUMERIC 或 DOUBLE
BigDecimalTypeHandler java.math.BigDecimal 数据库兼容的 NUMERIC 或 DECIMAL
StringTypeHandler java.lang.String CHAR, VARCHAR
ClobReaderTypeHandler java.io.Reader -
ClobTypeHandler java.lang.String CLOB, LONGVARCHAR
NStringTypeHandler java.lang.String NVARCHAR, NCHAR
NClobTypeHandler java.lang.String NCLOB
BlobInputStreamTypeHandler java.io.InputStream -
ByteArrayTypeHandler byte[] 数据库兼容的字节流类型
BlobTypeHandler byte[] BLOB, LONGVARBINARY
DateTypeHandler java.util.Date TIMESTAMP
DateOnlyTypeHandler java.util.Date DATE
TimeOnlyTypeHandler java.util.Date TIME
SqlTimestampTypeHandler java.sql.Timestamp TIMESTAMP
SqlDateTypeHandler java.sql.Date DATE
SqlTimeTypeHandler java.sql.Time TIME
ObjectTypeHandler Any OTHER 或未指定类型
EnumTypeHandler Enumeration Type VARCHAR 或任何兼容的字符串类型,用来存储枚举的名称(而不是索引序数值)
EnumOrdinalTypeHandler Enumeration Type 任何兼容的 NUMERIC 或 DOUBLE 类型,用来存储枚举的序数值(而不是名称)。
SqlxmlTypeHandler java.lang.String SQLXML
InstantTypeHandler java.time.Instant TIMESTAMP
LocalDateTimeTypeHandler java.time.LocalDateTime TIMESTAMP
LocalDateTypeHandler java.time.LocalDate DATE
LocalTimeTypeHandler java.time.LocalTime TIME
OffsetDateTimeTypeHandler java.time.OffsetDateTime TIMESTAMP
OffsetTimeTypeHandler java.time.OffsetTime TIME
ZonedDateTimeTypeHandler java.time.ZonedDateTime TIMESTAMP
YearTypeHandler java.time.Year INTEGER
MonthTypeHandler java.time.Month INTEGER
YearMonthTypeHandler java.time.YearMonth VARCHAR 或 LONGVARCHAR
JapaneseDateTypeHandler java.time.chrono.JapaneseDate DATE

自定义 TypeHandler
实现 org.apache.ibatis.type.TypeHandler 接口, 或继承一个很便利的类 org.apache.ibatis.type.BaseTypeHandler, 并且可以(可选地)将它映射到一个 JDBC 类型。

// ExampleTypeHandler.java
@MappedJdbcTypes(JdbcType.VARCHAR)
public class ExampleTypeHandler extends BaseTypeHandler<String> {

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
    ps.setString(i, parameter);
  }

  @Override
  public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
    return rs.getString(columnName);
  }

  @Override
  public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
    return rs.getString(columnIndex);
  }

  @Override
  public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
    return cs.getString(columnIndex);
  }
}

然后,mybatis-config.xml 配置自定义配置类:

<typeHandlers>
  <typeHandler handler="org.mybatis.example.ExampleTypeHandler"/>
</typeHandlers>

1.5 objectFactory(对象工厂)

// ExampleObjectFactory.java
public class ExampleObjectFactory extends DefaultObjectFactory {
  public Object create(Class type) {
    return super.create(type);
  }
  public Object create(Class type, List<Class> constructorArgTypes, List<Object> constructorArgs) {
    return super.create(type, constructorArgTypes, constructorArgs);
  }
  public void setProperties(Properties properties) {
    super.setProperties(properties);
  }
  public <T> boolean isCollection(Class<T> type) {
    return Collection.class.isAssignableFrom(type);
  }}
<!-- mybatis-config.xml -->
<objectFactory type="org.mybatis.example.ExampleObjectFactory">
  <property name="someProperty" value="100"/>
</objectFactory>

1.6 plugins(插件)

默认情况下,MyBatis 允许使用插件拦截的方法调用包括:

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

定义 拦截类:


// @Intercepts 注解的 @Signature属性定义了插件拦截的方法签名

import org.apache.ibatis.plugin.Interceptor;

@Intercepts({@Signature(
  type= Executor.class,
  method = "update",
  args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
  private Properties properties = new Properties();
  public Object intercept(Invocation invocation) throws Throwable {
    // 调用前置处理
    Object returnObject = invocation.proceed();
    // 调用后置处理
    return returnObject;
  }
  public void setProperties(Properties properties) {
    this.properties = properties;
  }
}

<!-- mybatis-config.xml -->
<plugins>
  <plugin interceptor="org.mybatis.example.ExamplePlugin">
    <property name="someProperty" value="100"/>
  </plugin>
</plugins>

1.7 environments(环境配置)

MyBatis 可以配置多种环境,比如 生产和开发环境。

尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择一种环境。

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

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

<!-- 默认加载 development 对应的环境配置  -->
<environments default="development">
  <!-- 环境 ID 为 “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>
事务管理器(transactionManager)

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

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

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

自定义事务管理器:自行实现 TransactionFactory 接口和 Transaction 接口。

数据源(dataSource)

dataSource 元素使用标准的 JDBC 数据源接口来配置 JDBC 连接对象的资源。
有三种内建的数据源类型(也就是 type="[UNPOOLED|POOLED|JNDI]"):

  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) 方法传递值为 UTF8 的 encoding 属性给数据库驱动。

  1. POOLED
    这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。
    除了上述提到 UNPOOLED 下的属性外,还有更多属性用来配置 POOLED 的数据源:
poolMaximumActiveConnections – 在任意时间可存在的活动(正在使用)连接数量,默认值:10
poolMaximumIdleConnections – 任意时间可能存在的空闲连接数。
poolMaximumCheckoutTime – 在被强制返回之前,池中连接被检出(checked out)时间,默认值:20000 毫秒(即 20 秒)
poolTimeToWait – 这是一个底层设置,如果获取连接花费了相当长的时间,连接池会打印状态日志并重新尝试获取一个连接(避免在误配置的情况下一直失败且不打印日志),默认值:20000 毫秒(即 20 秒)。
poolMaximumLocalBadConnectionTolerance – 这是一个关于坏连接容忍度的底层设置, 作用于每一个尝试从缓存池获取连接的线程。 如果这个线程获取到的是一个坏的连接,那么这个数据源允许这个线程尝试重新获取一个新的连接,但是这个重新尝试的次数不应该超过 poolMaximumIdleConnections 与 poolMaximumLocalBadConnectionTolerance 之和。 默认值:3(新增于 3.4.5)
poolPingQuery – 发送到数据库的侦测查询,用来检验连接是否正常工作并准备接受请求。默认是“NO PING QUERY SET”,这会导致多数数据库驱动出错时返回恰当的错误消息。
poolPingEnabled – 是否启用侦测查询。若开启,需要设置 poolPingQuery 属性为一个可执行的 SQL 语句(最好是一个速度非常快的 SQL 语句),默认值:false。
poolPingConnectionsNotUsedFor – 配置 poolPingQuery 的频率。可以被设置为和数据库连接超时时间一样,来避免不必要的侦测,默认值:0(即所有连接每一时刻都被侦测 — 当然仅当 poolPingEnabled 为 true 时适用)。
JNDI – 这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用。这种数据源配置只需要两个属性:

initial_context – 这个属性用来在 InitialContext 中寻找上下文(即,initialContext.lookup(initial_context))。这是个可选属性,如果忽略,那么将会直接从 InitialContext 中寻找 data_source 属性。
data_source – 这是引用数据源实例位置的上下文路径。提供了 initial_context 配置时会在其返回的上下文中进行查找,没有提供时则直接在 InitialContext 中查找。
和其他数据源配置类似,可以通过添加前缀“env.”直接把属性传递给 InitialContext。比如:env.encoding=UTF8
这就会在 InitialContext 实例化时往它的构造方法传递值为 UTF8 的 encoding 属性。

实现接口 org.apache.ibatis.datasource.DataSourceFactory 来使用第三方数据源实现.

  1. JNDI
    这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用。
    这种数据源配置只需要两个属性:
initial_context – 这个属性用来在 InitialContext 中寻找上下文(即,initialContext.lookup(initial_context))。这是个可选属性,如果忽略,那么将会直接从 InitialContext 中寻找 data_source 属性。
data_source – 这是引用数据源实例位置的上下文路径。提供了 initial_context 配置时会在其返回的上下文中进行查找,没有提供时则直接在 InitialContext 中查找。
和其他数据源配置类似,可以通过添加前缀“env.”直接把属性传递给 InitialContext。比如:env.encoding=UTF8
这就会在 InitialContext 实例化时往它的构造方法传递值为 UTF8 的 encoding 属性。

1.8 databaseIdProvider(数据库厂商标识)

MyBatis 可以根据不同的数据库厂商执行不同的语句,这种多厂商的支持是基于映射语句中的 databaseId 属性。

<databaseIdProvider type="DB_VENDOR">
  <property name="SQL Server" value="sqlserver"/>
  <property name="DB2" value="db2"/>
  <property name="Oracle" value="oracle" />
</databaseIdProvider>

通过实现接口 org.apache.ibatis.mapping.DatabaseIdProvider 并在 mybatis-config.xml 中注册来构建自己的 DatabaseIdProvider:

public interface DatabaseIdProvider {
  default void setProperties(Properties p) { // 从 3.5.2 开始,该方法为默认方法
    // 空实现
  }
  String getDatabaseId(DataSource dataSource) throws SQLException;
}

1.9 mappers(映射器)

MyBatis 使用相对于类路径的资源引用,或完全限定资源定位符(包括 file:/// 形式的 URL),或类名和包名等查找映射文件。

<!-- 使用相对于类路径的资源引用 -->
<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>

2. XML 映射器

XML 映射器主要用于配置 sql,定义返回结果字段映射。
SQL 映射文件只有很少的几个顶级元素:

  • cache – 该命名空间的缓存配置。
  • cache-ref – 引用其它命名空间的缓存配置。
  • resultMap – 描述如何从数据库结果集中加载对象,是最复杂也是最强大的元素。
  • parameterMap(未来被移除) – 老式风格的参数映射。此元素已被废弃,并可能在将来被移除!请使用行内参数映射。文档中不会介绍此元素。
  • sql – 可被其它语句引用的可重用语句块。
  • insert – 映射插入语句。
  • update – 映射更新语句。
  • delete – 映射删除语句。
  • select – 映射查询语句

2.1 select

<select/> 表示一个查询语句,使用示例:

<!-- 这个语句名为 selectPerson,接受一个 int(或 Integer)类型的参数,并返回一个 HashMap 类型的对象,其中的键是列名,值便是结果行中的对应值。 -->
<select id="selectPerson" parameterType="int" resultType="hashmap">
  SELECT * FROM PERSON WHERE ID = #{id}
</select>

#{id} 告诉 MyBatis 创建一个预处理语句(PreparedStatement)参数,在 JDBC 中,这样的一个参数在 SQL 中会由一个“?”来标识,并被传递到一个新的预处理语句中.
能够防止 SQL 注入风险。

select 标签的属性有:

属性 描述
id 在命名空间中唯一的标识符,可以被用来引用这条语句。
parameterType 将会传入这条语句的参数的类全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler)推断出具体传入语句的参数,默认值为未设置(unset)。
parameterMap 用于引用外部 parameterMap 的属性,目前已被废弃。请使用行内参数映射和 parameterType 属性。
resultType 期望从这条语句中返回结果的类全限定名别名resultType 和 resultMap 之间只能同时使用一个。
resultMap 对外部 resultMap 的命名引用。结果映射是 MyBatis 最强大的特性,如果你对其理解透彻,许多复杂的映射问题都能迎刃而解。 resultType 和 resultMap 之间只能同时使用一个。
flushCache 将其设置为 true 后,只要语句被调用,都会导致本地缓存二级缓存被清空,默认值:false
useCache 将其设置为 true 后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对 select 元素为 true。
timeout 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖数据库驱动)。
fetchSize 这是一个给驱动的建议值,尝试让驱动程序每次批量返回的结果行数等于这个设置值。 默认值为未设置(unset)(依赖驱动)。
statementType 可选 STATEMENT,PREPARED 或 CALLABLE。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED
resultSetType FORWARD_ONLY,SCROLL_SENSITIVE, SCROLL_INSENSITIVE 或 DEFAULT(等价于 unset) 中的一个,默认值为 unset (依赖数据库驱动)。
databaseId 如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有不带 databaseId 或匹配当前 databaseId 的语句;如果带和不带的语句都有,则不带的会被忽略。
resultOrdered 这个设置仅针对嵌套结果 select 语句:如果为 true,将会假设包含了嵌套结果集或是分组,当返回一个主结果行时,就不会产生对前面结果集的引用。 这就使得在获取嵌套结果集的时候不至于内存不够用。默认值:false。
resultSets 这个设置仅适用于多结果集的情况。它将列出语句执行后返回的结果集并赋予每个结果集一个名称,多个名称之间以逗号分隔。

2.2 insert, update 和 delete

数据变更语句 insert,update 和 delete 的实现非常接近:

<insert
  id="insertAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  keyProperty=""
  keyColumn=""
  useGeneratedKeys=""
  timeout="20">

<update
  id="updateAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  timeout="20">

<delete
  id="deleteAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  timeout="20">

Insert, Update, Delete 元素的属性:

属性 描述
id 在命名空间中唯一的标识符,可以被用来引用这条语句。
parameterType 将会传入这条语句的参数的类全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler)推断出具体传入语句的参数,默认值为未设置(unset)。
flushCache 将其设置为 true 后,只要语句被调用,都会导致本地缓存和二级缓存被清空,默认值:(对 insert、update 和 delete 语句)true。
timeout 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖数据库驱动)。
statementType 可选 STATEMENT,PREPARED 或 CALLABLE。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED
useGeneratedKeys (仅适用于 insert 和 update)这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键(比如:像 MySQL 和 SQL Server 这样的关系型数据库管理系统的自动递增字段),默认值:false
keyProperty (仅适用于 insert 和 update)指定能够唯一识别对象的属性,MyBatis 会使用 getGeneratedKeys 的返回值或 insert 语句的 selectKey 子元素设置它的值,默认值:未设置(unset)。如果生成列不止一个,可以用逗号分隔多个属性名称。
keyColumn (仅适用于 insert 和 update)设置生成键值在表中的列名,在某些数据库(像 PostgreSQL)中,当主键列不是表中的第一列的时候,是必须设置的。如果生成列不止一个,可以用逗号分隔多个属性名称。
databaseId 如果配置了数据库厂商标识(databaseIdProvider),MyBatis 会加载所有不带 databaseId 或匹配当前 databaseId 的语句;如果带和不带的语句都有,则不带的会被忽略。

下面是 insert,update 和 delete 语句的示例:

<insert id="insertAuthor" useGeneratedKeys="true"
    keyProperty="id">
  insert into Author (username, password, email, bio) values
  <foreach item="item" collection="list" separator=",">
    (#{item.username}, #{item.password}, #{item.email}, #{item.bio})
  </foreach>
</insert>

<update id="updateAuthor">
  update Author set
    username = #{username},
    password = #{password},
    email = #{email},
    bio = #{bio}
  where id = #{id}
</update>

<delete id="deleteAuthor">
  delete from Author where id = #{id}
</delete>

2.3 selectKey

对于不支持自动生成主键列的数据库和可能不支持自动生成主键的 JDBC 驱动,MyBatis 使用 <selectKey/>生成主键。

<selectKey
  keyProperty="id"
  resultType="int"
  order="BEFORE"
  statementType="PREPARED">
属性 描述
keyProperty selectKey 语句结果应该被设置到的目标属性。如果生成列不止一个,可以用逗号分隔多个属性名称。
keyColumn 返回结果集中生成列属性的列名。如果生成列不止一个,可以用逗号分隔多个属性名称。
resultType 结果的类型。
order 可以设置为 BEFORE 或 AFTER。如果设置为 BEFORE,那么它首先会生成主键,设置 keyProperty 再执行插入语句。如果设置为 AFTER,那么先执行插入语句,然后是 selectKey 中的语句 - 这和 Oracle 数据库的行为相似,在插入语句内部可能有嵌入索引调用。
statementType MyBatis 支持 STATEMENT,PREPARED 和 CALLABLE 类型的映射语句,分别代表 Statement, PreparedStatement 和 CallableStatement 类型。

2.4 sql

<sql/>用来定义可重用的 SQL 代码片段,以便在其它语句中使用。

<!-- 定义重用 sql 块 -->
<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>

<!-- 使用“<include/>”标签引用 上边定义的 sql块 -->
<select id="selectUsers" resultType="map">
  select
    <include refid="userColumns"><property name="alias" value="t1"/></include>,
    <include refid="userColumns"><property name="alias" value="t2"/></include>
  from some_table t1
    cross join some_table t2
</select>

2.5 参数

Mybatis 提供 2 种参数使用方式 : '\({参数名}' 和 '#{参数名}'。 '\){参数名}' : 只是替换文本,不做预编译处理,会出现 SQL 注入问题。
'#{参数名}':MyBatis 会创建 PreparedStatement 参数占位符,并通过占位符安全地设置参数(就像使用 ? 一样)。

参数的高级用法示例:

#{property,javaType=int,jdbcType=NUMERIC}


#{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}

对于数值类型,还可以设置 numericScale 指定小数点后保留的位数:
#{height,javaType=double,jdbcType=NUMERIC,numericScale=2}

mode 属性允许你指定 IN,OUT 或 INOUT 参数。如果参数的 mode 为 OUT 或 INOUT,将会修改参数对象的属性值,以便作为输出参数返回。 如果 mode 为 OUT(或 INOUT),而且 jdbcType 为 CURSOR(也就是 Oracle 的 REFCURSOR),你必须指定一个 resultMap 引用来将结果集 ResultMap 映射到参数的类型上。要注意这里的 javaType 属性是可选的,如果留空并且 jdbcType 是 CURSOR,它会被自动地被设为 ResultMap:
#{department, mode=OUT, jdbcType=CURSOR, javaType=ResultSet, resultMap=departmentResultMap}

MyBatis 也支持很多高级的数据类型,比如结构体(structs),但是当使用 out 参数时,你必须显式设置类型的名称:
#{middleInitial, mode=OUT, jdbcType=STRUCT, jdbcTypeName=MY_TYPE, resultMap=departmentResultMap}

2.6 结果映射

<resultMap/> 元素是 MyBatis 中最重要最强大的元素。定义了结果集字段与返回类型的字段的映射关系和返回结果间的关联嵌套关系。

ResultMap 的设计思想是,对简单的语句做到零配置,对于复杂一点的语句,只需要描述语句之间的关系就行了。

<!-- mybatis config xml 定义了类型别名-User  -->
<typeAlias type="com.someapp.model.User" alias="User"/>


<!--结果映射的第1中写法 :使用  resultType-->
<select id="selectUsers" resultType="User">
  select
    user_id             as "id",
    user_name           as "userName",
    hashed_password     as "hashedPassword"
  from some_table
  where id = #{id}
</select>


<!--结果映射的第1中写法 :使用  resultMap-->
<resultMap id="userResultMap" type="User">
  <id property="id" column="user_id" />
  <result property="username" column="user_name"/>
  <result property="password" column="hashed_password"/>
</resultMap>
<select id="selectUsers" resultMap="userResultMap">
  select user_id, user_name, hashed_password
  from some_table
  where id = #{id}
</select>
高级结果映射
<!-- 非常复杂的结果映射 -->
<resultMap id="detailedBlogResultMap" type="Blog">
  <constructor>
    <idArg column="blog_id" javaType="int"/>
  </constructor>
  <result property="title" column="blog_title"/>
  <association property="author" javaType="Author">
    <id property="id" column="author_id"/>
    <result property="username" column="author_username"/>
    <result property="password" column="author_password"/>
    <result property="email" column="author_email"/>
    <result property="bio" column="author_bio"/>
    <result property="favouriteSection" column="author_favourite_section"/>
  </association>
  <collection property="posts" ofType="Post">
    <id property="id" column="post_id"/>
    <result property="subject" column="post_subject"/>
    <association property="author" javaType="Author"/>
    <collection property="comments" ofType="Comment">
      <id property="id" column="comment_id"/>
    </collection>
    <collection property="tags" ofType="Tag" >
      <id property="id" column="tag_id"/>
    </collection>
    <discriminator javaType="int" column="draft">
      <case value="1" resultType="DraftPost"/>
    </discriminator>
  </collection>
</resultMap>

resultMap 元素的概念视图:

constructor - 用于在实例化类时,注入结果到构造方法中
    |____  idArg - ID 参数;标记出作为 ID 的结果可以帮助提高整体性能
    |____  arg - 将被注入到构造方法的一个普通结果
id – 一个 ID 结果;标记出作为 ID 的结果可以帮助提高整体性能
result – 注入到字段或 JavaBean 属性的普通结果
association – 一个复杂类型的关联;许多结果将包装成这种类型(嵌套结果映射 – 关联可以是 resultMap 元素,或是对其它结果映射的引用)
collection – 一个复杂类型的集合(嵌套结果映射 – 集合可以是 resultMap 元素,或是对其它结果映射的引用)
discriminator – 使用结果值来决定使用哪个 resultMap
    |____  case – 基于某些值的结果映射(嵌套结果映射 – case 也是一个结果映射,因此具有相同的结构和元素;或者引用其它的结果映射)

ResultMap 的属性列表:

属性 描述
id 当前命名空间中的一个唯一标识,用于标识一个结果映射。
type 类的完全限定名, 或者一个类型别名(关于内置的类型别名,可以参考上面的表格)。
autoMapping 如果设置这个属性,MyBatis 将会为本结果映射开启或者关闭自动映射。 这个属性会覆盖全局的属性 autoMappingBehavior。默认值:未设置(unset)。
id & result
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>

id 和 result 元素都将一个列的值映射到一个简单数据类型(String, int, double, Date 等)的属性或字段。

id 元素对应的属性会被标记为对象的标识符,在比较对象实例时使用。

Id 和 Result 的属性:

属性 描述
property 映射到列结果的字段或属性。如果 JavaBean 有这个名字的属性(property),会先使用该属性。否则 MyBatis 将会寻找给定名称的字段(field)。无论是哪一种情形,你都可以使用常见的点式分隔形式进行复杂属性导航。 比如,你可以这样映射一些简单的东西:“username”,或者映射到一些复杂的东西上:“address.street.number”。
column 数据库中的列名,或者是列的别名。
javaType 一个 Java 类的全限定名,或一个类型别名
jdbcType 只需要在可能执行插入、更新和删除的且允许空值的列上指定 JDBC 类型。
typeHandler 使用这个属性,你可以覆盖默认的类型处理器。 这个属性值是一个类型处理器实现类的全限定名,或者是类型别名。

支持的 JDBC 类型
image

constructor

构造方法注入允许你在初始化时为类设置属性的值,而不用暴露出公有方法。

public class User {
   //...
   public User(Integer id, String username, int age) {
     //...
  }
//...
}

<!-- 通过构造器参数类型匹配 -->
<constructor>
   <idArg column="id" javaType="int"/>
   <arg column="username" javaType="String"/>
   <arg column="age" javaType="_int"/>
</constructor>

<!-- 还可以通过参数名字匹配 -->
<constructor>
   <idArg column="id" javaType="int" name="id" />
   <arg column="age" javaType="_int" name="age" />
   <arg column="username" javaType="String" name="username" />
</constructor>

剩余的属性和规则和普通的 id 和 result 元素是一样的。

属性 描述
property 映射到列结果的字段或属性。如果 JavaBean 有这个名字的属性(property),会先使用该属性。否则 MyBatis 将会寻找给定名称的字段(field)。无论是哪一种情形,你都可以使用常见的点式分隔形式进行复杂属性导航。 比如,你可以这样映射一些简单的东西:“username”,或者映射到一些复杂的东西上:“address.street.number”。
column 数据库中的列名,或者是列的别名。
javaType 一个 Java 类的全限定名,或一个类型别名
jdbcType 只需要在可能执行插入、更新和删除的且允许空值的列上指定 JDBC 类型。
typeHandler 使用这个属性,你可以覆盖默认的类型处理器。 这个属性值是一个类型处理器实现类的全限定名,或者是类型别名。
select 用于加载复杂类型属性的映射语句的 ID,它会从 column 属性中指定的列检索数据,作为参数传递给此 select 语句。具体请参考关联元素。
resultMap 结果映射的 ID,可以将嵌套的结果集映射到一个合适的对象树中。 它可以作为使用额外 select 语句的替代方案。它可以将多表连接操作的结果映射成一个单一的 ResultSet。
name 构造方法形参的名字。从 3.4.3 版本开始,通过指定具体的参数名,你可以以任意顺序写入 arg 元素。
association(关联)

关联(association)元素处理“有一个”类型的关系。
MyBatis 有两种不同的方式加载关联:

  • 嵌套 Select 查询:通过执行另外一个 SQL 映射语句来加载期望的复杂类型。
  • 嵌套结果映射:使用嵌套的结果映射来处理连接结果的重复子集。

关联(association)元素的属性:

属性 描述
property 映射到列结果的字段或属性。如果 JavaBean 有这个名字的属性(property),会先使用该属性。否则 MyBatis 将会寻找给定名称的字段(field)。无论是哪一种情形,你都可以使用常见的点式分隔形式进行复杂属性导航。 比如,你可以这样映射一些简单的东西:“username”,或者映射到一些复杂的东西上:“address.street.number”。
column 数据库中的列名,或者是列的别名。
javaType 一个 Java 类的全限定名,或一个类型别名
jdbcType 只需要在可能执行插入、更新和删除的且允许空值的列上指定 JDBC 类型。
typeHandler 使用这个属性,你可以覆盖默认的类型处理器。 这个属性值是一个类型处理器实现类的全限定名,或者是类型别名。

关联的嵌套 Select 查询

属性 描述
column 数据库中的列名,或者是列的别名。 注意:在使用复合主键的时候,你可以使用 column="{prop1=col1,prop2=col2}" 这样的语法来指定多个传递给嵌套 Select 查询语句的列名。这会使得 prop1 和 prop2 作为参数对象,被设置为对应嵌套 Select 语句的参数。
select 用于加载复杂类型属性的映射语句的 ID,它会从 column 属性指定的列中检索数据,作为参数传递给目标 select 语句。注意:在使用复合主键的时候,你可以使用 column="{prop1=col1,prop2=col2}" 这样的语法来指定多个传递给嵌套 Select 查询语句的列名。这会使得 prop1 和 prop2 作为参数对象,被设置为对应嵌套 Select 语句的参数
fetchType 可选的。有效值为 lazy 和 eager。 指定属性后,将在映射中忽略全局配置参数 lazyLoadingEnabled,使用属性的值。

使用示例:

<resultMap id="blogResult" type="Blog">
  <association property="author" column="author_id" javaType="Author" select="selectAuthor"/>
</resultMap>

<select id="selectBlog" resultMap="blogResult">
  SELECT * FROM BLOG WHERE ID = #{id}
</select>

<select id="selectAuthor" resultType="Author">
  SELECT * FROM AUTHOR WHERE ID = #{id}
</select>

嵌套查询会产生“N+1”查询问题。

什么是 “N+1 查询问题”?

概括地讲,N+1 查询问题是这样子的:你执行了一个单独的 SQL 语句来获取结果的一个列表(就是“+1”),对返回列表的每条记录,你执行一个 select 查询语句来加载其详细信息(就是“N”)。这个问题会导致成百上千的 SQL 语句被执行。有时候,我们不希望产生这样的后果。

“N+1 查询问题” 解决方案:

  1. 开启延迟加载
<setting name="lazyLoadingEnabled" value="true"/>
  1. 使用嵌套结果映射

关联的嵌套结果映射

属性 描述
resultMap 结果映射的 ID,可以将此关联的嵌套结果集映射到一个合适的对象树中。 它可以作为使用额外 select 语句的替代方案。它可以将多表连接操作的结果映射成一个单一的 ResultSet。这样的 ResultSet 有部分数据是重复的。 为了将结果集正确地映射到嵌套的对象树中, MyBatis 允许你“串联”结果映射,以便解决嵌套结果集的问题。使用嵌套结果映射的一个例子在表格以后。
columnPrefix 当连接多个表时,你可能会不得不使用列别名来避免在 ResultSet 中产生重复的列名。指定 columnPrefix 列名前缀允许你将带有这些前缀的列映射到一个外部的结果映射中。
notNullColumn 默认情况下,在至少一个被映射到属性的列不为空时,子对象才会被创建。 你可以在这个属性上指定非空的列来改变默认行为,指定后,Mybatis 将只在这些列非空时才创建一个子对象。可以使用逗号分隔来指定多个列。默认值:未设置(unset)。
autoMapping 如果设置这个属性,MyBatis 将会为本结果映射开启或者关闭自动映射。 这个属性会覆盖全局的属性 autoMappingBehavior。注意,本属性对外部的结果映射无效,所以不能搭配 select 或 resultMap 元素使用。默认值:未设置(unset)。

<select id="selectBlog" resultMap="blogResult">
  select
    B.id            as blog_id,
    B.title         as blog_title,
    B.author_id     as blog_author_id,
    A.id            as author_id,
    A.username      as author_username,
    A.password      as author_password,
    A.email         as author_email,
    A.bio           as author_bio
  from Blog B left outer join Author A on B.author_id = A.id
  where B.id = #{id}
</select>

<resultMap id="blogResult" type="Blog">
  <id property="id" column="blog_id" />
  <result property="title" column="blog_title"/>
  <!-- 嵌套结果映射 -->
  <association property="author" column="blog_author_id" javaType="Author" resultMap="authorResult"/>
</resultMap>

<resultMap id="authorResult" type="Author">
  <id property="id" column="author_id"/>
  <result property="username" column="author_username"/>
  <result property="password" column="author_password"/>
  <result property="email" column="author_email"/>
  <result property="bio" column="author_bio"/>
</resultMap>

在上面的例子中,你可以看到,博客(Blog)作者(author)的关联元素委托名为 “authorResult” 的结果映射来加载作者对象的实例。
与上面等效的例子:

<resultMap id="blogResult" type="Blog">
  <id property="id" column="blog_id" />
  <result property="title" column="blog_title"/>
  <association property="author" javaType="Author">
    <id property="id" column="author_id"/>
    <result property="username" column="author_username"/>
    <result property="password" column="author_password"/>
    <result property="email" column="author_email"/>
    <result property="bio" column="author_bio"/>
  </association>
</resultMap>

关联的多结果集(ResultSet)

属性 描述
column 当使用多个结果集时,该属性指定结果集中用于与 foreignColumn 匹配的列(多个列名以逗号隔开),以识别关系中的父类型与子类型。
foreignColumn 指定外键对应的列名,指定的列将与父类型中 column 的给出的列进行匹配。
resultSet 指定用于加载复杂类型的结果集名字

从版本 3.2.3 开始,MyBatis 提供了另一种解决 N+1 查询问题的方法。

某些数据库允许存储过程返回多个结果集,或一次性执行多个语句,每个语句返回一个结果集。 我们可以利用这个特性,在不使用连接的情况下,只访问数据库一次就能获得相关数据。

在例子中,存储过程执行下面的查询并返回两个结果集。第一个结果集会返回博客(Blog)的结果,第二个则返回作者(Author)的结果。

SELECT * FROM BLOG WHERE ID = #{id}

SELECT * FROM AUTHOR WHERE ID = #{id}

在映射语句中,必须通过 resultSets 属性为每个结果集指定一个名字,多个名字使用逗号隔开。

<select id="selectBlog" resultSets="blogs,authors" resultMap="blogResult" statementType="CALLABLE">
  {call getBlogsAndAuthors(#{id,jdbcType=INTEGER,mode=IN})}
</select>

<!-- 可以指定使用 “authors” 结果集的数据来填充 “author” 关联: --> 
<resultMap id="blogResult" type="Blog">
  <id property="id" column="id" />
  <result property="title" column="title"/>
  <association property="author" javaType="Author" resultSet="authors" column="author_id" foreignColumn="id">
    <id property="id" column="id"/>
    <result property="username" column="username"/>
    <result property="password" column="password"/>
    <result property="email" column="email"/>
    <result property="bio" column="bio"/>
  </association>
</resultMap>
collection(集合)
<!-- ofType -->
<collection property="posts" ofType="domain.blog.Post">
  <id property="id" column="post_id"/>
  <result property="subject" column="post_subject"/>
  <result property="body" column="post_body"/>
</collection>

集合元素和关联元素几乎是一样的。

集合的嵌套 Select 查询

<resultMap id="blogResult" type="Blog">
  <!-- ofType 指明了集合里的存储的元素类型 -->
  <!-- posts 是一个存储 Post 类型对象的 ArrayList 集合-->
  <collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>
</resultMap>

<select id="selectBlog" resultMap="blogResult">
  SELECT * FROM BLOG WHERE ID = #{id}
</select>

<select id="selectPostsForBlog" resultType="Post">
  SELECT * FROM POST WHERE BLOG_ID = #{id}
</select>

集合的嵌套结果映射

<select id="selectBlog" resultMap="blogResult">
  select
  B.id as blog_id,
  B.title as blog_title,
  B.author_id as blog_author_id,
  P.id as post_id,
  P.subject as post_subject,
  P.body as post_body,
  from Blog B
  left outer join Post P on B.id = P.blog_id
  where B.id = #{id}
</select>

<resultMap id="blogResult" type="Blog">
  <id property="id" column="blog_id" />
  <result property="title" column="blog_title"/>
  <collection property="posts" ofType="Post">
    <id property="id" column="post_id"/>
    <result property="subject" column="post_subject"/>
    <result property="body" column="post_body"/>
  </collection>
</resultMap>

下面的等价形式:

<resultMap id="blogResult" type="Blog">
  <id property="id" column="blog_id" />
  <result property="title" column="blog_title"/>
  <collection property="posts" ofType="Post" resultMap="blogPostResult" columnPrefix="post_"/>
</resultMap>

<resultMap id="blogPostResult" type="Post">
  <id property="id" column="id"/>
  <result property="subject" column="subject"/>
  <result property="body" column="body"/>
</resultMap>

集合的多结果集(ResultSet)
像关联元素那样,我们可以通过执行存储过程实现,它会执行两个查询并返回两个结果集,一个是博客的结果集,另一个是文章的结果集:

SELECT * FROM BLOG WHERE ID = #{id}

SELECT * FROM POST WHERE BLOG_ID = #{id}

在映射语句中,必须通过 resultSets 属性为每个结果集指定一个名字,多个名字使用逗号隔开。

<select id="selectBlog" resultSets="blogs,posts" resultMap="blogResult">
  {call getBlogsAndPosts(#{id,jdbcType=INTEGER,mode=IN})}
</select>

<resultMap id="blogResult" type="Blog">
  <id property="id" column="id" />
  <result property="title" column="title"/>
  <collection property="posts" ofType="Post" resultSet="posts" column="id" foreignColumn="blog_id">
    <id property="id" column="id"/>
    <result property="subject" column="subject"/>
    <result property="body" column="body"/>
  </collection>
</resultMap>

discriminator(鉴别器)
<discriminator javaType="int" column="draft">
  <case value="1" resultType="DraftPost"/>
</discriminator>

有时候,一个数据库查询可能会返回多个不同的结果集(但总体上还是有一定的联系的)。 鉴别器(discriminator)元素就是被设计来应对这种情况的,另外也能处理其它情况,例如类的继承层次结构。 鉴别器的概念很好理解——它很像 Java 语言中的 switch 语句。

一个鉴别器的定义需要指定 column 和 javaType 属性。column 指定了 MyBatis 查询被比较值的地方。 而 javaType 用来确保使用正确的相等测试(虽然很多情况下字符串的相等测试都可以工作)。

<resultMap id="vehicleResult" type="Vehicle">
  <id property="id" column="id" />
  <result property="vin" column="vin"/>
  <result property="year" column="year"/>
  <result property="make" column="make"/>
  <result property="model" column="model"/>
  <result property="color" column="color"/>
  <discriminator javaType="int" column="vehicle_type">
    <case value="1" resultMap="carResult"/>
    <case value="2" resultMap="truckResult"/>
    <case value="3" resultMap="vanResult"/>
    <case value="4" resultMap="suvResult"/>
  </discriminator>
</resultMap>

<!-- vehicleResult 的属性也会加载 -->
<resultMap id="carResult" type="Car" extends="vehicleResult">
  <result property="doorCount" column="door_count" />
</resultMap>

在这个示例中,MyBatis 会从结果集中得到每条记录,然后比较它的 vehicle type 值。 如果它匹配任意一个鉴别器的 case,就会使用这个 case 指定的结果映射。 这个过程是互斥的,也就是说,剩余的结果映射将被忽略(除非它是扩展的,我们将在稍后讨论它)。 如果不能匹配任何一个 case,MyBatis 就只会使用鉴别器块外定义的结果映射。

2.7 自动映射

在简单的场景下,MyBatis 可以为你自动映射查询结果。但如果遇到复杂的场景,你需要构建一个结果映射。

当自动映射查询结果时,MyBatis 会获取结果中返回的列名并在 Java 类中查找相同名字的属性(忽略大小写)。 这意味着如果发现了 ID 列和 id 属性,MyBatis 会将列 ID 的值赋给 id 属性。

通常数据库列使用大写字母组成的单词命名,单词间用下划线分隔;而 Java 属性一般遵循驼峰命名法约定。
为了在这两种命名方式之间启用自动映射,需要将 mapUnderscoreToCamelCase 设置为 true

自动映射等级可用一下配置指定:

<setting name="autoMappingBehavior" value="PARTIAL"/>

有3种自动映射等级:

  1. NONE - 禁用自动映射。仅对手动映射的属性进行映射。
  2. PARTIAL - 对除在内部定义了嵌套结果映射(也就是连接的属性)以外的属性进行映射
  3. FULL - 自动映射所有属性。
    默认值是 PARTIAL 。

可以通过在结果映射上设置 autoMapping 属性来为指定的结果映射设置启用/禁用自动映射。

<resultMap id="userResultMap" type="User" autoMapping="false">
  <result property="password" column="hashed_password"/>
</resultMap>

2.8 缓存

MyBatis 内置了一个强大的事务性查询缓存机制,它可以非常方便地配置和定制。
默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。
要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:<cache/>
<cache/>的效果如下:

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

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

这些效果可以通过以下属性改变:

<!-- 这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。
-->
<cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="true"/>
属性 描述
eviction 清除策略,默认 LRU.LRU – 最近最少使用:移除最长时间不被使用的对象。
FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
flushInterval 缓存刷新间隔, 单位:毫秒. 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。
size 引用数目,默认值 1024,缓存队列的大小吧
readOnly 只读,默认值 false。只读的缓存会给所有调用者返回缓存对象的相同实例。

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

默认情况:

<select ... flushCache="false" useCache="true"/>
<insert ... flushCache="true"/>
<update ... flushCache="true"/>
<delete ... flushCache="true"/>

请注意,缓存的配置和缓存实例会被绑定到 SQL 映射文件的命名空间中。 因此,同一命名空间中的所有语句和缓存将通过命名空间绑定在一起。

使用 cache-ref 元素来引用另一个缓存

<cache-ref namespace="com.someone.application.data.SomeMapper"/>
自定义缓存
  1. 实现 org.apache.ibatis.cache.Cache 接口编写自定义缓存类
  2. xml 配置 MyCustomCache
<cache type="com.domain.something.MyCustomCache"/>

从版本 3.4.2 开始,MyBatis 已经支持在所有属性设置完毕之后,调用一个初始化方法。如果想要使用这个特性,请在你的自定义缓存类里实现 org.apache.ibatis.builder.InitializingObject 接口。

public interface InitializingObject {
  void initialize() throws Exception;
}

Mybatis 对缓存的配置(如清除策略、可读或可读写等),不能应用于自定义缓存。

3. 动态 SQL

动态 SQL 是 MyBatis 的强大特性之一。实现 sql 语句的动态拼接。

3.1 if

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

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

3.2 choose、when、otherwise

传入了 “title” 就按 “title” 查找,传入了 “author” 就按 “author” 查找的情形,否则按 “featured = 1” 条件查找:

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

3.3 trim、where、set

<!-- prefixOverrides 属性会忽略通过管道符分隔的文本序列(注意此例中的空格是必要的) -->
<trim prefix="WHERE" prefixOverrides="AND |OR ">
  ...
</trim>

where 元素只会在子元素返回任何内容的情况下才插入 “WHERE” 子句。而且,若子句的开头为 “AND” 或 “OR”,where 元素也会将它们去除。

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

set

<!-- set 元素会动态地在行首插入 SET 关键字,并会删掉额外的逗号(这些逗号是在使用条件语句给列赋值时引入的)。 -->
<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>

3.4 foreach

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

<select id="selectPostIn" resultType="domain.blog.Post">
  SELECT *
  FROM POST P
  <where>
    <foreach item="itemName" index="index" collection="list"
        open="ID in (" separator="," close=")" nullable="true">
          #{itemName}
    </foreach>
  </where>
</select>

foreach 元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和索引(index)变量。它也允许你指定开头与结尾的字符串以及集合项迭代之间的分隔符。

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

3.5 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);

3.6 bind

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

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

多数据库支持

如果配置了 databaseIdProvider,你就可以在动态代码中使用名为 “_databaseId” 的变量来为不同的数据库构建特定的语句。比如下面的例子:

<insert id="insert">
  <selectKey keyProperty="id" resultType="int" order="BEFORE">
    <if test="_databaseId == 'oracle'">
      select seq_users.nextval from dual
    </if>
    <if test="_databaseId == 'db2'">
      select nextval for seq_users from sysibm.sysdummy1"
    </if>
  </selectKey>
  insert into users values (#{id}, #{name})
</insert>

动态 SQL 中的插入脚本语言

MyBatis 从 3.2 版本开始支持插入脚本语言,这允许你插入一种语言驱动,并基于这种语言来编写动态 SQL 查询语句。

通过实现 org.apache.ibatis.scripting.LanguageDriver 接口插入一种语言:

public interface LanguageDriver {
  ParameterHandler createParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql);
  SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType);
  SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType);
}

实现自定义语言驱动后,你就可以在 mybatis-config.xml 文件中将它设置为默认语言:

<typeAliases>
  <typeAlias type="org.sample.MyLanguageDriver" alias="myLanguage"/>
</typeAliases>
<settings>
  <setting name="defaultScriptingLanguage" value="myLanguage"/>
</settings>

可以使用 lang 属性为特定的语句指定语言:

<select id="selectBlog" lang="myLanguage">
  SELECT * FROM BLOG
</select>

在你的 mapper 接口上添加 @Lang 注解:

public interface Mapper {
  @Lang(MyLanguageDriver.class)
  @Select("SELECT * FROM BLOG")
  List<Blog> selectBlog();
}

4. SqlSessionFactory

SqlSession 接口可以执行sql,获取映射器实例和管理事务,它是由 SqlSessionFactory 实例创建的。
只有每次进行sql 操作的时候都创建 1 个 SqlSession 这样才能保证线程安全。

SqlSessionFactory 对象包含创建 SqlSession 实例的各种方法。
SqlSessionFactory 对象是由 SqlSessionFactoryBuilder 构建的。

SqlSessionFactoryBuilder
SqlSessionFactoryBuilder 有五个 build() 方法,每一种都允许你从不同的资源中创建一个 SqlSessionFactory 实例:

SqlSessionFactory build(InputStream inputStream)
SqlSessionFactory build(InputStream inputStream, String environment)
SqlSessionFactory build(InputStream inputStream, Properties properties)
SqlSessionFactory build(InputStream inputStream, String env, Properties props)
SqlSessionFactory build(Configuration config)

上面 inputStream 就是 mybatis-config.xml 的文件流;
environment 决定加载 mybatis-config.xml中 哪个 <environment>,包括数据源和事务管理器,比如:

<environments default="development">
  <environment id="development">
    <transactionManager type="JDBC">
        ...
    <dataSource type="POOLED">
        ...
  </environment>
  <environment id="production">
    <transactionManager type="MANAGED">
        ...
    <dataSource type="JNDI">
        ...
  </environment>
</environments>

properties 传递的属性的优先级最高,resource 或 url 指定的属性优先级中等,在配置文件中 <properties> 元素体中指定的属性优先级最低。

从 mybatis-config.xml 文件创建 SqlSessionFactory 的示例:

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

Resources 类会帮助你从类路径下、文件系统或一个 web URL 中加载资源文件。

SqlSessionFactory build(Configuration config): 其中的 Configuration 实例对应就是 mybatis-config.xml 解析后的配置信息,允许在构建 SqlSessionFactory 手动创建 Configuration 实例。

SqlSessionFactory
SqlSessionFactory 有7个方法创建 SqlSession 实例。

SqlSession openSession()
// autoCommit=true,可开启自动提交功能 
SqlSession openSession(boolean autoCommit)
// 使用自定义的数据库连接实例
SqlSession openSession(Connection connection)
// 事务隔离级别支持 JDBC 的五个隔离级别(NONE、READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ 和 SERIALIZABLE)
SqlSession openSession(TransactionIsolationLevel level)
// execType 有三个值:
// ExecutorType.SIMPLE:该类型的执行器没有特别的行为。它为每个语句的执行创建一个新的预处理语句。
// ExecutorType.REUSE:该类型的执行器会复用预处理语句。
// ExecutorType.BATCH:该类型的执行器会批量执行所有更新语句,如果 SELECT 在多个更新中间执行,将在必要时将多条更新语句分隔开来,以方便理解。
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level)
SqlSession openSession(ExecutorType execType)
SqlSession openSession(ExecutorType execType, boolean autoCommit)
SqlSession openSession(ExecutorType execType, Connection connection)

默认的 openSession() 方法没有参数,它会创建具备如下特性的 SqlSession:

  • 事务作用域将会开启(也就是不自动提交)。
  • 将由当前环境配置的 DataSource 实例中获取 Connection 对象。
  • 事务隔离级别将会使用驱动或数据源的默认设置。
  • 预处理语句不会被复用,也不会批量处理更新。

如果你使用过 MyBatis 的旧版本,可能还记得 session、事务和批量操作是相互独立的。在新版本中则不是这样。上述三者都包含在 session 作用域内。你不必分别处理事务或批量操作就能得到想要的全部效果。

5. SqlSession

SqlSession 包含了所有执行语句、提交或回滚事务以及获取映射器实例的方法。

  1. 语句执行方法
// 返回一个值或 null,返回多个会抛出异常
<T> T selectOne(String statement, Object parameter)
// 如果不知道返回对象会有多少,请使用 selectList
<E> List<E> selectList(String statement, Object parameter)
// 游标(Cursor)与列表(List)返回的结果相同,不同的是,游标借助迭代器实现了数据的惰性加载。
<T> Cursor<T> selectCursor(String statement, Object parameter)
// selectMap 会将返回对象的其中一个属性作为 key 值,将对象作为 value 值,从而将多个结果集转为 Map 类型值。
<K,V> Map<K,V> selectMap(String statement, Object parameter, String mapKey)
// insert、update 以及 delete 方法返回的值表示受该语句影响的行数。
int insert(String statement, Object parameter)
int update(String statement, Object parameter)
int delete(String statement, Object parameter)

select 方法的三个高级版本,它们允许你限制返回行数的范围,或是提供自定义结果处理逻辑,通常在数据集非常庞大的情形下使用:

<E> List<E> selectList (String statement, Object parameter, RowBounds rowBounds)
<T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds)
<K,V> Map<K,V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowbounds)
void select (String statement, Object parameter, ResultHandler<T> handler)
void select (String statement, Object parameter, RowBounds rowBounds, ResultHandler<T> handler)

RowBounds
RowBounds 参数会告诉 MyBatis 略过指定数量的记录,并限制返回结果的数量。RowBounds 类的 offset 和 limit 值只有在构造函数时才能传入,其它时候是不能修改的。

数据库驱动决定了略过记录时的查询效率。为了获得最佳的性能,建议将 ResultSet 类型设置为 SCROLL_SENSITIVE 或 SCROLL_INSENSITIVE(换句话说:不要使用 FORWARD_ONLY)。

注意: RowBounds 分页查询只是对查询返回的 ResultSet 结果集进行分页,也就是人们常说的逻辑分页,而非物理分页。需要使用分页插件进行分页。

ResultHandler
从版本 3.4.6 开始,ResultHandler 会在存储过程的 REFCURSOR 输出参数中传递使用的 CALLABLE 语句。
使用 ResultHandler 的时候需要注意以下两个限制:

  • 使用带 ResultHandler 参数的方法时,收到的数据不会被缓存。
  • 当使用高级的结果映射集(resultMap)时,MyBatis 很可能需要数行结果来构造一个对象。如果你使用了 ResultHandler,你可能会接收到关联(association)或者集合(collection)中尚未被完整填充的对象。
  1. 立即批量更新方法
    当你将 ExecutorType 设置为 ExecutorType.BATCH 时,可以使用这个方法清除(执行)缓存在 JDBC 驱动类中的批量更新语句。
List<BatchResult> flushStatements()
  1. 事务控制方法
    有 4 个方法用来控制事务作用域。当然,如果你已经设置了自动提交或你使用了外部事务管理器,这些方法就没什么作用了。然而,如果你正在使用由 Connection 实例控制的 JDBC 事务管理器,那么这四个方法就会派上用场:
void commit()
void commit(boolean force)
void rollback()
void rollback(boolean force)

默认情况下 MyBatis 不会自动提交事务,除非它侦测到调用了插入、更新或删除方法改变了数据库。

注意,在自动提交模式或者使用了外部事务管理器的情况下,设置 force 值对 session 无效

  1. 本地缓存
    Mybatis 使用到了两种缓存:本地缓存(local cache)和二级缓存(second level cache)。

每当一个新 session 被创建,MyBatis 就会创建一个与之相关联的本地缓存。任何在 session 执行过的查询结果都会被保存在本地缓存中,所以,当再次执行参数相同的相同查询时,就不需要实际查询数据库了。本地缓存将会在做出修改、事务提交或回滚,以及关闭 session 时清空。

默认情况下,本地缓存数据的生命周期等同于整个 session 的周期。由于缓存会被用来解决循环引用问题和加快重复嵌套查询的速度,所以无法将其完全禁用。但是你可以通过设置 localCacheScope=STATEMENT 来只在语句执行时使用缓存。

注意,如果 localCacheScope 被设置为 SESSION,对于某个对象,MyBatis 将返回在本地缓存中唯一对象的引用。对返回的对象(例如 list)做出的任何修改将会影响本地缓存的内容,进而将会影响到在本次 session 中从缓存返回的值。因此,不要对 MyBatis 所返回的对象作出更改,以防后患。

你可以随时调用以下方法来清空本地缓存:

void clearCache()
  1. 确保 SqlSession 被关闭
    手动调用 SqlSession#close() 方法
void close()

或者

SqlSession session = sqlSessionFactory.openSession();
try (SqlSession session = sqlSessionFactory.openSession()) {
    // 假设下面三行代码是你的业务逻辑
    session.insert(...);
    session.update(...);
    session.delete(...);
    session.commit();
}
  1. 使用映射器
<T> T getMapper(Class<T> type)

通过 SqlSession#getMapper()方法返回一个映射器实例。

映射器接口不需要去实现任何接口或继承自任何类。只要方法签名可以被用来唯一识别对应的映射语句就可以了。

映射器接口可以继承自其他接口。在使用 XML 来绑定映射器接口时,保证语句处于合适的命名空间中即可。唯一的限制是,不能在两个具有继承关系的接口中拥有相同的方法签名(这是潜在的危险做法,不可取)。

映射器接口可以继承,那么就是说我们可以提供一个父类接口提供基本的增删改查方法,其余映射器接口可以继承此接口进行扩展。

映射器接口的方法传入参数有多个时,可以在参数上使用 @Param("paramName") 注解进行区分,mybatis将会根据 @Param 注解进行sql参数匹配。

映射器注解
MyBatis 3 构建在全面且强大的基于 Java 语言的配置 API 之上。它是 XML 和注解配置的基础。注解提供了一种简单且低成本的方式来实现简单的映射语句。

如果仅仅是简单查询,那么使用注解就能够满足需求,否则最好还是使用 Mapper.xml 编写 sql 语句。

个人喜欢使用的注解只有 @Param,它可以对方法中相同类型的参数进行区分。

注解 使用对象 XML 等价形式 描述
@CacheNamespace <cache> 为给定的命名空间(比如类)配置缓存。属性:implemetation、eviction、flushInterval、size、readWrite、blocking、properties。
@Property <property> 指定参数值或占位符(placeholder)(该占位符能被 mybatis-config.xml 内的配置属性替换)。属性:name、value。(仅在 MyBatis 3.4.2 以上可用)
@CacheNamespaceRef <cacheRef> 引用另外一个命名空间的缓存以供使用。注意,即使共享相同的全限定类名,在 XML 映射文件中声明的缓存仍被识别为一个独立的命名空间。属性:value、name。如果你使用了这个注解,你应设置 value 或者 name 属性的其中一个。value 属性用于指定能够表示该命名空间的 Java 类型(命名空间名就是该 Java 类型的全限定类名),name 属性(这个属性仅在 MyBatis 3.4.2 以上可用)则直接指定了命名空间的名字。
@ConstructorArgs 方法 <constructor> 收集一组结果以传递给一个结果对象的构造方法。属性:value,它是一个 Arg 数组。
@Arg <arg>``<idArg> ConstructorArgs 集合的一部分,代表一个构造方法参数。属性:id、column、javaType、jdbcType、typeHandler、select、resultMap.id 属性和 XML 元素 <idArg> 相似,它是一个布尔值,表示该属性是否用于唯一标识和比较对象。从版本 3.5.4 开始,该注解变为可重复注解。
@TypeDiscriminator 方法 <discriminator> 决定使用何种结果映射的一组取值(case)。属性:column、javaType、jdbcType、typeHandler、cases。cases 属性是一个 Case 的数组。
@Case <case> 表示某个值的一个取值以及该取值对应的映射。属性:value、type、results。results 属性是一个 Results 的数组,因此这个注解实际上和 ResultMap 很相似,由下面的 Results 注解指定。
@Results 方法 <resultMap> 一组结果映射,指定了对某个特定结果列,映射到某个属性或字段的方式。属性:value、id。value 属性是一个 Result 注解的数组。而id 属性则是结果映射的名称。从版本 3.5.4 开始,该注解变为可重复注解。
@Result <result> <id> 在列和属性或字段之间的单个结果映射。属性:id、column、javaType、jdbcType、typeHandler、one、many。id 属性和 XML 元素<id> 相似,它是一个布尔值,表示该属性是否用于唯一标识和比较对象。one 属性是一个关联,和 <association> 类似,而 many 属性则是集合关联,和 <collection> 类似。这样命名是为了避免产生名称冲突。
@One <association> 复杂类型的单个属性映射。属性: select,指定可加载合适类型实例的映射语句(也就是映射器方法)全限定名; fetchType,指定在该映射中覆盖全局配置参数 lazyLoadingEnabled; resultMap(available since 3.5.5), which is the fully qualified name of a result map that map to a single container object from select result; columnPrefix(available since 3.5.5), which is column prefix for grouping select columns at nested result map. 提示 注解 API 不支持联合映射。这是由于 Java 注解不允许产生循环引用。
@Many <collection> 复杂类型的集合属性映射。属性: select,指定可加载合适类型实例集合的映射语句(也就是映射器方法)全限定名; fetchType,指定在该映射中覆盖全局配置参数 lazyLoadingEnabled resultMap(available since 3.5.5), which is the fully qualified name of a result map that map to collection object from select result; columnPrefix(available since 3.5.5), which is column prefix for grouping select columns at nested result map. 提示 注解 API 不支持联合映射。这是由于 Java 注解不允许产生循环引用。
@MapKey 方法 供返回值为 Map 的方法使用的注解。它使用对象的某个属性作为 key,将对象 List 转化为 Map。属性:value,指定作为 Map 的 key 值的对象属性名。
@Options 方法 映射语句的属性 该注解允许你指定大部分开关和配置选项,它们通常在映射语句上作为属性出现。与在注解上提供大量的属性相比,Options 注解提供了一致、清晰的方式来指定选项。属性:useCache=true、flushCache=FlushCachePolicy.DEFAULT、resultSetType=DEFAULT、statementType=PREPARED、fetchSize=-1、timeout=-1、useGeneratedKeys=false、keyProperty=""、keyColumn=""、resultSets="", databaseId=""。注意,Java 注解无法指定 null 值。因此,一旦你使用了 Options 注解,你的语句就会被上述属性的默认值所影响。要注意避免默认值带来的非预期行为。 The databaseId(Available since 3.5.5), in case there is a configured DatabaseIdProvider, the MyBatis use the Options with no databaseId attribute or with a databaseId that matches the current one. If found with and without the databaseId the latter will be discarded.
注意:keyColumn 属性只在某些数据库中有效(如 Oracle、PostgreSQL 等)。要了解更多关于 keyColumn 和 keyProperty 可选值信息,请查看“insert, update 和 delete”一节。
@Insert
@Update
@Delete
@Select
方法 <insert><update><delete><select> 每个注解分别代表将会被执行的 SQL 语句。它们用字符串数组(或单个字符串)作为参数。如果传递的是字符串数组,字符串数组会被连接成单个完整的字符串,每个字符串之间加入一个空格。这有效地避免了用 Java 代码构建 SQL 语句时产生的“丢失空格”问题。当然,你也可以提前手动连接好字符串。属性:value,指定用来组成单个 SQL 语句的字符串数组。 The databaseId(Available since 3.5.5), in case there is a configured DatabaseIdProvider, the MyBatis use a statement with no databaseId attribute or with a databaseId that matches the current one. If found with and without the databaseId the latter will be discarded.
@InsertProvider
@UpdateProvider
@DeleteProvider
@SelectProvider
方法 <insert><update><delete><select> 允许构建动态 SQL。这些备选的 SQL 注解允许你指定返回 SQL 语句的类和方法,以供运行时执行。(从 MyBatis 3.4.6 开始,可以使用 CharSequence 代替 String 来作为返回类型)。当执行映射语句时,MyBatis 会实例化注解指定的类,并调用注解指定的方法。你可以通过 ProviderContext 传递映射方法接收到的参数、"Mapper interface type" 和 "Mapper method"(仅在 MyBatis 3.4.5 以上支持)作为参数。(MyBatis 3.4 以上支持传入多个参数) 属性:value、type、method、databaseId。 value and type 属性用于指定类名 (The type attribute is alias for value, you must be specify either one. But both attributes can be omit when specify the defaultSqlProviderType as global configuration)。 method 用于指定该类的方法名(从版本 3.5.1 开始,可以省略 method 属性,MyBatis 将会使用 ProviderMethodResolver 接口解析方法的具体实现。如果解析失败,MyBatis 将会使用名为 provideSql 的降级实现)。提示 接下来的“SQL 语句构建器”一章将会讨论该话题,以帮助你以更清晰、更便于阅读的方式构建动态 SQL。 The databaseId(Available since 3.5.5), in case there is a configured DatabaseIdProvider, the MyBatis will use a provider method with no databaseId attribute or with a databaseId that matches the current one. If found with and without the databaseId the latter will be discarded.
@Param 参数 如果你的映射方法接受多个参数,就可以使用这个注解自定义每个参数的名字。否则在默认情况下,除 RowBounds 以外的参数会以 "param" 加参数位置被命名。例如 #{param1}, #{param2}。如果使用了 @Param("person"),参数就会被命名为 #{person}。
@SelectKey 方法 <selectKey> 这个注解的功能与 <selectKey> 标签完全一致。该注解只能在 @Insert 或 @InsertProvider 或 @Update 或 @UpdateProvider 标注的方法上使用,否则将会被忽略。如果标注了 @SelectKey 注解,MyBatis 将会忽略掉由 @Options 注解所设置的生成主键或设置(configuration)属性。属性:statement 以字符串数组形式指定将会被执行的 SQL 语句,keyProperty 指定作为参数传入的对象对应属性的名称,该属性将会更新成新的值,before 可以指定为 true 或 false 以指明 SQL 语句应被在插入语句的之前还是之后执行。resultType 则指定 keyProperty 的 Java 类型。statementType 则用于选择语句类型,可以选择 STATEMENT、PREPARED 或 CALLABLE 之一,它们分别对应于 Statement、PreparedStatement 和 CallableStatement。默认值是 PREPARED。 The databaseId(Available since 3.5.5), in case there is a configured DatabaseIdProvider, the MyBatis will use a statement with no databaseId attribute or with a databaseId that matches the current one. If found with and without the databaseId the latter will be discarded.
@ResultMap 方法 这个注解为 @Select 或者 @SelectProvider 注解指定 XML 映射中 <resultMap> 元素的 id。这使得注解的 select 可以复用已在 XML 中定义的 ResultMap。如果标注的 select 注解中存在 @Results 或者 @ConstructorArgs 注解,这两个注解将被此注解覆盖。
@ResultType 方法 在使用了结果处理器的情况下,需要使用此注解。由于此时的返回类型为 void,所以 Mybatis 需要有一种方法来判断每一行返回的对象类型。如果在 XML 有对应的结果映射,请使用 @ResultMap 注解。如果结果类型在 XML 的 <select> 元素中指定了,就不需要使用其它注解了。否则就需要使用此注解。比如,如果一个标注了 @Select 的方法想要使用结果处理器,那么它的返回类型必须是 void,并且必须使用这个注解(或者 @ResultMap)。这个注解仅在方法返回类型是 void 的情况下生效。
@Flush 方法 如果使用了这个注解,定义在 Mapper 接口中的方法就能够调用 SqlSession#flushStatements() 方法。(Mybatis 3.3 以上可用)

注解使用例子请看官方 https://mybatis.org/mybatis-3/java-api.html#sqlSessions

从源码看核心API

1 SqlSessionFactoryBuilder - org.apache.ibatis.session.SqlSessionFactoryBuilder

使用 mybatis 流程上来说,第 1 步就是创建 sql session 工厂构造器 SqlSessionFactoryBuilder 构造 sql session 工厂对象实例。

SqlSessionFactoryBuilder 构造方法传参

  • mybatis 配置输入流
  • mybatis 使用的 environment 名字
  • mybatis 属性

构造方法如下:

public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
      // 1. 创建 XMLConfigBuilder 实例,用于解析 mybtais config xml,并生成 Configuration 对象。
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      // 2. 解析 mybatis config xml 文件生成 Configuration 对象;
      //    使用  Configuration 对象构建 SqlSessionFactory
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();
      try {
        if (reader != null) {
          reader.close();
        }
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }

2 SqlSessionFactory

SqlSessionFactory 主要用于创建 SqlSession 实例,通过重载 openSession() 方法影响 SqlSession 的行为;默认实现:org.apache.ibatis.session.defaults.DefaultSqlSessionFactory。

openSession 方法传参有:

  • 是否需要自动提交
  • JDBC 数据库连接(java.sql.Connection)
  • Mybatis SQL语句执行器类型(org.apache.ibatis.session.ExecutorType)
  • Mybatis 事务隔离级别(org.apache.ibatis.session.TransactionIsolationLevel)

DefaultSqlSessionFactory#openSession 源码

  public SqlSession openSession() {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
  }

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level,
      boolean autoCommit) {
    Transaction tx = null;
    try {
      // 1. 当前 configuration 对象中获取当前使用的环境信息
      final Environment environment = configuration.getEnvironment();
      // 2. 从 environment 实例中的 TransactionFactory
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      // 3. TransactionFactory 使用 数据源、事务隔离级别、是否自动提交创建事务对象
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      // 4. configuration实例使用 事务对象、ExecutorType 创建 Executor 对象
      final Executor executor = configuration.newExecutor(tx, execType);
	  // 5. 使用 configuration、executor、是否自动提交构建 DefaultSqlSession
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

从以上源码可以看出 openSession 方法主要做了几件事情:

  • 创建 Transaction 对象
  • 创建 Executor 对象
  • 使用 Transaction、Executor、autoCommit 创建 DefaultSqlSession 对象

3 SqlSession

MyBatis SQL 会话对象,类似于JDBC Connection.默认实现:org.apache.ibatis.session.defaults.DefaultSqlSession

SqlSession 职责

  1. 执行 sql ;
  2. 管理事务
  3. 获取 Mapper 接口实例

DefaultSqlSession#selectList 方法

private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
    try {
      // 1. 从 configuration 获取 MappedStatement
      MappedStatement ms = configuration.getMappedStatement(statement);
      dirty |= ms.isDirtySelect();
	  // 2. 调用 Executor 的 query 执行查询
      return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

从上面方法可以看出,真正的查询方法还是交给 Executor 去执行。

4 Executor

MyBatis 实际 Statement 执行者。

从之前的【DefaultSqlSessionFactory#openSession 源码】中可以看到 Executor 是由 configuration.newExecutor(tx, execType);创建的;
configuration.newExecutor(tx, execType);源码如下:

 public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    return (Executor) interceptorChain.pluginAll(executor);
  }

从上面代码可知,SimpleExecutor 是默认实现;如果开启缓存,会使用 CachingExecutor 包装后返回;最后将配置 mybatis 插件拦截 Executor ,返回 Executor 代理对象。

Executor 抽象实现 - org.apache.ibatis.executor.BaseExecutor
BaseExecutor 的 query 源码:


@Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
      CacheKey key, BoundSql boundSql) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        // 本地缓存有值,则返回
         handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
	    // 本地缓存没有,则从数据库查询
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }
  
  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds,
      ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
	  // doQuery 是模板方法,子类提供实现
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

org.apache.ibatis.executor.SimpleExecutor的 doQuery方法源码:

@Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
      BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      // 1. 创建 StatementHandler,具体实现是 org.apache.ibatis.executor.statement.RoutingStatementHandler(默认:PreparedStatementHandler)
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler,
          boundSql);
      // 2. 生成预编译语句
      stmt = prepareStatement(handler, ms.getStatementLog());
      // 3. 执行 语句
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }
  
  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    // 从 transaction 获取数据库链接
    Connection connection = getConnection(statementLog);
    // 预编译语句
    stmt = handler.prepare(connection, transaction.getTimeout());
	// 
    handler.parameterize(stmt);
    return stmt;
  }

从以上代码可以看出,Excetor 执行的查询方法是交给 StatementHandler 去执行。

5 StatementHandler

MyBatis JDBC Statement 处理器,可以被 MyBatis Interceptor 拦截。

StatementHandler 是由 Configuration#newStatementHandler 方法创建的:

  public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement,
      Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject,
        rowBounds, resultHandler, boundSql);
    return (StatementHandler) interceptorChain.pluginAll(statementHandler);
  }

上面可以看出,StatementHandler 默认实现是 RoutingStatementHandler,然后返回代理后的 statementHandler

RoutingStatementHandler 使用 org.apache.ibatis.mapping.StatementType构造不同类型的 StatementHandler :

public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

    switch (ms.getStatementType()) {
      case STATEMENT:
        delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case PREPARED:
        delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case CALLABLE:
        delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      default:
        throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
    }
  }

默认使用的 statementType 是 PREPARED 。

StatementHandler 抽象实现 org.apache.ibatis.executor.statement.BaseStatementHandler

protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject,
      RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    this.configuration = mappedStatement.getConfiguration();
    this.executor = executor;
    this.mappedStatement = mappedStatement;
    this.rowBounds = rowBounds;

    this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
    this.objectFactory = configuration.getObjectFactory();

    if (boundSql == null) { // issue #435, get the key before calculating the statement
      generateKeys(parameterObject);
      // 
      boundSql = mappedStatement.getBoundSql(parameterObject);
    }

    this.boundSql = boundSql;
    // 创建 parameterHandler
    this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
    //  创建 resultSetHandler
    this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler,
        resultHandler, boundSql);
  }

BaseStatementHandler#prepare 方法:

public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
    ErrorContext.instance().sql(boundSql.getSql());
    Statement statement = null;
    try {
      statement = instantiateStatement(connection);
      setStatementTimeout(statement, transactionTimeout);
      setFetchSize(statement);
      return statement;
    } catch (SQLException e) {
      closeStatement(statement);
      throw e;
    } catch (Exception e) {
      closeStatement(statement);
      throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
    }
  }

PreparedStatementHandler#instantiateStatement 方法返回 java.sql.PreparedStatement的实例 :

  @Override
  protected Statement instantiateStatement(Connection connection) throws SQLException {
    String sql = boundSql.getSql();
    if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
      String[] keyColumnNames = mappedStatement.getKeyColumns();
      if (keyColumnNames == null) {
        return connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
      } else {
        return connection.prepareStatement(sql, keyColumnNames);
      }
    }
    if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
      return connection.prepareStatement(sql);
    } else {
      return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(),
          ResultSet.CONCUR_READ_ONLY);
    }
  }

PreparedStatementHandler#parameterize 方法 填充 PreparedStatement 对应位置的参数:

  public void parameterize(Statement statement) throws SQLException {
    parameterHandler.setParameters((PreparedStatement) statement);
  }

PreparedStatementHandler#query 方法调用 JDBC API 执行 PreparedStatement :

  @Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    // ResultSetHandler 处理 sql 语句执行后的结果
    return resultSetHandler.handleResultSets(ps);
  }

6 ResultSet 处理器 - org.apache.ibatis.executor.resultset.ResultSetHandler

处理 JDBC 查询后的 ResultSet。

7 事务 - org.apache.ibatis.transaction.

Transaction 是在创建 SqlSession 实例化的,然后一直跟随 Executor 对象。

实现类

  • org.apache.ibatis.transaction.jdbc.JdbcTransaction(默认实现,使用 JDBC Connection 实现事务)
  • org.apache.ibatis.transaction.managed.ManagedTransaction(事务提交与回滚交给容器去处理)

`JdbcTransaction#getConnection() :

  @Override
  public Connection getConnection() throws SQLException {
    if (connection == null) {
      openConnection();
    }
    return connection;
  }
  protected void openConnection() throws SQLException {
    if (log.isDebugEnabled()) {
      log.debug("Opening JDBC Connection");
    }
    connection = dataSource.getConnection();
    if (level != null) {
      connection.setTransactionIsolation(level.getLevel());
    }
    setDesiredAutoCommit(autoCommit);
  }
  1. 如果创建了 connection ,会一直复用这个连接;如果没创建,则从 datasource 创建一个 数据库连接。
  1. 如果多次 Statement 执行时,JdbcTransaction 不会重复创建 JDBC Connection。

8 Configuration 全局配置

管理 Mapper 接口

将 Mapper 接口创建 org.apache.ibatis.binding.MapperProxyFactory
底层依赖于 org.apache.ibatis.binding.MapperRegistry

Mapper 注册中心 - org.apache.ibatis.binding.MapperRegistry

注册 Mapper 接口类方法:

  public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        knownMappers.put(type, new MapperProxyFactory<>(type));
        // It's important that the type is added before the parser is run
        // otherwise the binding may automatically be attempted by the
        // mapper parser. If the type is already known, it won't try.
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

注册条件:1.必须是接口类;2.之前没有注册过这个类型的类

获取 Mapper 接口类方法:

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

使用 MapperProxyFactory#newInstace 方法创建 Mapper 代理对象(org.apache.ibatis.binding.MapperProxy)

Mapper 代理工厂 - org.apache.ibatis.binding.MapperProxyFactory

创建 Mapper 代理:

  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

org.apache.ibatis.binding.MapperProxy 其实就是标准的Java 动态代理,是 java.lang.reflect.InvocationHandler 实现。

MapperProxy#invoke 方法

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else {
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

 private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
    try {
      return MapUtil.computeIfAbsent(methodCache, method, m -> {
        if (m.isDefault()) {
          try {
            if (privateLookupInMethod == null) {
              return new DefaultMethodInvoker(getMethodHandleJava8(method));
            } else {
              return new DefaultMethodInvoker(getMethodHandleJava9(method));
            }
          } catch (IllegalAccessException | InstantiationException | InvocationTargetException
              | NoSuchMethodException e) {
            throw new RuntimeException(e);
          }
        } else {
          return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
        }
      });
    } catch (RuntimeException re) {
      Throwable cause = re.getCause();
      throw cause == null ? re : cause;
    }
  }

其中 cachedInvoker 方法内部存在多种 org.apache.ibatis.binding.MapperProxy.MapperMethodInvoker 实现,如:

  • MethodHandler 实现 - DefaultMethodInvoker
  • Java 反射实现 - PlainMethodInvoker
    其中 PlainMethodInvoker 会简单地委派给 org.apache.ibatis.binding.MapperMethod 实现。

org.apache.ibatis.binding.MapperMethod
代理执行 - execute:

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      case INSERT: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          Object param = method.convertArgsToSqlCommandParam(args);
          result = sqlSession.selectOne(command.getName(), param);
          if (method.returnsOptional() && (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName()
          + "' attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }
posted on 2023-08-05 22:26  不安分的黑娃  阅读(47)  评论(0编辑  收藏  举报