Mybatis

一、Mybatis

学习版本:3.4.2
Mybatis封装了JDBC的操作,使用户专注于SQL语句的编写
GITEE仓库地址

①、pom.xml

创建普通的 maven 工程,修改 pom.xml 文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    

<groupId>com.micozone</groupId>
    <artifactId>mybatis3.4.2</artifactId>
    <version>1.0.0</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>


    <dependencies>

        <!--Mybatis-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.2</version>
        </dependency>

        <!--Mysql驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.15</version>
        </dependency>

        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.28</version>
        </dependency>

    </dependencies>

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

</project>

②、建表、entity、mapper

建库:mybatis,建表:mybatis_table

CREATE TABLE `mybatis_table` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_name` varchar(100) DEFAULT NULL,
  `user_age` int DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

MybatisTable 类

@Data
public class MybatisTable {
    private Long id;
    private String userName;
    private int userAge;
}

MybatisTableMapper 接口

public interface MybatisTableMapper {
    @Select("select id,user_name as userName,user_age as userAge from mybatis_table where id = #{arg0} and user_name = #{arg1}")
    MybatisTable selectByIdAndName(Long id, String userName);   
}

MybatisTableMapper.xml

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

<mapper namespace="com.micozone.mybatis.mapper.MybatisTableMapper">
</mapper>

③、mybatis 配置文件

resources/mybatis.xml

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

    <properties resource="test/jdbc.properties">
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
    </properties>

    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>

    <typeAliases>
        <package name="com.micozone.mybatis.entity"/>
    </typeAliases>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${driver}"/>
                <property name="url"
                          value="jdbc:mysql://localhost:3306/mybatis?useUnicode=true&amp;characterEncoding=UTF-8&amp;serverTimezone=GMT%2B8"/>
                <property name="username" value="${username}"/>
                <property name="password" value="jrbjrb811"/>
            </dataSource>
        </environment>
    </environments>

    <mappers>
        <package name="com.micozone.mybatis.mapper"/>
    </mappers>

</configuration>

resources/test/jdbc.properties

username=root

④、测试

测试类

Reader reader = Resources.getResourceAsReader("mybatis.xml");
// 第一步、创建SqlSessionFactory
SqlSessionFactory defaultSqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
// 第二步、创建SqlSession
SqlSession defaultSqlSession = defaultSqlSessionFactory.openSession();
// 第三步、获取mapper接口的动态代理对象
MybatisTableMapper mybatisTableMapperProxy = defaultSqlSession.getMapper(MybatisTableMapper.class);
// 第四步、调用方法访问数据库
MybatisTable mybatisTable = mybatisTableMapperProxy.selectByIdAndName(1L, "micozone");

二、JDBC

1、概念

JDBC 是 Java 提供的访问数据库(数据源)的接口(规范),数据库厂商根据 JDBC 规范完成 JDBC 的驱动程序,从而实现 Java 对数据库的访问;

2、原理

1、java.sql.Driver 接口、java.sql.DriverManager 类

所有的 JDBC 驱动程序都必须提供一个实现 java.sql.Driver 接口的实现类,并且必须提供一个静态代码块,代码块中向 DriverManager 注册自己的实例。以 mysql-connector-java-8.0.15为例:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    // Register ourselves with the DriverManager
    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }
    ......
}

只有成功的向 DriverManager 中注册驱动,才可以通过 DriverManager.getConnection(…) 方法获取 Connection 对象,所以一般情况下,需要显式的加载驱动实现类,如:Class.forName(“com.mysql.cj.jdbc.Driver”); 由于类加载到内存中会执行静态代码块,从而完成向 DriverManager 中注册驱动。但 JDK 提供了一种 SPI(Service Provider Interface) 机制,使我们不用显式的加载驱动实现类,直接通过 DriverManager.getConnection(…); 也能加载驱动实现类。

2、SPI

SPI 机制需要配合 java.util.ServiceLoader 类使用;
第一步:服务提供者,提供 java.sql.Driver 接口的实现类 com.mysql.cj.jdbc.Driver
第二步:在 classpath(resources) 目录下新建 META-INF/services 目录,创建文件名为 java.sql.Driver,文件内容为 com.mysql.cj.jdbc.Driver
第三步:java.sql.DriverManager 的静态代码块中,使用 ServiceLoader.load(java.sql.Driver.class); 方法,加载驱动实现类 com.mysql.cj.jdbc.Driver,从而执行驱动实现类的静态代码块:向 DriverManager 中注册自己的实例。
在这里插入图片描述
java.sql.DriverManager

public class DriverManager {
    ...... 
    /**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the {@code ServiceLoader} mechanism
     */
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    ......
}

3、java.sql.Connection 接口

成功创建 Connection 接口对象,代表与数据库成功建立连接;
创建方式一:使用 DriverManager

Connection connection = DriverManager.getConnection(
   "jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8",
   "root",
   "password");

创建方式二:使用 DataSource(推荐)

// 一个 DataSource 代表一个数据源
// UnpooledDataSource 是 mybatis 提供的 DataSource 接口的实现类
DataSource dataSource = new UnpooledDataSource(
   "com.mysql.cj.jdbc.Driver",
   "jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8",
   "root",
   "password");
Connection connection = dataSource.getConnection();

最后要调用 close(); 方法关闭 Connection 对象。

4、java.sql.PreparedStatement 接口

通过 connection.prepareStatement(String sql); 方法获取 PreparedStatement 对象;
PreparedStatement 是 Statement 接口的实现类,Statement 接口为 SQL 语句的执行器,通过调用方法从而执行 SQL,常用方法有:execute();、executeQuery();、executeUpdate();
PreparedStatement 相比于 Statement 的优点是,PreparedStatement 实例可以表示预编译的 SQL 语句,占位符使用 “?” 代替,不会出现 SQL 注入的风险。
在使用 setXXX(); 方法为占位符设置值时,存在将 Java 数据类型转换为 JDBC 类型的过程,这一过程由 JDBC 驱动程序实现,如:

Java 类型JDBC 类型
intINTEGER
LongBIGINT

最后要调用 close(); 方法关闭 preparedStatement 对象。
PS:调用存储过程使用 CallableStatement 接口;

5、java.sql.ResultSet 接口

用于解析 SQL 执行结果。

6、JDBC 事务

事务是为了保证多个 SQL 语句同时执行成功或者同时执行失败;
创建 Connection 对象时,默认事务是自动提交的。当调用 Connection 对象的 void setAutoCommit(boolean autoCommit); 方法,参数传递 false 时,禁用事务自动提交,此时必须显式的调用 connection.commit(); 方法提交事务或者 connection.rollback(); 方法回滚事务;

7、事务隔离级别

事务隔离级别代表一个事务对数据的操作对另一个事务的“可见性”。

级别解析
读未提交脏读,第一个事务未提交的数据,第二个事务可以读取到
读已提交Oracle 默认级别,不可重复读,第一个事务读到到某行数据,第二个事务修改改行数据并提交,第一个事务再读取该行数据,此时会与第一次读取的结果不同
可重复读MySQL 默认级别,幻读,某行数据无论另一个事务是否修改过都不会出现不可重复读现象,但是如果查询结果是多行数据,此时会出现幻读现象
序列化读事务串行执行,但是并发率低

3、使用

在这里插入图片描述

package com.micozone.mybatis.test;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

/**
 * 使用 JDBC 访问数据库流程
 *
 * @author Mico Zone
 */
public class JDBC {
    public static void main(String[] args) throws Exception {
        Connection connection = DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8",
                "root",
                "password");
        String sql = "select * from mybatis_table where id = ? and user_name = ?";
        PreparedStatement preparedStatement = connection.prepareStatement(sql);
        preparedStatement.setLong(1, 1L);
        preparedStatement.setString(2, "micozone");
        ResultSet resultSet = preparedStatement.executeQuery();
        while (resultSet.next()) {
            System.out.println(resultSet.getLong(1)); // 1
            System.out.println(resultSet.getString(2)); // micozone
            System.out.println(resultSet.getInt(3)); // 27
        }
    }
}

三、MyBatis中的工具类

1、SQL 类—拼接 SQL

【MyBatis 3 源码深度解析】P59

Connection connection = getConnection();
String sql = new SQL()
        .SELECT("id,user_name,user_age")
        .FROM("mybatis_table")
        .WHERE("id = ?")
        .toString();
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setLong(1, 1L);
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
    System.out.println(resultSet.getLong(1));
    System.out.println(resultSet.getString(2));
    System.out.println(resultSet.getInt(3));
}

2、ScriptRunner 类—执行 SQL 脚本

Connection connection = getConnection();
ScriptRunner scriptRunner = new ScriptRunner(connection);
scriptRunner.runScript(Resources.getResourceAsReader("test/sqlScript.sql"));

resources/test/sqlScript.sql

INSERT INTO mybatis_table
(id, user_name, user_age)
VALUES(504, 'zs', 21);

INSERT INTO mybatis_table
(id, user_name, user_age)
VALUES(505, 'ls', 34);

INSERT INTO mybatis_table
(id, user_name, user_age)
VALUES(600, 'ls', 34),(601, 'ls', 34);

3、SqlRunner 类—执行 SQL 语句

Connection connection = getConnection();
SqlRunner sqlRunner = new SqlRunner(connection);
String selectSql = new SQL()
        .SELECT("id,user_name as userName,user_age as userAge")
        .FROM("mybatis_table")
        .WHERE("id = ?").toString();
Map<String, Object> selectResult = sqlRunner.selectOne(selectSql, 1L);
// selectResult: {USERAGE=27, USERNAME=micozone, ID=1}
System.out.println("selectResult: " + selectResult);

String deleteSql = new SQL()
        .DELETE_FROM("mybatis_table")
        .WHERE("id = ?")
        .toString();
int deleteResult = sqlRunner.delete(deleteSql, 500L);
// deleteResult: 1
System.out.println("deleteResult: " + deleteResult);

String updateSql = new SQL()
        .UPDATE("mybatis_table")
        .SET("user_name = ?")
        .SET("user_age = ?")
        .WHERE("id = ?")
        .toString();
int updateResult = sqlRunner.update(updateSql, "12", 34, 501L);
// updateResult: 1
System.out.println("updateResult: " + updateResult);

String insertSql = new SQL()
        .INSERT_INTO("mybatis_table")
        .INTO_COLUMNS("id,user_name,user_age")
        .INTO_VALUES("?,?,?")
        .toString();
int insertResult = sqlRunner.insert(insertSql, 700L, "qwe", 14);
// insertResult: -2147482647
System.out.println("insertResult: " + insertResult);

4、MetaObject—通过反射获取/设置属性值

List<Book> bookList = new ArrayList<>();
bookList.add(new Book("水浒传"));
bookList.add(new Book("红楼梦"));
BookShop bookShop = new BookShop("新华书店", bookList);
MetaObject metaObject = SystemMetaObject.forObject(bookShop);
System.out.println("第一本书:" + metaObject.getValue("bookList[0].bookName"));
metaObject.setValue("bookList[1].bookName", "三国演义");
System.out.println("第二本书:" + metaObject.getValue("bookList[1].bookName"));
System.out.println("BookShop是否有bookList属性且有getter方法:" + metaObject.hasGetter("bookList"));
System.out.println("BookShop是否有shopName属性且有setter方法:" + metaObject.hasSetter("shopName"));

Book.java

@Data
@AllArgsConstructor
class Book {
    private String bookName;

    public Book() {
    }
}

BookShop.java

@Data
@AllArgsConstructor
class BookShop {
    private String shopName;
    private List<Book> bookList;

    public BookShop() {
    }
}

5、MetaClass—通过反射获取类相关信息

MetaClass metaClass = MetaClass.forClass(BookShop.class, new DefaultReflectorFactory());
// 是否有shopName属性:shopName
System.out.println("是否有shopName属性:" + metaClass.findProperty("shopName"));
// 是否有abc属性:null
System.out.println("是否有abc属性:" + metaClass.findProperty("abc"));
// 所有有getter方法的属性名:[shopName, bookList]
System.out.println("所有有getter方法的属性名:" + Arrays.toString(metaClass.getGetterNames()));
// 所有有setter方法的属性名:[shopName, bookList]
System.out.println("所有有setter方法的属性名:" + Arrays.toString(metaClass.getSetterNames()));

6、ObjectFactory—通过反射创建对象

ObjectFactory objectFactory = new DefaultObjectFactory();
BookShop noParamBookShop = objectFactory.create(BookShop.class);
// noParamBookShop: BookShop(shopName=null, bookList=null)
System.out.println("noParamBookShop: " + noParamBookShop);
List<Book> bookList = new ArrayList<>();
bookList.add(new Book("水浒传"));
bookList.add(new Book("红楼梦"));
List<Class<?>> constructorArgTypes = new ArrayList<>();
constructorArgTypes.add(String.class);
constructorArgTypes.add(List.class);
List<Object> constructorArgs = new ArrayList<>();
constructorArgs.add("新华书店");
constructorArgs.add(bookList);
BookShop bookShop = objectFactory.create(BookShop.class, constructorArgTypes, constructorArgs);
// bookShop: BookShop(shopName=新华书店, bookList=[Book(bookName=水浒传), Book(bookName=红楼梦)])
System.out.println("bookShop: " + bookShop);

7、XPathParser—解析 xml 文件

XPathParser 封装了 XPath,从而完成对 xml 文件的解析。

Reader reader = Resources.getResourceAsReader("test/pens.xml");
XPathParser xPathParser = new XPathParser(reader);
List<Pen> penList = new ArrayList<>();
List<XNode> pensXNodeList = xPathParser.evalNodes("/pens/*");
for (XNode penXNode : pensXNodeList) {
    Pen pen = new Pen();
    Long id = penXNode.getLongAttribute("id");
    pen.setId(id);
    List<XNode> penChildrenXNodeList = penXNode.getChildren();
    for (XNode childrenXNode : penChildrenXNodeList) {
        String name = childrenXNode.getName();
        if ("name".equals(name)) {
            pen.setName(childrenXNode.getStringBody());
        } else if ("price".equals(name)) {
            pen.setPrice(childrenXNode.getIntBody());
        }
    }
    penList.add(pen);

}
System.out.println(penList);

resources/test/pens.xml

<?xml version="1.0" encoding="UTF-8" ?>
<pens>
    <pen id="1">
        <name>晨光</name>
        <price>3</price>
    </pen>
    <pen id="2">
        <name>三菱</name>
        <price>8</price>
    </pen>
</pens>

四、创建 SqlSessionFactory 对象

Reader reader = Resources.getResourceAsReader("mybatis.xml");
// 第一步、创建SqlSessionFactory
SqlSessionFactory defaultSqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);

大体分为两步,第一步:解析 mybatis 配置文件,将内容封装到 Configuration 对象中;第二步:创建 DefaultSqlSessionFactory 对象,将第一步创建的 Configuration 对象赋给 DefaultSqlSessionFactory 对象的 Configuration 属性。

1、解析 mybatis 配置文件

sqlSessionFactoryBuilder.build();

public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
  ......
  // XMLConfigBuilder 底层通过 XPathParser 对象解析 mybatis 配置文件
  XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
  // Configuration config = parser.parse();
  return build(parser.parse());
  ......
}

xmlConfigBuilder.parse();

public Configuration parse() {
  ......
  parseConfiguration(parser.evalNode("/configuration"));
  return configuration;
}

xmlConfigBuilder.parseConfiguration();

private void parseConfiguration(XNode root) {
    /*
    一、解析 <properties> 标签
    <properties resource="test/jdbc.properties">
       <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
    </properties>
    会将jdbc.properties文件中的键值对、以及所有的<property>标签的键值对封装到 Configuration.variables,
    其中 Properties variables = new Properties();
    */
    propertiesElement(root.evalNode("properties"));
    /*
    二、解析 <settings> 标签
    <settings>
       <setting name="logImpl" value="STDOUT_LOGGING"/>
       <setting name="cacheEnabled" value="true"/>
    </settings>
    会将所有的<setting>标签的键值对封装到 Configuration 对象的对应属性中,如:
    <setting name="logImpl" value="STDOUT_LOGGING"/> ===> configuration.logImpl = StdOutImpl.class
    <setting name="cacheEnabled" value="true"/> ===> configuration.cacheEnabled = true
    */
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    loadCustomVfs(settings);
    /*
    三、解析 <typeAliases> 标签
    <typeAliases>
       <package name="com.micozone.mybatis.entity"/>
    </typeAliases>
    会将<package>标签对应的包名下的所有实体类以及对应别名存放到 configuration.typeAliasRegistry.TYPE_ALIASES 中,
    其中 Map<String, Class<?>> TYPE_ALIASES = new HashMap<String, Class<?>>();
    */
    typeAliasesElement(root.evalNode("typeAliases"));
    /*
    四、解析 <plugins> 标签
    <plugins>
       <plugin interceptor="com.micozone.mybatis.mybatis.MyPageInterceptor">
          <property name="name" value="zs"/>
          <property name="age" value="15"/>
       </plugin>
    </plugins>
    MyPageInterceptor 类要实现 Interceptor 接口,并且要有 @Intercepts 注解,插件原理放在第 八 部分。
    这里会将 MyPageInterceptor 类的对象放到 configuration.interceptorChain.interceptors 中,
    其中 List<Interceptor> interceptors = new ArrayList<Interceptor>();
    */
    pluginElement(root.evalNode("plugins"));
    /*
    五、解析 <objectFactory> 标签
    <objectFactory type="com.micozone.mybatis.mybatis.MyObjectFactory">
       <property name="zs" value="14"/>
    </objectFactory>
    MyObjectFactory 类最好继承 DefaultObjectFactory 类,DefaultObjectFactory 对象的作用是创建 Mapper 映射结果对象,
    这里会对 configuration 对象的属性赋值:configuration.objectFactory = new MyObjectFactory(); 
    其中 ObjectFactory objectFactory = new DefaultObjectFactory();
    
    @Data
    public class MyObjectFactory extends DefaultObjectFactory {
       private Properties properties;
       
       @Override
       public <T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
          if (type.equals(MybatisTable.class)) {
             System.out.println("MyObjectFactory.create() execute 正在创建 MybatisTable 对象");
          }
          return super.create(type, constructorArgTypes, constructorArgs);
       }
    }
    */
    objectFactoryElement(root.evalNode("objectFactory"));
    /*
    六、objectWrapperFactory TODO
    */
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    /*
    七、reflectorFactory TODO
    */
    reflectorFactoryElement(root.evalNode("reflectorFactory"));
    settingsElement(settings);
    /*
    八、解析 <environments> 标签
    <environments default="development">
       <environment id="development">
          <transactionManager type="JDBC"/>
          <dataSource type="POOLED">
             <property name="driver" value="${driver}"/>
             <property name="url"
                      value="jdbc:mysql://localhost:3306/mybatis?useUnicode=true&amp;characterEncoding=UTF-8&amp;serverTimezone=GMT%2B8"/>
             <property name="username" value="${username}"/>
             <property name="password" value="password"/>
          </dataSource>
       </environment>
    </environments>
    这里会对 configuration 对象的属性赋值:configuration.environment = new Environment();
    根据<transactionManager>、<dataSource>标签创建 Environment 对象:
    <transactionManager type="JDBC"/> ===> environment.transactionFactory = new JdbcTransactionFactory();
    <dataSource type="POOLED"><property ...>...</dataSource> ===> environment.dataSource = new PooledDataSource();
    */
    environmentsElement(root.evalNode("environments"));
    /*
    九、databaseIdProvider TODO
    */
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    /*
    十、typeHandlers TODO
    */
    typeHandlerElement(root.evalNode("typeHandlers"));
    /*
    十一、解析 <mappers> 标签
    <mappers>
       <!--<package name="com.micozone.mybatis.mapper"/>-->
       <mapper class="com.micozone.mybatis.mapper.BuyerMapper"/>
    </mappers>
    解析 <mappers> 标签步骤较多,放到 七-1 部分
    */
    mapperElement(root.evalNode("mappers"));
}

2、创建 DefaultSqlSessionFactory 对象

sqlSessionFactoryBuilder.build();

public SqlSessionFactory build(Configuration config) {
  // defaultSqlSessionFactory.configuration = config;
  return new DefaultSqlSessionFactory(config);
}

五、创建 SqlSession 对象

SqlSessionFactory defaultSqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
// 第二步、创建SqlSession
SqlSession defaultSqlSession = defaultSqlSessionFactory.openSession();

大体分为两步,第一步:创建 Executor 对象;第二步:创建 DefaultSqlSession 对象,将 executor 赋给 defaultSqlSession 的 executor 属性。

1、创建 Executor 对象

defaultSqlSessionFactory.openSession();

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

defaultSqlSessionFactory.openSessionFromDataSource();

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
  // 在解析 <environments> 标签时,对 configuration 的 environment 属性赋值
  final Environment environment = configuration.getEnvironment();
  // <transactionManager type="JDBC"/> ===> JdbcTransactionFactory
  final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
  // JdbcTransaction,其中<dataSource type="POOLED">... ===> PooledDataSource
  Transaction tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
  /*
  如果<setting name="cacheEnabled" value="true"/>,默认为true:executor = new CachingExecutor(); executor.delegate = new SimpleExecutor();
  如果<setting name="cacheEnabled" value="false"/>:executor = SimpleExecutor;
  */
  final Executor executor = configuration.newExecutor(tx, execType);
  return new DefaultSqlSession(configuration, executor, autoCommit);
}

其中 Executor、Transaction、DataSource的关系:
在这里插入图片描述

2、创建 DefaultSqlSession 对象

defaultSqlSessionFactory.openSessionFromDataSource();

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
  ......
  return new DefaultSqlSession(configuration, executor, autoCommit);
}

DefaultSqlSession 的构造函数

public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
  this.configuration = configuration;
  this.executor = executor;
  this.dirty = false;
  this.autoCommit = autoCommit;
}

六、创建 Mapper 接口的代理对象

大体分为两步,第一步:获取 MapperProxyFactory 对象;第二步:通过 mapperProxyFactory 对象创建 Mapper 接口的动态代理对象。

1、获取 MapperProxyFactory 对象

SqlSessionFactory defaultSqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession defaultSqlSession = defaultSqlSessionFactory.openSession();
// 第三步、获取mapper接口的动态代理对象
MybatisTableMapper mybatisTableMapperProxy = defaultSqlSession.getMapper(MybatisTableMapper.class);

defaultSqlSession.getMapper();

@Override
public <T> T getMapper(Class<T> type) {
  return configuration.<T>getMapper(type, this);
}

configuration.getMapper();

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  return mapperRegistry.getMapper(type, sqlSession);
}

mapperRegistry.getMapper();

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  // 解析 <mappers> 标签时,针对每一个 Mapper 接口:configuration.mapperRegistry.knownMappers.put(Mapper.class, new MapperProxyFactory<T>(Mapper.class));
  // 此处取到 mapperProxyFactory 对象
  final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
  return mapperProxyFactory.newInstance(sqlSession);
}

2、创建 Mapper 接口的动态代理对象

mapperProxyFactory.newInstance(SqlSession sqlSession);

public T newInstance(SqlSession sqlSession) {
  // mapperInterface = Mapper.class
  // MapperProxy 类实现 InvocationHandler 接口,即 MapperProxy 对象为 Mapper 接口动态代理对象的调用处理程序
  final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
  return newInstance(mapperProxy);
}

mapperProxyFactory.newInstance(MapperProxy mapperProxy);

protected T newInstance(MapperProxy<T> mapperProxy) {
  // 这里创建 Mapper 接口的动态代理对象,通过该对象调用方法时,实际上会调用 mapperProxy 对象的 invoke(); 方法
  return (T) Proxy.newProxyInstance(
                          mapperInterface.getClassLoader(), 
                          new Class[] { mapperInterface }, 
                          mapperProxy);
}

七、调用 Mapper 接口的代理对象的方法

此处在介绍 “调用代理对象的方法的执行流程” 前,先介绍一下 四-1 中遗留的 mappers 标签的解析流程。

1、mappers 标签解析

mybatis.xml

<mappers>
   <package name="com.micozone.mybatis.mapper"/>
</mappers>

xmlConfigBuilder.parseConfiguration();

private void parseConfiguration(XNode root) {   
   ......
   mapperElement(root.evalNode("mappers"));
}

大体分为三步,第一步:将 Mapper 接口添加到 knownMappers 缓存中;第二步:loadXmlResource(); 第三步:加载 Mapper 接口中含有 @Select、@Insert、@Update、@Delete 注解的方法。

①、将 Mapper 接口添加到 knownMappers 缓存中

mapperRegistry.addMappers();

public void addMappers(String packageName, Class<?> superType) {
  ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
  resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
  Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
  // 遍历指定包下的所有 Mapper 接口
  for (Class<?> mapperClass : mapperSet) {
    addMapper(mapperClass);
  }
}

mapperRegistry.addMapper();

public <T> void addMapper(Class<T> type) {
  // 其中 Map<Class, MapperProxyFactory> knownMappers = new HashMap<>();
  knownMappers.put(type, new MapperProxyFactory<T>(type));
  MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
  parser.parse();
}

②、loadXmlResource();

mapperAnnotationBuilder.parse();

public void parse() {
  String resource = type.toString();
  if (!configuration.isResourceLoaded(resource)) {
    // 加载 xml 资源
    loadXmlResource();
    ......
    Method[] methods = type.getMethods();
    for (Method method : methods) {
      parseStatement(method);
    }
  }
  ......
}

mapperAnnotationBuilder.loadXmlResource();

private void loadXmlResource() {
  if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
    String xmlResource = type.getName().replace('.', '/') + ".xml";
    // 默认加载 Mapper 接口所在包下的同名 Mapper.xml 文件
    InputStream inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
    XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
    xmlParser.parse();
  }
}

xmlMapperBuilder.parse();

public void parse() {
  ...
  configurationElement(parser.evalNode("/mapper"));
  ...
}

xmlMapperBuilder.configurationElement();

private void configurationElement(XNode context) {
  cacheRefElement(context.evalNode("cache-ref"));
  /*
  1、创建 Cache 对象
  2、configuration.caches.put(cache.getId(), cache);
  其中 Map<String, Cache> caches = new StrictMap<Cache>("Caches collection");
  */
  cacheElement(context.evalNode("cache"));
  parameterMapElement(context.evalNodes("/mapper/parameterMap"));
  /*
  <resultMap id="baseResultMap" type="mybatisTable">
     <id property="id" column="id" jdbcType="BIGINT"/>
     <result property="userName" column="user_name" jdbcType="VARCHAR"/>
     <result property="userAge" column="user_age" jdbcType="INTEGER"/>
  </resultMap>
  1、针对每一个<id>、<result>标签,创建 ResultMapping 对象,放到 List<ResultMapping> resultMappings 集合中
  2、创建 ResultMap 对象,将 resultMappings 集合赋给 resultMap 对象的 resultMappings 属性
  3、configuration.resultMaps.put(resultMap.getId(), resultMap);
  其中 Map<String, ResultMap> resultMaps = new StrictMap<ResultMap>("Result Maps collection");
  */
  resultMapElements(context.evalNodes("/mapper/resultMap"));
  /*
  <sql id="Base_Column_List">
     id,user_name,user_age
  </sql>
  1、configuration.sqlFragments.put(String id, XNode context);
  其中 Map<String, XNode> sqlFragments;
  */
  sqlElement(context.evalNodes("/mapper/sql"));
  /*
  <insert id="insertOne" parameterType="mybatisTable">
     insert into mybatis_table (id, user_name, user_age)
     values (#{id}, #{userName}, #{userAge})
  </insert>
  1、针对每一个<select>、<insert>、<update>、<delete>标签,封装 MappedStatement 对象
  2、configuration.mappedStatements.put(mappedStatement.getId(), mappedStatement);
  其中 Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
  */
  buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
}

③、加载 Mapper 接口中的方法

只加载含有 @Select、@Insert、@Update、@Delete 注解的方法。
mapperAnnotationBuilder.parse();

public void parse() {
  String resource = type.toString();
  if (!configuration.isResourceLoaded(resource)) {
    loadXmlResource();
    ......
    Method[] methods = type.getMethods();
    for (Method method : methods) {
      // 处理每一个含有 @Select、@Insert、@Update、@Delete 注解的方法
      /*
      @Select("select id,user_name as userName,user_age as userAge from mybatis_table where id = #{arg0} and user_name = #{arg1}")
      MybatisTable selectByIdAndName(Long id, String userName);

      1、针对每一个含有 @Select、@Insert、@Update、@Delete 注解的方法,封装 MappedStatement 对象
      2、configuration.mappedStatements.put(mappedStatement.getId(), mappedStatement);
      其中 Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
      */
      parseStatement(method);
    }
  }
  ......
}

2、调用代理对象的方法

介绍完 mappers 标签的解析之后,我们知道针对 Mapper.xml 中的每一个标签,都会创建相应的对象放到 configuration 对象的某个缓存中,针对含有 @Select、@Insert、@Update、@Delete 注解的方法,处理逻辑与 Mapper.xml 中的 select、insert、update、delete 标签的处理逻辑大体一致。
再介绍一下 “调用代理对象的方法访问数据库并执行 SQL” 的逻辑。

SqlSessionFactory defaultSqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession defaultSqlSession = defaultSqlSessionFactory.openSession();
MybatisTableMapper mybatisTableMapperProxy = defaultSqlSession.getMapper(MybatisTableMapper.class);
// 第四步、调用方法访问数据库
MybatisTable mybatisTable = mybatisTableMapperProxy.selectByIdAndName(1L, "micozone");

其中 MybatisTableMapper.java

public interface MybatisTableMapper {
    @Select("select id,user_name as userName,user_age as userAge from mybatis_table where id = #{arg0} and user_name = #{arg1}")
    MybatisTable selectByIdAndName(Long id, String userName);
}

这里调用的主要步骤有:mapperProxy.invoke(); ===> defaultSqlSession.selectOne(); ===> executor.query(); ===> 使用 JDBC 访问数据库
主要针对 executor.query(); 以及 使用 JDBC 访问数据库 这两步进行深度分析。

解析一:executor.query();

mapperProxy.invoke();

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  ......
  final MapperMethod mapperMethod = cachedMapperMethod(method);
  return mapperMethod.execute(sqlSession, args);
}

mapperMethod.execute();

public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  switch (command.getType()) {
    case INSERT: {
    ...
    }
    case UPDATE: {
      ...
    }
    case DELETE: {
      ...
    }
    case SELECT:
      ...
      } else {
        // 该方法会对参数进行封装,封装形式见下图
        Object param = method.convertArgsToSqlCommandParam(args);
        // 由于返回结果是一个实体类对象,此处会调用 sqlSession.selectOne(); 方法
        result = sqlSession.selectOne(command.getName(), param);
      }
      break;
    ...
  }
  ...
  return result;
}

在这里插入图片描述
defaultSqlSession.selectOne();

@Override
public <T> T selectOne(String statement, Object parameter) {
  List<T> list = this.<T>selectList(statement, parameter);
  ......
}

defaultSqlSession.selectList();

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  // 先根据 语句id,获取 MappedStatement 对象:MappedStatement ms = configuration.mappedStatements.get(id);
  // 其中 Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
  MappedStatement ms = configuration.getMappedStatement(statement);
  return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
}

cachingExecutor.query(…);

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  // boundSql 对象封装了 sql 语句信息以及请求参数信息
  BoundSql boundSql = ms.getBoundSql(parameterObject);
  // cachingExecutor的 query(); 方法主要分为两步,第一步:尝试查询二级缓存,如果二级缓存未开启或者没查询到;第二步:通过装饰者模式,调用 simpleExecutor.query(); 方法
  // 通过 mappedStatement、boundSql 等对象封装 cacheKey 对象,cacheKey 对象作为二级缓存的 key
  CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
  return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

cachingExecutor.query(…);

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
  // 在解析 mybatis 配置文件的 <mappers> 标签时,除了会将 Mapper 接口的 @CacheNamespace 注解以及 Mapper.xml 中的 <cache> 标签,封装为 cache 对象放到 configuration.caches 缓存中,还会赋给封装的 MappedStatement 对象的 cache 属性。  
  // 由于二级缓存的数据是存放到 MappedStatement 对象的 cache 属性中,而 MappedStatement 封装的一个 sql 语句的相关信息,所以二级缓存的数据与 SqlSession 的生命周期无关
  Cache cache = ms.getCache();
  // 如果 cache != null,说明开启了二级缓存
  if (cache != null) {
    ...
    // 其中 tcm 是 cachingExecutor的属性,类型为 TransactionalCacheManager,通过该对象去查询、添加二级缓存数据的作用是保证该 sqlSession 在没有提交事务之前,二级缓存中的数据不会更新;
    // TransactionalCacheManager 会把查询结果放到 TransactionalCache 对象的某个属性中,sqlSession.commit(); 时,才会把 TransactionalCache 对象的该属性对应的所有查询结果放到 MappedStatement.cache 中;
    // 所以既是同一个 sqlSession 同一个请求参数连续查询两次,第二次也不会走二级缓存,但会走一级缓存;
    // 只有第一个 sqlSession.commit(); 第二个 sqlSession 同样地请求参数再次查询时会走二级缓存
    List<E> list = (List<E>) tcm.getObject(cache, key);
    if (list == null) {
        // delegate 的类型为 SimpleExecutor 
        list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        // 将查询结果放到临时的一个缓存中
        tcm.putObject(cache, key, list);
    }
    return list;
  }
  return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

SimpleExecutor 集成 BaseExecutor,最终会调用 BaseExecutor.query();

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  ......
  // 先尝试查询一级缓存
  // 由于 localCache 是 BaseExecutor 对象的属性,而 BaseExecutor 对象是 CachingExecutor 对象的属性,而 CachingExecutor 对象是 DefaultSqlSession 对象的属性,所以一级缓存针对于同一个 sqlSession 对象,即同一个 sqlSession 对象创建的代理对象,调用同一个方法两次且传递同一个参数时,第二次调用会走一级缓存
  List<E>  list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
  ......
  if (list == null) {
      // 如果一级缓存没有查到,通过 JDBC 访问数据库并执行 SQL
      list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
  } 
  ......
  return list;
}

BaseExecutor.queryFromDatabase();

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  ......
  // 访问数据库,获取查询结果
  List<E> list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
  // 将查询结果放到一级缓存
  localCache.putObject(key, list);
  ......
  return list;
}

解析二:使用 JDBC 访问数据库

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();
    // configuration.newStatementHandler(); 方法主要有两步
    // 第一步:创建 PreparedStatementHandler 对象,
    // 第二步:创建 parameterHandler、ResultSetHandler 对象,其中 PreparedStatementHandler 继承 BaseStatementHandler,BaseStatementHandler 有两个属性:parameterHandler、ResultSetHandler
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    // SimpleExecutor.prepareStatement(); 方法主要有两步
    // 第一步:调用 handler.prepare(); 方法获取 PreparedStatement 对象,底层主要代码为:connection.prepareStatement();
    // 第二步:调用 handler.parameterize() 方法为 PreparedStatement 对象的占位符设置值,底层调用 parameterHandler.setParameters(); 方法,底层调用具体的 TypeHandler 处理 Java 类型 ===> JDBC 类型
    stmt = prepareStatement(handler, ms.getStatementLog());
    // handler.query(); 方法主要有两步
    // 第一步:调用 preparedStatement.execute(); 方法执行 SQL
    // 第二步:调用 resultSetHandler.handleResultSets(); 方法处理查询结果,底层通过 TypeHandler 完成 JDBC 类型 ===> Java 类型
    return handler.<E>query(stmt, resultHandler);
  } finally {
    closeStatement(stmt);
  }
}

八、MyBatis 插件

MyBatis 插件可以看作是自定义拦截器,对 MyBatis 访问数据库中的某些步骤进行功能增强。
以自定义一个分页插件为例:

准备工作

mybatis_table 表中数据:
在这里插入图片描述
MybatisTableMapper 接口

@CacheNamespace // 开启二级缓存
public interface MybatisTableMapper {
    @Select("select id,user_name as userName,user_age as userAge from mybatis_table")
    List<MybatisTable> selectPage(MyPage myPage);
}

MyPage 类

@Data
public class MyPage {
    // 总条数
    private int total;
    // 页数(从1开始)
    private int pageNo;
    // 每页个数
    private int pageSize;

    public MyPage(int pageNo, int pageSize) {
        this.pageNo = pageNo;
        this.pageSize = pageSize;
    }
}

①、创建自定义拦截器(插件)

package com.micozone.mybatis.mybatis;

import lombok.Data;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.SystemMetaObject;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Map;
import java.util.Properties;

/**
 * 自定义分页插件(拦截 {@link StatementHandler#prepare(Connection, Integer)} )
 *
 * @author Mico Zone
 */
@Data
@Intercepts({
        // 此处可以配置多个 @Signature
        // 拦截器拦截的维度是 Executor、StatementHandler、ParameterHandler、ResultSetHandler 接口中的某个方法
        @Signature(
                type = StatementHandler.class,
                method = "prepare",
                args = {Connection.class, Integer.class})
})
public class MyPageInterceptor implements Interceptor {

    private Properties properties;

    @Override
    // 分页逻辑,即 statementHandler.prepare(); 时,会走这里的逻辑
    // invocation.proceed(); 执行的是原 statementHandler.prepare(); 的逻辑
    public Object intercept(Invocation invocation) throws Throwable {
        // 目标对象
        StatementHandler target = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = target.getBoundSql();
        // 拿到参数
        Object parameterObject = boundSql.getParameterObject();
        MyPage myPage = null;
        // 判断参数
        if (parameterObject instanceof MyPage) {
            myPage = (MyPage) parameterObject;
        } else if (parameterObject instanceof Map) {
            myPage = (MyPage) ((Map<?, ?>) parameterObject).values().stream().filter(parameter -> parameter instanceof MyPage).findFirst().orElse(null);
        }
        if (myPage == null) {
            return invocation.proceed();
        }

        // 从参数列表中,获取一个Connection
        Connection connection = (Connection) invocation.getArgs()[0];
        String sql = boundSql.getSql();
        int total = 0;
        String countSQL = String.format("select count(*) from (%s) as _page", sql);
        PreparedStatement preparedStatement = connection.prepareStatement(countSQL);
        target.getParameterHandler().setParameters(preparedStatement);
        ResultSet resultSet = preparedStatement.executeQuery();
        if (resultSet.next()) {
            // 获取总条数
            total = resultSet.getInt(1);
        }
        // 关闭资源,连接
        resultSet.close();
        preparedStatement.close();
        // 设置总条数
        myPage.setTotal(total);
        // 对原有SQL进行修改,拼接limit
        String newSQL = String.format("%s limit %s , %s", sql, (myPage.getPageNo() - 1) * myPage.getPageSize(), myPage.getPageSize());
        SystemMetaObject.forObject(boundSql).setValue("sql", newSQL);
        return invocation.proceed();
    }

    @Override
    /* 
    InterceptorChain 对象有一个 pluginAll(); 方法:
    public Object pluginAll(Object target) {
       for (Interceptor interceptor : interceptors) {
         // target 为目标对象,Executor、StatementHandler、ParameterHandler、ResultSetHandler 四者之一
         // 调用 plugin(); 方法的作用是创建目标对象的代理对象
         target = interceptor.plugin(target);
       }
       return target;
    }
    */
    public Object plugin(Object target) {
        // Plugin.wrap(); 方法的作用就是判断目标对象是否实现 “当前插件拦截的方法所在的接口”,如果实现,则创建目标对象的代理对象(JDK动态代理),调用处理程序为 Plugin 对象
        // 所以 statementHandler.prepare(); 时,实际上直接调用 plugin.invoke(); 方法,invoke(); 方法中判断该插件是否拦截的是 statementHandler.prepare(); 方法,如果不是,走目标对象的 prepare(); 逻辑,如果是,则会调用 MyPageInterceptor.interceptor(); 方法
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // 将 <property> 标签配置的键值对保存到这里
        this.properties = properties;
    }
}

②、mybatis 配置文件中配置插件

mybatis.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>
    ......
    <plugins>
        <!--在此处配置完插件后,MyPageInterceptor 对象将会放到 configuration.interceptorChain.interceptors 集合中-->
        <plugin interceptor="com.micozone.mybatis.mybatis.MyPageInterceptor">
            <property name="name" value="zs"/>
            <property name="age" value="15"/>
        </plugin>
    </plugins>
    ......
</configuration>

③、调用方法执行 MyPageInterceptor.intercept();

Reader reader = Resources.getResourceAsReader("mybatis.xml");
SqlSessionFactory defaultSqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
// 在创建 Executor 对象时,会调用 configuration.newExecutor(); 方法,内部会遍历所有拦截器创建动态代理对象:executor = (Executor) interceptorChain.pluginAll(executor);
SqlSession defaultSqlSession = defaultSqlSessionFactory.openSession();
MybatisTableMapper mybatisTableMapperProxy = defaultSqlSession.getMapper(MybatisTableMapper.class);
// 在二级缓存、一级缓存都没有查到的情况下,会创建 StatementHandler、ParameterHandler、ResultSetHandler 对象通过 JDBC 访问数据库
// 在创建该三个对象时,同样会调用 configuration.newStatementHandler();、configuration.newParameterHandler();、configuration.newResultSetHandler(); 三个方法
// 三个方法内部也会调用 interceptorChain.pluginAll(); 方法
List<MybatisTable> mybatisTableList = mybatisTableMapperProxy.selectPage(new MyPage(2, 3));

九、级联映射

通过 MyBatis 的级联映射,可以实现一对一、一对多、多对多的关联查询。

准备工作

建表:

-- 订单表
CREATE TABLE `orders` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `buyer_id` bigint DEFAULT NULL,
  `order_name` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

-- 买家表(一个买家可以对应一个或者多个订单)
CREATE TABLE `buyer` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `buyer_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

Orders 类

@Data
public class Orders {
    private Long id;
    private Long buyerId;
    private String orderName;
    private Buyer buyer;
}

OrdersMapper 接口

public interface OrdersMapper {
}

OrdersMapper.xml

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

<mapper namespace="com.micozone.mybatis.mapper.OrdersMapper">
 
</mapper>

Buyer 类

@Data
public class Buyer {
    private Long id;
    private String buyerName;
    private List<Orders> ordersList;
}

BuyerMapper 接口

public interface BuyerMapper {
}

BuyerMapper.xml

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

<mapper namespace="com.micozone.mybatis.mapper.BuyerMapper">

</mapper>

一对多

实现方式一:collection 标签添加 select 属性

这种方式的特点是会执行两次 SQL。

BuyerMapper 接口

public interface BuyerMapper {
    Buyer selectBuyerByIdOneToMany1(Long id);
}

BuyerMapper.xml

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

<mapper namespace="com.micozone.mybatis.mapper.BuyerMapper">

    <resultMap id="oneToManyByCollection" type="buyer">
        <id property="id" column="id" jdbcType="BIGINT"/>
        <result property="buyerName" column="buyer_name" jdbcType="VARCHAR"/>
        <!-- 通过 select 属性,当某个查询的 resultMap="oneToManyByCollection" 时,会进行二次查询 -->
        <collection property="ordersList"
                    ofType="com.micozone.mybatis.entity.Orders"
                    select="com.micozone.mybatis.mapper.OrdersMapper.selectOrdersListByBuyerId"
                    javaType="java.util.ArrayList"
                    column="id"/><!-- id 对应 buyer 的 id -->
    </resultMap>

    <select id="selectBuyerByIdOneToMany1" parameterType="long" resultMap="oneToManyByCollection">
        select *
        from buyer
        where id = #{id}
    </select>
</mapper>

OrdersMapper 接口

public interface OrdersMapper {
    List<Orders> selectOrdersListByBuyerId(Long buyerId);
}

OrdersMapper.xml

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

<mapper namespace="com.micozone.mybatis.mapper.OrdersMapper">

    <resultMap id="baseResultMap" type="orders">
        <id property="id" column="id" jdbcType="BIGINT"/>
        <result property="orderName" column="order_name" jdbcType="VARCHAR"/>
        <result property="buyerId" column="buyer_id" jdbcType="BIGINT"/>
    </resultMap>

    <select id="selectOrdersListByBuyerId" resultMap="baseResultMap">
        select *
        from orders
        where buyer_id = #{buyerId}
    </select>

</mapper>

测试类

Reader reader = Resources.getResourceAsReader("mybatis.xml");
SqlSessionFactory defaultSqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession defaultSqlSession = defaultSqlSessionFactory.openSession(true);
BuyerMapper buyerMapper = defaultSqlSession.getMapper(BuyerMapper.class);
buyerMapper.selectBuyerByIdOneToMany1(1L)); // 执行两次 sql

实现方式二:使用 join 进行关联查询

这种方式的特点是只会执行一次 SQL。

BuyerMapper 接口

public interface BuyerMapper {
    Buyer selectBuyerByIdOneToMany2(Long id);
}

BuyerMapper.xml

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

<mapper namespace="com.micozone.mybatis.mapper.BuyerMapper">

    <resultMap id="oneToManyByJoin" type="buyer">
        <id property="id" column="id" jdbcType="BIGINT"/>
        <result property="buyerName" column="buyer_name" jdbcType="VARCHAR"/>
        <collection property="ordersList" ofType="com.micozone.mybatis.entity.Orders">
            <!-- 这里 column 不能等于 id,否则关联查询时返回列有两个 id 会报错,此处区分开 buyer 和 orders 的 id -->
            <id property="id" column="orders_id" jdbcType="BIGINT"/>
            <result property="orderName" column="order_name" jdbcType="VARCHAR"/>
            <result property="buyerId" column="buyer_id" jdbcType="BIGINT"/>
        </collection>
    </resultMap>

    <select id="selectBuyerByIdOneToMany2" parameterType="long" resultMap="oneToManyByJoin">
        select buyer.*, orders.id as orders_id, orders.order_name, orders.buyer_id
        from buyer
                 join orders on buyer.id = orders.buyer_id
        where buyer.id = #{id}
    </select>
    
</mapper>

测试类

Reader reader = Resources.getResourceAsReader("mybatis.xml");
SqlSessionFactory defaultSqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession defaultSqlSession = defaultSqlSessionFactory.openSession(true);
BuyerMapper buyerMapper = defaultSqlSession.getMapper(BuyerMapper.class);
buyerMapper.selectBuyerByIdOneToMany2(1L));

一对一

实现方式一:association 标签添加 select 属性

这种方式的特点是会执行两次 SQL。

OrdersMapper 接口

public interface OrdersMapper {
    Orders selectOrdersByIdOneToOne1(Long id);
}

OrdersMapper.xml

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

<mapper namespace="com.micozone.mybatis.mapper.OrdersMapper">

    <resultMap id="oneToOneByAssociation" type="orders">
        <id property="id" column="id" jdbcType="BIGINT"/>
        <result property="orderName" column="order_name" jdbcType="VARCHAR"/>
        <result property="buyerId" column="buyer_id" jdbcType="BIGINT"/>
        <association property="buyer"
                     javaType="com.micozone.mybatis.entity.Buyer"
                     select="com.micozone.mybatis.mapper.BuyerMapper.selectBuyerById"
                     column="buyer_id"/><!--对应 orders 表的列-->
    </resultMap>

    <select id="selectOrdersByIdOneToOne1" parameterType="long" resultMap="oneToOneByAssociation">
        select *
        from orders
        where id = #{id}
    </select>

</mapper>

BuyerMapper 接口

public interface BuyerMapper {
    @Select("select id ,buyer_name as buyerName from buyer where id = #{arg0}")
    Buyer selectBuyerById(Long id);
}

测试类

Reader reader = Resources.getResourceAsReader("mybatis.xml");
SqlSessionFactory defaultSqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession defaultSqlSession = defaultSqlSessionFactory.openSession(true);
OrdersMapper ordersMapper = defaultSqlSession.getMapper(OrdersMapper.class);
ordersMapper.selectOrdersByIdOneToOne1(1L); // 执行两次 sql

实现方式二:使用 join 进行关联查询

这种方式的特点是只会执行一次 SQL。

OrdersMapper 接口

public interface OrdersMapper {
    Orders selectOrdersByIdOneToOne2(Long id);
}

OrdersMapper.xml

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

<mapper namespace="com.micozone.mybatis.mapper.OrdersMapper">

    <resultMap id="oneToOneByJoin" type="orders">
        <id property="id" column="id" jdbcType="BIGINT"/>
        <result property="orderName" column="order_name" jdbcType="VARCHAR"/>
        <result property="buyerId" column="buyer_id" jdbcType="BIGINT"/>
        <association property="buyer" javaType="com.micozone.mybatis.entity.Buyer">
            <id property="id" column="b_id" jdbcType="BIGINT"/>
            <result property="buyerName" column="buyer_name" jdbcType="VARCHAR"/>
        </association>
    </resultMap>

    <select id="selectOrdersByIdOneToOne2" parameterType="long" resultMap="oneToOneByJoin">
        select orders.*, buyer.id as b_id, buyer.buyer_name
        from orders
                 left join buyer on orders.buyer_id = buyer.id
        where orders.id = #{id}
    </select>

</mapper>

测试类

Reader reader = Resources.getResourceAsReader("mybatis.xml");
SqlSessionFactory defaultSqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession defaultSqlSession = defaultSqlSessionFactory.openSession(true);
OrdersMapper ordersMapper = defaultSqlSession.getMapper(OrdersMapper.class);
ordersMapper.selectOrdersByIdOneToOne2(1L);

二、Mybatis-Spring

三、Mybatis-SpringBoot

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

MicoZone

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值