Skip to the content.

概述

MyBatis 框架,ORM(Object/Relational Mapping,即对象关系映射)框架。ORM 框架描述了 Java 对象与数据库表之间的映射关系,可以自动将 Java 应用程序中的对象持久化到关系型数据库的表中。

PS:MyBatis 3.4x 版本,把它内部需要的三方 jar 都整合在一起了。

常见的 ORM 框架有:Hibernate、MyBatis、Spring JPA。

MyBatis 提供了两种 SQL 语句映射方式:xml 和 注解

使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java 注解不仅力不从心,还会让本就复杂的 SQL 语句更加混乱不堪。 因此,如果你需要做一些很复杂的操作,最好用 XML 来映射语句。Tongyilima xml 生成的提示还可以。

解决的问题

MyBatis 减少了样板代码,简化了持久层的开发。

基本原理

MyBatis 框架在操作数据库时,大体经过了 8 个步骤

快捷键基础

win10 快捷键

IDEA 快捷键

重要配置

如果需要 xml 文件和接口文件存储在一个路径下,则需要为 maven 配置下面这个属性

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

上述配置会导致 resources 目录下的资源文件无法被发布到 classes 下。如果希望 resources 目录下的文件也被发布到 classes 下,需要增加下面的配置。

<bulid>        
    <resources>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.xml</include>
            </includes>
            <filtering>true</filtering>
        </resource>
        <!-- src/main/resources 下的所有文件也是需要发布的资源 -->
        <resource>
        	<directory>src/main/resources</directory>
            <includes>
            	<include>*.*</include>
            </includes>
        </resource>
    </resources>
</build>

原因:maven 通过 <resources> 标签可以定义项目的资源目录。先前只定义了 src/main/java/ 下的 xml 文件是项目的资源目录。

设计模式

相对路径 src/java/main/文件名.xml

读配置文件

① 用类加载器,读类路径下的;

② 用 Servlet Context 对象的 getRealPath

创建工程 MyBatis 用了构建者模式。告诉需求,根据需求创建我们想要的。

// in 形式下创建的工厂,多了几个类,操作看起来麻烦了,但是组合更加灵活的。
build.build(in) 

生成 SqlSession 用了工厂模式,解耦对象的创建过程。

创建 Dao 接口实现类用了代理模式

在看 MyBatis 源码的时候,通过一些类的名称大概知道了 MyBatis 用到了什么技术。MyBatis 解析的时候应该用到了词法分析,分析字符串。在动态生成代理类的时候用到了字节码增强技术。

基础篇

表结构

create table mybatis.clazz
(
    id   int auto_increment primary key,
    name varchar(60) default 'one' not null
);

create table mybatis.users
(
    id       int auto_increment primary key,
    name     varchar(60)      not null,
    sex      char default '1' not null,
    clazz_id int  default 1   not null -- 和 clazz 表的 id 对应,是逻辑外键关系
);

基本环境搭建

工程环境

Maven 工程使用 MyBatis 的时候,配置文件需要放在 resrouces 目录下,否则无法找到。

整合 Druid 的时候,需要的是数据源,需要我们手动 new 出 Druid 的数据源。

maven 的 pom 文件

<?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>cn.payphone</groupId>
    <artifactId>LearnMyBatis</artifactId>
    <version>1.0-SNAPSHOT</version>

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

    <dependencies>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.6</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.21</version>
        </dependency>
        
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.1.1</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
    </dependencies>

</project>

SqlConfig 配置文件

<?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>
    <!--配置包别名【resultType】-->
    <typeAliases>
        <package name="com.bbxx.pojo"/>
    </typeAliases>
    <!-- 配置 mybatis 的环境 -->
    <environments default="mysql">
        <!-- 配置 mysql 的环境 -->
        <environment id="mysql">
            <!-- 配置事务的类型 -->
            <transactionManager type="JDBC"></transactionManager>
            <!-- 配置连接数据库的信息:用的是数据源(连接池) 如果用的是三方数据源,如阿里的druid-->
            <!-- <dataSource type="xx.xx.DruidPoolConfig">这个DruidPoolConfig是我们自己new出来的 -->
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>
    <!-- 告知 mybatis 映射配置的位置 -->
    <!-- com/bbxx/dao/UserDao.xml是mavenresouce目录下的哦! -->
    <mappers>
        <mapper resource="com/bbxx/dao/UserDao.xml"/>
        <!-- 如果是用的注解SQL,则采用接口全名,因为注解方式不用Mapper文件! -->
        <mapper class="com.bbxx.dao.IUserDao"/>
    </mappers>
</configuration>

mapper 文件示例

<?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是接口的类全名 resultType是返回类型的类全名,可通过配置简写 -->
<mapper namespace="cn.mapper.UserMapper">
    <select id="selectAll" resultType="cn.pojo.User">
        select * from users
    </select>
</mapper>

执行 SQL 的代码

public class HelloMyBatis {
    public static void main(String[] args) throws IOException {
        String resourcePath = "MyBatisConfig.xml";
        InputStream in = Resources.getResourceAsStream(resourcePath);
        // 创建 SqlSessionFactory 工厂
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
        SqlSession sqlSession = sqlSessionFactory.openSession();
		
        // 根据唯一空间标识符,调用方法对应的 SQL 语句
        List<User> objects = sqlSession.selectList("cn.mapper.UserMapper.selectAll",User.class);
        // 如果方法名是唯一的,则可以省略前缀 cn.mapper.UserMapper
        List<User> selectAll = sqlSession.selectList("selectAll", User.class);

        selectAll.forEach(System.out::println);
        
        // 直接拿到UserMapper 接口对应的动态代理对象,然后调用方法
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        List<User> users = mapper.selectAll();
        users.forEach(System.out::println);
    }
}

public class User {
    private int id;
    private String name;
    private String sex;
	//... some code
}

// mapper 
public interface UserMapper {
    List<User> selectAll();
}

集成Druid

集成 Druid 只需要在前面的基础上修改一点东西即可。

public class DataSourceDruid extends UnpooledDataSourceFactory {

    @Override
    public DataSource getDataSource() {
        DruidDataSource druidDataSource = new DruidDataSource();
        // Druid的配置信息。看的源码知道的,可以通过这种方式进行配置。
        druidDataSource.setUrl("jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC");
        druidDataSource.setUsername("root");
        druidDataSource.setPassword("root");
        druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        return druidDataSource;
    }
}
<?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>
    <!-- 配置pojo的别名 -->
    <typeAliases>
        <package name="com.bbxx.pojo"/>
    </typeAliases>
    <!-- 配置 mybatis 的环境 可以通过 <environments\> 元素配置多种数据源,即配置多种数据库。-->
    <environments default="mysql">
        <!-- 配置 mysql 的环境 -->
        <environment id="mysql">
            <!-- 配置事务的类型 -->
            <transactionManager type="JDBC"></transactionManager>
            <!-- 配置连接数据库的信息:用的是Druid数据源(连接池) 这个类是我们自己定义的,且重写了getDataSource方法!-->
            <dataSource type="com.bbxx.utils.DataSourceDruid">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC"/>
                <property name="username" value="root"/>
                <property name="password" value="root"/>
            </dataSource>
        </environment>
    </environments>
    <!-- 告知 mybatis 映射配置的位置 -->
    <mappers>
        <mapper resource="com/bbxx/dao/UserDao.xml"/>
    </mappers>
</configuration>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.5</version>
</dependency>

日志相关

log4j 的日志放在 resources 下。

log4j 日志配置

#log4j基本配置
log4j.rootLogger=DEBUG,console,file
#设置输出控制台信息
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.Target = System.out
log4j.appender.console.Threshold=DEBUG     #控制台只会打印INFO级别及以上的日志信息
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%c-%m%n
#设置输出文件信息
log4j.appender.file = org.apache.log4j.RollingFileAppender
log4j.appender.file.File=log/mybatis.log     #日志文件路径,在应用下的log/mybatis.log文件中
log4j.appender.file.Append=true   #追加
log4j.appender.file.MaxFileSize=100mb    #达到100m后产生一个新文件
log4j.appender.file.Threshold=ERROR      #只会写ERROR级别及以上的日志信息
log4j.appender.file.layout=org.apache.log4j.PatternLayout     #布局器
log4j.appender.file.layout.ConversionPattern=%c-%m%n   #布局器格式

log4j 仅打印 SQL 语句

# 全局日志配置
log4j.rootLogger=ERROR, stdout
# MyBatis 日志配置  com.bbxx.dao是包名。
log4j.logger.com.bbxx.dao=TRACE
# 控制台输出
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n

Mapper映射文件

官方链接

Mapper 映射文件放在 maven 工程 resource 下 com/daily/mapper 也是 resource 的子目录

1、用文件路径引入

<mappers>
    <mapper resource="com/daily/mapper/UserMapper.xml" />
    <mapper resource="com/daily/mapper/ProductMapper.xml" />
    <mapper resource="com/daily/mapper/BlogMapper.xml" />
</mappers>

2、用包名引入

这种引入方式相当于批量引入一个包下的所有映射器。此种方式要求 xml 和接口名称一致。

<mappers>
    <package name="com.daily.mapper"/>
</mappers>

3、用类注册引入

<mappers>
    <mapper class="com.daily.mapper.UserMapper" />
    <mapper class="com.daily.mapper.ProductMapper" />
    <mapper class="com.daily.mapper.BlogMapper" />
</mappers>

4、使用 URL 方式引入

<mappers>
    <mapper url="xml文件访问URL" />
</mappers>

maven 项目下,所有的非 *.java 文件都要放在 resources 目录下。resources 是项目的资源根目录!

如:src/main/java 目录下的包和类都是以 classes 为根目录进行发布。resources 下的资源也是以 classes 为根目录。

mybatis 多对多是由两个一对一组成的,如:user 一对多 role,role 一对多 user,这样 user 和 role 就是多对多关系了。 数据库的多对多需要一个中间表来描述两表的多对多关系。

如果需要 xml 文件和接口文件存储在一个路径下,则需要为 maven 配置下面这个属性

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

mave resource 属性详解

项目地址

简单的CURD

POJO 代码

public class User {
    private int id;
    private String name;
    private String sex;

    public User(){}
    public User(String name,String sex){
        this.name = name;
        this.sex = sex;
    }
    // 省略 set get toString
}

Mapper 接口

package cn.mapper;

import cn.pojo.User;
import java.util.List;

public interface UserMapper {
    List<User> selectAll();

    List<User> findByName(String name);

    boolean insert(User user);

    boolean update(User user);

    boolean deleteById(int id);

    long findTotal();
}

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="cn.mapper.UserMapper">
	<!-- 如果配置了别名,那么 resultType 就不用写全名了 -->
    <select id="selectAll" resultType="cn.pojo.User">
        select * 
        from users
    </select>

    <select id="findByName" resultType="cn.pojo.User">
        select * 
        from users
        where name like concat("%", #{name}, "%")
    </select>

    <!--    拿到自增的主键 id-->
    <!--
         让MyBatis自动地将自增id赋值给传入地 User 对象的id属性。
         useGeneratedKeys="true";原生jdbc获取自增主键的方法:
         keyProperty="",将刚才自增的id封装给哪个属性。-->
    <insert id="insert"  useGeneratedKeys="true" keyProperty="id">
        insert into users(name, sex) values (#{name}, #{sex} )
    </insert>

    <!-- parameterType默认可以不写!mybatis会自动判断的 -->
    <update id="update" parameterType="User">
        update users
        set name=#{name}, sex=#{sex}
        where id = #{id}
    </update>

    <delete id="deleteById" parameterType="int">
        delete
        from users
        where id = #{value}
    </delete>

    <select id="findTotal" resultType="java.lang.Long">
        select count(*)
        from users
    </select>
</mapper>

测试代码

public class CRUDTest {
    String resourcePath = "MyBatisConfig.xml";
    SqlSession sqlSession;
    UserMapper userDao;

    @BeforeEach
    public void init() throws IOException {
        InputStream in = Resources.getResourceAsStream(resourcePath);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
        sqlSession = sqlSessionFactory.openSession(true); // 設置自動提交事務
        userDao = sqlSession.getMapper(UserMapper.class);
    }

    @Test
    void selectAll() {
        List<User> users = userDao.selectAll();
        Assertions.assertNotEquals(users.size(), 0);
    }

    @Test
    void findByName() {
        List<User> byName = userDao.findByName("1");
        byName.forEach(System.out::println);
    }
    
    @Test
    void insert() {
        User cqq1 = new User("cqq", "0");
        boolean cqq = userDao.insert(cqq1);
        System.out.println(cqq1.getId());
        Assertions.assertTrue(cqq);
    }

    @Test
    void update() {
        User cqq1 = new User("cqq", "1");
        cqq1.setId(9);
        boolean cqq = userDao.update(cqq1);
        Assertions.assertTrue(cqq);
    }

    @Test
    void deleteById() {
        boolean b = userDao.deleteById(9);
        Assertions.assertTrue(b);
    }

    @Test
    void findTotal() {
        long total = userDao.findTotal();
        Assertions.assertNotEquals(total,0);
    }

}

核心配置

配置文件和映射文件这部分内容主要是随查随用。

核心对象

在使用 MyBatis 框架时,主要涉及两个核心对象:SqlSessionFactory 和 SqlSession。

SqlSessionFactory

可以认为 SqlSessionFactory 与数据库一一对应,一个 SqlSessionFactory 对应一个数据库实例。它的主要作用是创建 SqlSession。

SqlSessionFactory 的创建:SqlSessionFactoryBuilder 通过 xml 配置文件创建出一个具体的 SqlSessionFactory。

InputStream in = Resources.getResourceAsStream("配置文件路径");
SqlSessionFactory sf = new SqlSessionFactoryBuilder().build(in);

SqlSessionFactory 对象是线程安全的,它一旦被创建,在整个应用执行期间都会存在。如果我们多次地创建同一个数据库的 SqlSessionFactory,那么此数据库的资源将很容易被耗尽。为了解决此问题,通常每一个数据库都会只对应一个 SqlSessionFactory,所以在构建 SqlSessionFactory 实例时,建议使用单例模式。

SqlSession

可以将 SqlSession 当作是一个 JDBC 连接,可以用来执行 SQL 语句,里面封装了一些常用的操作数据库的方法,如查询、修改、删除等操作。

DefaultSqlSession

SqlSession 是 MyBatis 中的一个接口,它的子类 DefaultSqlSession 存在线程安全问题。而 SqlSessionManager 和 SqlSessionTemplate 是线程安全的。

SqlSessionManager 和 SqlSessionTemplate

SqlSessionManager 和 SqlSessionTemplate 是怎么保证 SqlSession 线程安全的呢?避免多个线程并发使用同一个 DefaultSqlSession 实例即可。

SqlSessionManager 内部通过维护一个 ThreadLocal 变量,记录一个与当前线程绑定的 SqlSession。当通过 SqlSessionFactory 创建 SqlSession 时就从这个 ThreadLocal 里取。

public class SqlSessionManager implements SqlSessionFactory, SqlSession {

    private final SqlSessionFactory sqlSessionFactory;
    private final SqlSession sqlSessionProxy;
    // ThreadLocal 用来记录一个与当前线程绑定的 SqlSession
    private final ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<>();

    private SqlSessionManager(SqlSessionFactory sqlSessionFactory) {
        this.sqlSessionFactory = sqlSessionFactory;
        // 对 sqlSessionFactory 对象进行代理,所有的操作都是由代理对象完成
        this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance(
            SqlSessionFactory.class.getClassLoader(),
            new Class[]{SqlSession.class},
            // 仔细看下 SqlSessionInterceptor 中的方法可以发现,是从 localSqlSession 中获取 Session
            new SqlSessionInterceptor());
    }

    @Override
    public SqlSession openSession(Connection connection) {
        return sqlSessionFactory.openSession(connection);
    }

    private class SqlSessionInterceptor implements InvocationHandler {
        // some code

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            final SqlSession sqlSession = SqlSessionManager.this.localSqlSession.get();
            if (sqlSession != null) {
                try {
                    return method.invoke(sqlSession, args);
                } catch (Throwable t) {
                    throw ExceptionUtil.unwrapThrowable(t);
                }
            } else {
                try (SqlSession autoSqlSession = openSession()) {
                    try {
                        final Object result = method.invoke(autoSqlSession, args);
                        autoSqlSession.commit();
                        return result;
                    } catch (Throwable t) {
                        autoSqlSession.rollback();
                        throw ExceptionUtil.unwrapThrowable(t);
                    }
                }
            }
        }
    }
}

SqlSessionTemplate 又是怎么保证线程安全的呢?和 SqlSessionManager 的做法类似。详细内容可参考这篇博客Spring+MyBatis源码解析之SqlSessionTemplate_henry_2016的博客-CSDN博客

public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
                          PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
    notNull(executorType, "Property 'executorType' is required");

    this.sqlSessionFactory = sqlSessionFactory;
    this.executorType = executorType;
    this.exceptionTranslator = exceptionTranslator;
    // 对 SqlSessionFactory 进行代理。
    this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
                                                         new Class[] { SqlSession.class }, new SqlSessionInterceptor());
}

private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 保证每次请求都是生成一个新的 sqlSession。(如果 ThreadLocal 中有,会返回 ThreadLocal 中的)
        SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
                                              SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
        try {
            Object result = method.invoke(sqlSession, args);
            //...
            return result;
        } catch (Throwable t) {
            //...
        } finally {
            // ...
        }
    }
}

/*
SqlSessionUtils#getSqlSession
TransactionSynchronizationManager获取当前线程threadLocal是否有SqlSessionHolder,
如果有就从SqlSessionHolder取出当前SqlSession,
如果当前线程threadLocal没有SqlSessionHolder,就从sessionFactory中创建一个SqlSession,
最后注册会话到当前线程threadLocal中。
*/
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
                                       PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
    notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);

    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
        return session;
    }

    LOGGER.debug(() -> "Creating a new SqlSession");
    session = sessionFactory.openSession(executorType);

    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

    return session;
}

SqlSessionManager 的获取

InputStream in = Resources.getResourceAsStream(resourcePath);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
SqlSessionManager sqlSessionManager = SqlSessionManager.newInstance(sqlSessionFactory);

SqlSessionTemplate 是 mybatis-spring 整合包中的。

SqlSession中的方法

SqlSession 对象中包含了很多方法,部分方法如下。

public interface SqlSession extends Closeable {
    // statement 表示的是 <select> 元素的id
    // 使用该方法后,会返回执行 SQL 语句查询结果的一条泛型对象
    <T> T selectOne(String statement);
	
    // parameter 是查询所需的参数。
    <T> T selectOne(String statement, Object parameter);

    <E> List<E> selectList(String statement);

    <E> List<E> selectList(String statement, Object parameter);

    // 返回一个和 SqlSession 绑定的 Mapper 接口的代理对象。
    <T> T getMapper(Class<T> type);
}

配置文件

主要元素

graph LR
c(configuration<根元素>)--子元素-->properties
c--子元素-->settings
c--子元素-->typeAliases
c--子元素-->typeHandlers
c--子元素-->objectFactory
c--子元素-->plugins
c--子元素-->environments--子元素-->environment
c--子元素-->databaseUdProvider
c--子元素-->mappers
environment--子元素-->transactionManager
environment--子元素-->dataSource

properties

<properties> 是一个配置属性的元素,该元素通常用于将内部的配置外在化,即通过外部的配置来动态地替换内部定义的属性。例如,数据库的连接等属性,就可以通过典型的 Java 属性文件中的配置来替换,具体方式如下。

# properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/xx
jdbc.username=root
jdbc.password=root

在 MyBatis 配置文件 mybatis-config.xml 中配置 <properties… /> 属性

<properties resource="db.properties" />

修改配置文件中数据库连接的信息,具体如下。

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

完成上述配置后,dataSource 中连接数据库的 4 个属性(driver、url、username 和 password)值将会由 db.properties 文件中对应的值来动态替换。这样就为配置提供了诸多灵活的选择。

settings

<settings> 元素主要用于改变 MyBatis 运行时的行为,例如开启二级缓存、开启延迟加载等。

typeAliases

<typeAliases> 元素用于为配置文件中的 Java 类型设置一个简短的别名。别名的设置与 XML 配置相关,可以避免写全类名。

<typeAliases>
    <!-- 为某个类配置别名 -->
	<typeAlias alias="user" type="xx.it.po.User"/>
</typeAliases>

使用自动扫描包来配置别名,这样会使用 Bean 的首字母小写的非限定类名来作为它的别名。如 xx.it.po.User 类对应的别名就是 user。如果类有注解的话,则别名为其注解的值。

<typeAliases>
    <!-- 使用自动扫描包来配置别名 -->
	<package  name="xx.it.po"/>
</typeAliases>
@Alias(value="user")
public class User{}

typeHandler

typeHandler(类型处理器)将预处理语句中传入的参数从 javaType(Java 类型)转换为 jdbcType(JDBC 类型),或者从数据库取出结果时将 jdbcType 转换为 javaType。

MyBatis 提供的一些默认的类型处理器。如果需要定义类型处理器可以通过实现 TypeHandler 接口或者继承 BaseTypeHandle 类来定义

注册类型处理器

<typeHandlers>
    <!-- 注册一个包中所有的 typeHandler -->
	<package name="cn.it.type" />
</typeHandlers>

objectFactory

每次 MyBatis 创建结果对象的新实例时,它都会使用一个对象工厂(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>

plugins

MyBatis 允许在已映射语句执行过程中的某一点,进行拦截调用。这种拦截调用是通过插件来实现的。<plugins> 元素的作用就是配置用户所开发的插件。

MyBatis 允许使用插件来拦截的方法调用有:

编写插件时,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。【MyBatis 功能拓展】

// ExamplePlugin.java
@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 {
        // implement pre processing if need
        Object returnObject = invocation.proceed();
        // implement post processing if need
        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>

environments

可以通过 <environments> 元素配置多种数据源,即配置多种数据库。

<environments default="development"> <!-- 默认使用1 -->
    <environment id="development">
        <!-- 使用 JDBC 事务管理 -->
        <transactionManager type="JDBC" />
        <!-- 配置数据源 -->
        <dataSource type="POOLED">
            <property name="driver" value="${driver}"/>
            <property name="url" value="${url}"/>
            <property name="username" value="${username}"/>
            <property name="password" value="${password}"/>
        </dataSource>
    </environment>
    <!-- 配置第二种数据源 -->
    <environment id="development2">
        <!-- 使用 JDBC 事务管理 -->
        <transactionManager type="JDBC" />
        <!-- 配置数据源 -->
        <dataSource type="POOLED">
            <property name="driver" value="${driver2}"/>
            <property name="url" value="${url2}"/>
            <property name="username" value="${username2}"/>
            <property name="password" value="${password2}"/>
        </dataSource>
    </environment>
</environments>

transactionManager,元素用于配置事务管理,它的 type 属性用于指定事务管理的方式,即使用哪种事务管理器

dataSource 元素用于配置数据源,它的 type 属性用于指定使用哪种数据源,如果使用的是外部的数据源,type 属性的值就是 dataSource 的类全名。

mappers

mappers 元素用于指定 MyBatis 映射文件的位置

使用类路径引入

<mappers>
	<mapper resource="cn/xx/xx/A.xml" />
</mappers>

使用本地文件路径引入

<mappers>
	<mapper url="file:///D:/cn/xx/xx/A.xml" />
</mappers>

使用接口类引入

<mappers>
	<mapper class="cn.xx.xx.AMapper" />
</mappers>

使用包名引入

<mappers>
	<mapper name="cn.xx.xx.dao" />
</mappers>

映射文件

graph LR
mapper-->select-->查询语句,可自定义参数,返回结果
mapper-->insert-->插入语句,返回整数
mapper-->update-->更新语句,返回整数
mapper-->delete-->删除语句,返回整数
mapper-->sql-->sql片段
mapper-->cache-->给定命名空间的缓存配置
mapper-->cache-ref-->其他命名空间缓存配置的引用
mapper-->resultMap-->用于描述如何从数据库结果集中加载对象

简单介绍下 resultMp。

resultMap 元素表示结果映射集,是 MyBatis 中最重要也是最强大的元素。它的主要作用是定义映射规则、级联的更新以及定义类型转化器等。

<!--resultMap的元素结构 -->
<resultMap type="" id="">
    <constructor>  <!-- 类在实例化时,用来注入结果到构造方法中-->
        <idArg/>   <!-- ID参数;标记结果作为ID-->
        <arg/>      <!-- 注入到构造方法的一个普通结果-->
    </constructor>
    <id/>           <!-- 用于表示哪个列是主键-->
    <result/>       <!-- 注入到字段或JavaBean属性的普通结果-->
    <association property="" />  <!-- 用于一对一关联 -->
    <collection property="" />   <!-- 用于一对多关联 -->
    <discriminator javaType="">  <!-- 使用结果值来决定使用哪个结果映射-->
        <case value="" />        <!-- 基于某些值的结果映射 -->
    </discriminator>
</resultMap>

默认情况下,MyBatis 程序在运行时会自动地将查询到的数据与需要返回的对象的属性进行匹配赋值(需要表中的列名与对象的属性名称完全一致)。然而实际开发时,数据表中的列和需要返回的对象的属性可能不会完全一致,这种情况下 MyBatis 是不会自动赋值的。此时,就可以使用 <resultMap> 元素进行处理。

<mapper namespace="com.xx.mapper.UserMapper">
    <resultMap type="com.xx.po.User" id="resultMap">
        <id property="id" column="t_id"/>
        <result property="name" column="t_name"/>
        <result property="age" column="t_age"/>
    </resultMap>
    <select id="findAllUser" resultMap="resultMap">
        select * from t_user
    </select>
</mapper>

参数取值★

基本用法

#{} 和 ${};#{} 等同于占位符 “?”,${} 相当于字符串拼接

#{} 表示一个占位符号。通过 #{} 可以实现 preparedStatement 向占位符中设置值,自动进行 Java 类型和 jdbc 类型转换,

#{} 可以有效防止 sql 注入。#{} 可以接收简单类型值或 pojo 属性值。 如果 parameterType 传输单个简单类型值,#{} 括号中可以是 value 或其它名称。

${} 表示拼接 sql 串。通过 ${} 可以将 parameterType 传入的内容拼接在 sql 中且不进行 jdbc 类型转换,${} 可以接收简单类型值或 pojo 属性值,如果 parameterType 传输单个简单类型值,${} 括号中只能是 value。

从源码上理解

class A{
    @Override
    public String handleToken(String content) {
      Object parameter = context.getBindings().get("_parameter");
      if (parameter == null) {
        context.getBindings().put("value", null);
      } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
        context.getBindings().put("value", parameter);
      }
      Object value = OgnlCache.getValue(content, context.getBindings());
      String srtValue = (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"
      checkInjection(srtValue);
      return srtValue;
    }
}

读取的 key 的名字就是 ”value”,所以我们在绑定参数时就只能叫 value 的名字

只有一个形式参数时

public User getOne(int id);
<select id="getOne" resultType="cn.pojo.User">
    select *
    from users
    where id = #{随便写什么}<!-- 随便写什么,最好见名知意 -->
</select>

有多个形参时:可以用注解取别名,方便拿对应的参数;也可以不取别名,按框架的规则进行取数据

// 有多个形参
public User getTwo(String name, String sex);

// 有多个形参,直接用注解为它取别名
public User getTwoAnnotation(@Param("findName") String name, @Param("findSex") String sex);
<!--
     直接用 name,sex 作为 #{} 的话,会报错。而且不同版本的 MyBatis 可用的 parameters 还不一样
	 因此最好还是用 @Param 注解指定名称。
     Caused by: org.apache.ibatis.binding.BindingException:
     Parameter 'id' not found
     Available parameters are [arg0, arg1, param1, param2]
-->
<select id="getTwo" resultType="cn.pojo.User">
    select *
    from users
    where name = #{arg0} and sex=#{arg1}
    <!-- 或者是 #{param1}, #{param2} -->
</select>

<!-- 也可以用注解指定别名 -->
<select id="getTwoAnnotation" resultType="cn.pojo.User">
    select *
    from users
    where name = #{findName} and sex = #{findSex}
</select>

总结

Map<String,Object> map = new HashMap<>();
map.put("1","传入的值1");
map.put("2","传入的值2");

如果我们不想这样做,想指定 key,那么我们如何指定封装时使用的 key?

使用注解 @Param 指定 map 的 key 的值!具体看看源码。

// 有多个形参,直接用注解为它取别名
public User getTwoAnnotation(@Param("findName") String name, @Param("findSex") String sex);
<select id="getTwoAnnotation" resultType="cn.pojo.User">
    select *
    from users
    where name = #{findName} and sex = #{findSex}
    <!-- name 和 findName 是一组映射关系,sex 和 findSex 又是一组映射关系 -->
</select>

用法小结

1)单个参数

2)多个参数

Map<String,Object> map = new HashMap<>()
map.put("1","传入的值1");
map.put("2","传入的值2");
// #{1},就是取出 map 中 key=1 的 value

3)@Param,为参数指定 key;命名参数;推荐这种做法。我们可以使用 @Param 注解告诉 MyBatis,封装参数 map 的时候别乱来,使用我们指定的 key

4)传入了Map:将多个要使用的参数封装起来,取值 #{key}

5)扩展:多个参数;自动封装 map

public XX method(@Param("id")Integer id, String empName,Employee employee);
Integer id ==> #{id}
String empName ==> #{param2}
Employee employee取出它的email==> #{param3.email}

无论传入什么参数都要能正确的取出值;

mybatis 两类取值方式的区别

源码分析

MapperMethod 类

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 {
                // 单条结果的查询方法 走这里。
                // converArgsToSqlCommandParam 做 sql 语句的参数映射
                // 将 args 中的内容封装为一个 map。
                // 跟进 converXX 方法进去看一下。
                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;
}

继续看 method.convertArgsToSqlCommandParam(args);【MapperMethod 类中】

public Object convertArgsToSqlCommandParam(Object[] args) {
    return paramNameResolver.getNamedParams(args);
}

点进去,进入了 ParamNameResolver 类

public Object getNamedParams(Object[] args) {
    // names存放的是key-value
    //			  key的取值是0,1,2的取值
    //			  value如果没有用注解的话,就是0,1,2的取值,如果用了注解,就是注解中的值。
    final int paramCount = names.size();
    if (args == null || paramCount == 0) {
        return null;
    } else if (!hasParamAnnotation && paramCount == 1) { // 只有一个参数且没有加注解,则直接返回这个参数的值。
        return args[names.firstKey()];
    } else {
        final Map<String, Object> param = new ParamMap<Object>();
        int i = 0;
        for (Map.Entry<Integer, String> entry : names.entrySet()) {
            // 封装成 map,names 中的内容是 args0, args1
            param.put(entry.getValue(), args[entry.getKey()]);
            // add generic param names (param1, param2, ...)
            // GENERIC_NAME_PREFIX = "param";
            final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
            // 再尝试将 param1 作为 key,args[x] 作为 value 存入 map
            // ensure not to overwrite parameter named with @Param
            if (!names.containsValue(genericParamName)) {
                param.put(genericParamName, args[entry.getKey()]);
            }
            i++;
        }
        return param;
    }
}

中级篇

返回值为map

常规情况

public Map<String, Object> getByIdReturnMap(int id);
// key是列名,value是字段对应的值。
id	name	email
1	a		afsdf
2	b		afsf
// 此处 id 就是列名,1,2 就是 value
// 这个如果查询出的是多条数据,value 应该会是一个集合。
<!-- pamramater一般可以不写。 -->
<select id="getByIdReturnMap" resultType="java.util.Map">
    select *
    from users
    where id = #{id}
</select>

POJO 情况

// key	 就是这个记录的主键
// value 就是这条记录封装好的对象
// 把查询记录的id的值作为key封装这个map(注解@MapKey)
@MapKey("id")
public Map<String, User> getAllUser();
<!-- 查询多个返回一个map,查询多个情况下,集合里面写元素类型 不过 IDEA 中安装的 MyBatis 插件居然会报错,说返回值类型不正确 -->
<select id="getAllUser" resultType="User">
    select * from users
</select>

自定义结果集

当 JavaBean 中的字段名和数据库表中的列名并非完全一致,且驼峰规则无效时,可以使用自定义 ResultType,将数据库中的字段和 JavaBean 中的进行一一对应。

type:指定为哪个 JavaBean 自定义封装规则;全类名。

id:唯一标识符,让别名在后面引用

<resultMap type="com.xx.xx.Cat" id="mycat">
    <!--
        column="id":指定哪一列是主键列(数据库中的字段名)
        property="":指定cat的哪个属性封装id这一列数据(JavaBean 中的字段名)
	-->
    <!-- 主键列 -->
	<id property="pojoid" column="id">
    <!-- 普通列 -->
    <result property="" column=""></result>
</resultMap>
    
<!-- resultMap="mycat":查出数据封装结果的时候,使用mycat自定义的规则。-->
<select id="getAllCat" resultMap="mycat">
	select * from cat where id=#{id} 
</select>

动态SQL

动态 SQL 是 MyBatis 的强大特性之一,MyBatis 3 采用 OGNL 表达式来实现的动态 SQL。

元素 说明
<if> 判断语句,用于单条件分支判断
<choose>(<when><otherwise>) 相当于 Java 中得 switch…case…default 语句,
用于多分支判断
<where>、<trim>、<set> 辅助元素,处理 SQL 拼接和特殊字符问题
<foreach> 循环语句,常用于 in 语句等列举条件中
<bind> 从 OGBL 表达式中创建一个变量,将其绑定到上下文,
常用于模糊查询得 sql 中。

标签学习

if 标签

用于实现某些简单的条件选择。

<select id="xxx" resultType='User'>
	select * from t_user where 1=1
    <if test=" name!=null and name!='' ">
    	and name like concat('%d',#{name},'%')
    </if>
    <if test=" jobs!=null and jobs!='' ">
    	and jobs=#{jobs}
    </if>
</select>

<choose> (<when> <otherwise>)

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

举例:查询条件为,传入了 “title” 就按 “title” 查找,传入了 “author” 就按 “author” 查找的情形。若两者都没有传入,就返回标记为 featured 的 BLOG

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

where 标签

where 标签可以帮我们去除掉前面的 and

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

trim 标签

<!--
	prefix=""	前缀:为我们下面的sql整体添加一个前缀
	prefixOverrides	取出整体字符串前面多余的字符
	suffix	为整体添加一个后缀
	suffixOverrides	后面哪个多了可以去掉
-->
<trim prefix="where" prefixOverrides="and">
	<if test="id!=null">
    	id > #{id} and
    </if>
    <!--
		有些字符是xml的标记,所以需要转义
	-->
    <if test="name != null &amp;&amp; !name.equals(&quot;&quot;)">
    	teacherName like #{name} and
    </if>
    <if test="birth != null">
    	birth_date &lt; #{birth} and
    </if>
</trim>

foreach

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

提示:我们可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象作为集合参数传递给 foreach。

select xxxxx where id in
<!--
	collection	指定要遍历的集合的key
	close		以什么结束
	open		以什么开始
	index		索引
		如果遍历的是一个list,index指定的变量保存了当前索引
		如果遍历的是一个map,index 指定的变量就是保存了当前遍历元素的key
	item		变量名
	separator	每次遍历元素的分隔符
	(#{id_item},#{id_item},#{id_item})
	这里collection可以用ids 是因为 用了@Param("ids")为key重新命名了。没有这个的话,List类型默认用的key是list
-->
<foreach collection="ids" item="id_item" index="id_index" separator="," open="(" close=")">
    #{id_item}
</foreach>

抽取 sql 片段

<sql id="selectSql">
	select xxx sfaf
</sql>
<select id="xx" xx>
	<include refid="selectSql"></include>
    where id=#{id}
</select>

<!-- 传递参数 -->
<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </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>

完整示例

Java代码

public interface IUserDao {

    // 删除 -- 测试事务
    Integer delete(Integer id);

    // 查询所有 -- 查看事务是否成功提交
    List<UserVO> findAll();

    // 条件查询 -- 动态 SQL 之 if
    List<UserVO> findCondition(UserVO vo);

    // 条件查询 -- 动态 SQL 之 where
    List<UserVO> findCondition2(UserVO vo);

    // 新增 -- 动态SQL 之 set
    boolean update(UserVO vo);

    // 循环新增 -- 动态 SQL 之 foreach
    Long insertBatch(List<UserVO> vos);

    // 循环删除 -- 动态 SQL之 foreach 数组
    Long deleteBatch(Integer[] ids);

    // 循环删除 -- 动态 SQL 之 foreach 集合
    Long deleteBatch(List<Integer> lists);
}

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.bbxx.dao.IUserDao">
    <!-- 配置查询所有操作 -->
    <select id="findAll" resultType="UserVO">
        select *
        from users
    </select>

    <!--动态SQL if-->
    <select id="findCondition" resultType="UserVO">
        select * from users where 1 = 1
        <if test="id!=null">
            and id=#{id}
        </if>
        <if test="username!=null">
            and username like concat("%",#{username},"%")
        </if>
        <if test="birthday!=null">
            and birthday=#{birthday}
        </if>
    </select>
    
    <!--动态SQL where 去除前面多余的and-->
    <select id="findCondition2" resultType="UserVO">
        select * from users
        <where>
            <if test="id!=null">
                and id=#{id}
            </if>
            <if test="username!=null">
                and username like concat("%",#{username},"%")
            </if>
            <if test="birthday!=null">
                and birthday=#{birthday}
            </if>
        </where>
    </select>
    
    <!--测试事务-->
    <delete id="delete">
        delete
        from users
        where id = #{value}
    </delete>

    <!--动态SQL测试set 去除后面多余的逗号-->
    <update id="update" parameterType="UserVO">
        update users
        <set>
            <if test="username!=null">username=#{username},</if>
            <if test="birthday!=null">,birthday=#{birthday}</if>
        </set>
        where id=#{id}
    </update>

    <!-- 循环新增 ==> 动态SQL 之 foreach 使用ArrayList集合,collection中写的是参数的类型!这里是list集合 -->
    <insert id="insertBatch" parameterType="UserVO">
        insert into users(username,birthday,address)
        values
        <foreach collection="list" item="data" separator=",">
            (#{data.username},#{data.birthday},#{data.address})
        </foreach>
    </insert>

    <!--
    循环删除 ==> 动态SQL 之 动态SQL之 foreach 数组
    Map的话,查文档得知 index是key item是value
    -->
    <insert id="deleteBatch">
        delete from users where id in
        <foreach collection="array" item="data" open="(" separator="," close=")">
            #{data}
        </foreach>
    </insert>
</mapper>

单元测试代码

public class Demo {
    InputStream in = null;
    SqlSessionFactoryBuilder builder = null;
    SqlSessionFactory factory = null;
    SqlSession sqlSession = null;
    IUserDao mapper = null;

    @Before
    public void init() throws IOException {
        in = Resources.getResourceAsStream("SqlConfig.xml");
        builder = new SqlSessionFactoryBuilder();
        factory = builder.build(in);
        sqlSession = factory.openSession();
        mapper = sqlSession.getMapper(IUserDao.class);
    }

    @Test
    public void findAll() {
        List<UserVO> all = mapper.findAll();
    }

    // 测试事务
    @Test
    public void affairs() {
        sqlSession.commit(false);
        Integer delete = mapper.delete(5);
        System.out.println(delete);
        sqlSession.commit();// 提交事务后数据才会真的删除。 // 对比数据库中的信息查看即可。
    }

    // 测试动态SQL ==> if
    @Test
    public void testIf() {
        // 查出四条数据
        List<UserVO> condition = mapper.findCondition(new UserVO(null, null, null));
        // 查出三条数据
        List<UserVO> o = mapper.findCondition(new UserVO(null, "o", null));
    }

    // 测试where,会去掉前面多余的and
    @Test
    public void testWhere() {
        // 查出四条数据
        List<UserVO> condition = mapper.findCondition2(new UserVO(null, null, null));
        // 查出三条数据
        List<UserVO> o = mapper.findCondition2(new UserVO(null, "o", null));
    }

    // 测试set,会去掉末尾多余的 逗号(,)
    @Test
    public void testSet() {
        boolean kkx = mapper.update(new UserVO(2, "kkx", null));
        sqlSession.commit();
    }

    // 批量新增
    @Test
    public void testForeach1() {
        ArrayList<UserVO> userVOS = new ArrayList<>();
        userVOS.add(new UserVO(null, "001", "1988-11-11"));
        userVOS.add(new UserVO(null, "002", "1988-02-01"));
        userVOS.add(new UserVO(null, "003", "1999-11-11"));
        userVOS.add(new UserVO(null, "004", "1995-02-21"));
        Long aLong = mapper.insertBatch(userVOS);
        System.out.println(aLong);
        sqlSession.commit();
        findAll();
    }

    // 循环删除
    @Test
    public void testForeach2() {
        Integer[] ids = {10, 11, 12, 13};
        Long aLong = mapper.deleteBatch(ids);
        System.out.println(aLong);
        sqlSession.commit();
    }

    @After
    public void destroy() throws IOException {
        sqlSession.close();
        in.close();
    }
}

关联查询

关联查询是 MyBatis 针对多表操作提供的关联映射,通过关联映射就可以很好地处理对象与对象之间的关联关系。

在关系型数据库中,多表之间存在着三种关联关系,分别为一对一、一对多和多对多。

class A{
    B b; // 一对一关系
}

class A{
    List<B> b;// 一对多关系
}

// 多对多关系
class A{
    List<B> b;
}
class B{
    List<A> a;
}

一对一查询

resultMap 元素中,包含了一个 association 子元素,通过该元素可以处理一对一关联关系的。MyBatis 中一对一的查询方式有两种:

association:只是表示对象,它具有以下属性

准备数据

数据库表信息

USE mybatis;

# 创建一个名称为tb_idcard的表
CREATE TABLE  tb_idcard(
    id INT PRIMARY KEY AUTO_INCREMENT,
    CODE VARCHAR(18)
);
# 插入两条数据
INSERT INTO tb_idcard(CODE) VALUES('152221198711020624');
INSERT INTO tb_idcard(CODE) VALUES('152201199008150317');

# 创建一个名称为tb_person的表
CREATE TABLE  tb_person(
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(32),
    age INT,
    sex VARCHAR(8),
    card_id INT UNIQUE,
    FOREIGN KEY(card_id) REFERENCES tb_idcard(id)
);
# 插入两条数据
INSERT INTO tb_person(name, age, sex, card_id) VALUES('Rose',29, '女',1);
INSERT INTO tb_person(name, age, sex, card_id) VALUES('tom',27, '男',2);

POJO 代码

public class IdCard {
    private Integer id;
    private String code;
	// some code
}

public class Person {
    private Integer id;
    private String name;
    private Integer age;
    private String sex;
    private IdCard card;
	// some code
}

Mapper 代码

public interface IdCardMapper {
    IdCard findCardById(int id);
}

// 一对一查询
public interface PersonMapper {
    Person findPersonById(int id);
}

嵌套子查询方式

<mapper namespace="cn.mapper.IdCardMapper">
    <select id="findCardById" resultType="cn.pojo.IdCard">
        select *
        from mybatis.tb_idcard
        where id = #{id}
    </select>
</mapper>

<!-- 一对一关联查询 -->
<mapper namespace="cn.mapper.PersonMapper">
    <select id="findPersonById" resultMap="findPersonByIdResult">
        select *
        from mybatis.tb_person
        where id = #{id}
    </select>
	
    <resultMap id="findPersonByIdResult" type="Person">
        <id column="id" property="id"/>
        <result column="name" property="name"/>
        <result column="age" property="age"/>
        <result column="sex" property="sex"/>
        <!-- 一对一关联查询的方式,嵌套查询的方式 -->
        <association property="card" 
                     column="card_id" 
                     javaType="IdCard" 
                     select="cn.mapper.IdCardMapper.findCardById"/>
    </resultMap>
</mapper>

测试代码

package quickstart;

public class TestAssociation {
    String resourcePath = "MyBatisConfig.xml";
    SqlSession sqlSession;
    PersonMapper dao;

    @BeforeEach
    public void init() throws IOException {
        InputStream in = Resources.getResourceAsStream(resourcePath);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
        sqlSession = sqlSessionFactory.openSession(true); // 設置自動提交事務
        dao = sqlSession.getMapper(PersonMapper.class);
    }

    @Test
    public void f1() {
        Person personById = dao.findPersonById(1);
        System.out.println(personById);
    }
}

嵌套结果方式

使用嵌套查询的方式比较简单,但是 MyBatis 嵌套查询的方式要执行多条 SQL 语句,这对于大型数据集合和列表展示不是很好,因为这样可能会导致成百上千条关联的 SQL 语句被执行,从而极大地消耗数据库性能并且会降低查询效率。我们可以使用 MyBatis 提供的嵌套结果方式,来进行关联查询。

<select id="findPersonById2" resultMap="findPersonByIdResult2">
    select p.*, idcard.code
    from mybatis.tb_person as p,
    mybatis.tb_idcard as idcard
    where p.card_id = idcard.id
    and p.id = #{id}
</select>

<resultMap id="findPersonByIdResult2" type="Person">
    <id column="id" property="id"/>
    <result column="name" property="name"/>
    <result column="age" property="age"/>
    <result column="sex" property="sex"/>
    
    <association property="card" javaType="IdCard">
        <id column="card_id" property="id"/>
        <result column="code" property="code"/>
    </association>
</resultMap>

注意:association 中的列名(column) 不要和 resultMap 中的重复了,否则在对 Java POJO 数据进行赋值时会出现问题。

一对多查询

MyBatis 通过 collection 来实现一对多关联查询。collection 元素与 association 元素基本相同,但 collection 保护一个特殊的属性 ofType。ofType 属性与 javaType 属性对应,它用于指定实体对象中集合类属性所包含的元素类型。

collection:定义集合元素的封装

<!-- 这个property应该是用注解标记了,使用keys作为property -->
<collection property="keys" ofType="com.xx.Key">
	<id></id>
    <result></result>
</collection>

准备数据

数据库表

use mybatis;

# 创建一个名称为tb_user的表
CREATE TABLE tb_user (
    id int(32) PRIMARY KEY AUTO_INCREMENT,
    username varchar(32),
    address varchar(256)
);
# 插入3条数据
INSERT INTO tb_user VALUES ('1', '詹姆斯', '克利夫兰');
INSERT INTO tb_user VALUES ('2', '科比', '洛杉矶');
INSERT INTO tb_user VALUES ('3', '保罗', '洛杉矶');

# 创建一个名称为tb_orders的表
CREATE TABLE tb_orders (
    id int(32) PRIMARY KEY AUTO_INCREMENT,
    number varchar(32) NOT NULL,
    user_id int(32) NOT NULL,
    FOREIGN KEY(user_id) REFERENCES tb_user(id)
);
# 插入3条数据
INSERT INTO tb_orders VALUES ('1', '1000011', '1');
INSERT INTO tb_orders VALUES ('2', '1000012', '1');
INSERT INTO tb_orders VALUES ('3', '1000013', '2');

POJO 代码

public class Orders {
    private Integer id;
    private String number;
	// some code
}

public class Users {
    private Integer id;
    private String username;
    private String address;
    private List<Orders> ordersList;
	// some code
}

Mapper 代码

public interface UsersMapper {
    Users findById(int id);
}

嵌套结果方式

<mapper namespace="cn.mapper.UsersMapper">
    <select id="findById" resultType="cn.pojo.Users" resultMap="findByIdMap">
        select u.*, o.id as order_id, o.number as order_number
        from tb_user as u,
        tb_orders as o
        where u.id = o.user_id
        and u.id = #{id}
    </select>

    <resultMap id="findByIdMap" type="Users">
        <id property="id" column="id"/>
        <id property="username" column="username"/>
        <id property="address" column="address"/>
        <!-- ofType 表示集合中元素的类型 -->
        <collection property="ordersList" ofType="Orders">
            <id property="id" column="order_id"/>
            <result property="number" column="order_number"/>
        </collection>
    </resultMap>
</mapper>

测试代码

public class TestAssociationOne2Mu {
    String resourcePath = "MyBatisConfig.xml";
    SqlSession sqlSession;
    UsersMapper dao;

    @BeforeEach
    public void init() throws IOException {
        InputStream in = Resources.getResourceAsStream(resourcePath);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
        sqlSession = sqlSessionFactory.openSession(true); // 設置自動提交事務
        dao = sqlSession.getMapper(UsersMapper.class);
    }

    @Test
    public void f1() {
        Users byId = dao.findById(1);
        System.out.println(byId);
    }

}

JavaType 和 OfType:JavaTypeofType 都是用来指定对象类型的,但是 JavaType 是用来指定 pojo 中属性的类型,而 ofType 指定的是映射到 list 集合属性中 pojo 的类型。

多对多查询

典型的多对多例子:一个订单可以包含多种商品,而一种商品又可以属于多个订单,订单和商品就属于多对多的关联关系。其实只是 SQL 语句和一对多的不一样,MyBatis 中用到的标签还是和一对多一样的。

而多对多查询的意思是,我们如何从这个多对多的关系中查询到我们想要的数据:如查询某个订单表的信息,包括订单表中涉及到的所有的商品信息。

在数据库中,多对多的关联关系通常使用一个中间表来维护,中间表中的订单 id 作为外键参照订单表的 id,商品 id 作为外键参照商品表的 id。

graph LR
订单表---中间表---商品表

在 MyBatis 中我们可以通过这几种方式来实现多对多查询。

graph LR
订单表-->一对多查询-->拿到查询到的商品-->对商品进行一对多查询

一个不错的博客

准备数据

假定我们的需求是,查询某个订单的信息,订单中涉及到的商品信息也都要查询出来。

数据库表

# 创建一个名称为tb_product的表
CREATE TABLE tb_product
(
    id    INT(32) PRIMARY KEY AUTO_INCREMENT,
    NAME  VARCHAR(32),
    price DOUBLE
);
# 插入3条数据
INSERT INTO tb_product
VALUES ('1', 'Java基础入门', '44.5');
INSERT INTO tb_product
VALUES ('2', 'Java Web程序开发入门', '38.5');
INSERT INTO tb_product
VALUES ('3', 'SSM框架整合实战', '50');

# 创建一个名称为tb_ordersitem的中间表
CREATE TABLE tb_ordersitem
(
    id         INT(32) PRIMARY KEY AUTO_INCREMENT,
    orders_id  INT(32),
    product_id INT(32),
    FOREIGN KEY (orders_id) REFERENCES tb_orders (id),
    FOREIGN KEY (product_id) REFERENCES tb_product (id)
);
# 插入3条数据
INSERT INTO tb_ordersitem
VALUES ('1', '1', '1');
INSERT INTO tb_ordersitem
VALUES ('2', '1', '3');
INSERT INTO tb_ordersitem
VALUES ('3', '3', '3');

# 创建一个名称为tb_orders的表,如果之前没有创建的话
CREATE TABLE tb_orders (
    id int(32) PRIMARY KEY AUTO_INCREMENT,
    number varchar(32) NOT NULL,
    user_id int(32) NOT NULL,
    FOREIGN KEY(user_id) REFERENCES tb_user(id)
);
# 插入3条数据
INSERT INTO tb_orders VALUES ('1', '1000011', '1');
INSERT INTO tb_orders VALUES ('2', '1000012', '1');
INSERT INTO tb_orders VALUES ('3', '1000013', '2');

POJO 代码

// some code
public class Orders {
    private Integer id;
    private String number;
    private List<Product> productList;
}

public class Product {
    private Integer id;
    private String name;
    private Double price;
    private List<Orders> orders;
	// some code
}

Mapper 代码

public interface OrderMapper {
    Orders findOrdersWithProduct(int id);
}

public interface ProductMapper {
    Product findProductById(int id);
}

嵌套子查询

<mapper namespace="cn.mapper.OrderMapper">
    <select id="findOrdersWithProduct" resultType="cn.pojo.Orders" resultMap="findOrdersWithProductMap">
        select * from tb_orders where id=#{id}
    </select>

    <resultMap id="findOrdersWithProductMap" type="Orders">
        <id column="id" property="id"/>
        <result column="number" property="number"/>
        <!-- 
			会将 id 这列 作为参数传递给 collection 
			中的查询语句findProductById 
		-->
        <collection property="productList" 
                    column="id" 
                    ofType="Product" 
                    select="cn.mapper.ProductMapper.findProductById"/>
    </resultMap>
</mapper>

<mapper namespace="cn.mapper.ProductMapper">
    <select id="findProductById" resultType="cn.pojo.Product">
        select *
        from tb_product
        where id in (
            select product_id
            from tb_ordersitem
            where orders_id = #{id}
        )
    </select>
</mapper>

测试代码

public class TestAssociationMu2Mu {
    String resourcePath = "MyBatisConfig.xml";
    SqlSession sqlSession;
    OrderMapper dao;

    @BeforeEach
    public void init() throws IOException {
        InputStream in = Resources.getResourceAsStream(resourcePath);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
        sqlSession = sqlSessionFactory.openSession(true); // 設置自動提交事務
        dao = sqlSession.getMapper(OrderMapper.class);
    }

    @Test
    public void f1(){
        Orders ordersWithProduct = dao.findOrdersWithProduct(1);
        System.out.println(ordersWithProduct);

    }
}

/*
Orders{id=1, number='1000011'
[[
Product{id=1, name='Java基础入门', price=44.5, orders=null}, 
Product{id=3, name='SSM框架整合实战', price=50.0, orders=null}
]]}
*/

嵌套结果方式

就是通过一条 SQL 查询到所有的数据

<select id="findOrdersWithProduct" resultType="cn.pojo.Orders" resultMap="findOrdersWithProductMap">
    select o.*, p.id as pid, p.NAME, p.price
    from tb_orders o,
    tb_product p,
    tb_ordersitem item
    where item.orders_id = o.id
    and item.product_id = p.id
    and o.id = #{id}
</select>

<resultMap id="findOrdersWithProductMap" type="Orders">
    <id column="id" property="id"/>
    <result column="number" property="number"/>
    <collection property="productList" ofType="Product">
        <id property="id" column="pid"/>
        <result property="name" column="name"/>
        <result property="price" column="price"/>
    </collection>
</resultMap>
<!-- 
查询出的结果和上面的一样
Orders{id=1, number='1000011'
[[
Product{id=1, name='Java基础入门', price=44.5, orders=null}, 
Product{id=3, name='SSM框架整合实战', price=50.0, orders=null}
]]}
-->

延迟加载

分步查询

<select id="getXX" resultMap="mykey02">
	select * from key where id=#{id}
</select>
<!--
	告诉 mybatis 自己去调用一个查询
	select:指定一个查询sql的唯一标识;mybatis自动调用指定的sql将查询出的lock封装起来
			public Lock getLockByIdSimple(Integer id); 需要传入锁子id
	column:指定将哪一列的数据传递过去。
			getLockByIdSimple(Integer id)不是需要一个查询条件 id吗,column就是把指定列的数据传递过去。
-->
<resultMap type="com.xx.key" id="mykey02">
    <id></id>
    <result></result>
    <association property="lock" 
                 select="getLockByIdSimple" 
                 column="lockid"/>
</resultMap>

分布查询,两个查询都会执行,即便没有用到第二个查询的数据。这样严重浪费了数据库的性能。我们可以采用按需加载,需要的时候再去查询:全局开启按需加载策略!

延迟加载

使用嵌套查询方式进行关联查询映射时,使用延迟加载在一定程度上可以降低运行消耗并提高查询效率。MyBatis 默认没有开启延迟加载,可以在核心配置文件 mybatis-config.xml 中的 <settings> 元素内进行全局配置,具体配置方式如下

<settings>
    <!-- 打开延迟加载的开关 -->
    <setting name="lazyLoadingEnabled" value="true" />
    <!-- 将积极加载改为消息加载,即按需加载 -->
    <setting name="aggressiveLazyLoading" value="false"/>
</settings>

如果只是个别的 SQL 需要开启延迟加载,则直接在需要延迟加载的 SQL 配置中将 fetchType 改为 lazy

<select id="findPersonById" resultMap="findPersonByIdResult">
    select *
    from mybatis.tb_person
    where id = #{id}
</select>    
<resultMap id="findPersonByIdResult" type="Person">
    <id column="id" property="id"/>
    <result column="name" property="name"/>
    <result column="age" property="age"/>
    <result column="sex" property="sex"/>
    <!-- Mapper xml文件中按需加载的写法 -->
    <!-- 将 fetchType 改为 lazy,默认是 eager 指定属性后,将在映射中忽略全局配置参数-->
    <association property="card" fetchType="lazy" 
                 column="card_id" javaType="IdCard" 
                 select="cn.mapper.IdCardMapper.findCardById"/>
</resultMap>

总结

如果 POJO 字段的名称和数据库的名称不对应则采用

<resultMap type="类型 如xx类" id="标识符">
	<id column="数据库字段名" property="代码中的字段名"></id> // 主键
    <result column="数据库字段名" property="代码中的字段名"></result> // 普通字段
</resultMap>

如果是一对一采用

<resultMap type="类型 如xx类" id="标识符">
	<id column="数据库字段名" property="代码中的字段名"></id> // 主键
    <result column="数据库字段名" property="代码中的字段名"></result> // 普通字段
    <association property="代码字段名" javaType="POJO属性的类型">
        <id column="数据库字段名" property="代码中的字段名"></id> 
    	<result column="数据库字段名" property="代码中的字段名"></result>
    </association>
</resultMap>

如果是一对多采用

<resultMap type="类型 如xx类" id="标识符">
	<id column="数据库字段名" property="代码中的字段名"></id> // 主键
    <result column="数据库字段名" property="代码中的字段名"></result> // 普通字段
    <collection property="代码字段名" ofType="指定的是映射到list集合属性中pojo的类型。">
        <id column="数据库字段名" property="代码中的字段名"></id> 
    	<result column="数据库字段名" property="代码中的字段名"></result>
    </collection>
</resultMap>

延迟加载指定属性

<association fetchType="lazy" property="card"
             column="card_id" javaType="IdCard" 
             select="cn.mapper.IdCardMapper.findCardById"/>

缓存机制

暂时存储一些数据;加快系统的查询速度

MyBatis 缓存机制:Map;能保存查询出的一些数据;

一级缓存失效

一级缓存是 SqlSession 级别的缓存,只要 SqlSession 没有 flush 或 close,它就存在!

<mapper namespace="com.itheima.dao.IUserDao">
    <!-- 根据 id 查询 -->
    <select id="findById" resultType="UsEr" parameterType="int" useCache="true">
        select * from user where id = #{uid}
    </select>
</mapper>

请自行编码验证!

看下 MyBatis 缓存部分的源码就知道,这个缓存机制真的很弱鸡。

一级缓存是 SqlSession 级别的缓存

1)不同的 sqlSession,使用不同的一级缓存

​ 只有在同一个 sqlSession 期间查询到的数据会保存在这个 sqlSession 的缓存中。

​ 下次使用这个 sqlSession 查询会从缓存中拿

2)同一个方法,不同的参数,由于可能之前没查询过,所以还有发新的 sql;

3)在这个 sqlSession 期间执行任何一次增删改操作,增删改都会把缓存清空。(不管你改的是不是我的数据,我都清空)

4)手动清空缓存 openSession.clearCache()

MyBatis 缓存是在 Cache 类 - org.apache.ibatis.cache - PerpetualCache变量中

public class PerpetualCache implements Cache {

  private final String id;
	
  // 所谓的缓存其实就是一个Map
  private Map<Object, Object> cache = new HashMap<Object, Object>();

  // some method
}

二级缓存失效

全局作用域缓存

二级缓存默认不开启,需要手动配置

MyBatis 提供二级缓存的接口及其实现,缓存实现要求 POJO 实现 Serializable 接口

整合Spring

除了导入 Spring 和 MyBatis 的包之外,还需要导入 MyBatis 和 Spring 的整合包。

思路

整合的思路比较简单,就是将 MyBatis 的对象交由 Spring 进行关联。

整合方式

传统 Dao 方式开发整合

编写 Dao 接口和 Dao 接口的实现类

public interface CDao{
    public XX find(int id);
}

public class CDaoImpl extends SqlSessionDaoSupport implements CDao{
    public XX find(int id){
        return this.getSqlSession().selectOne("命名空间",id);
    }
}

CDaoImpl 类继承了 SqlSessionDaoSupport 类,并实现了 CDao 接口。其中,SqlSessionDaoSupport 类在使用时需要一个 SqlSessionFactory 或一个 SqlSessionTemplate 对象,所以需要通过 Spring 给 SqlSessionDaoSupport 类的子类对象注入一个 SqlSessionFactory 或 SqlSessionTemplate。这样,在子类中就能通过调用 SqlSessionDaoSupport 类的 getSqlSession() 方法来获取 SqlSession 对象,并使用 SqlSession 对象中的方法了。

Mapper 接口方式

接口的实现类通过动态代理生成。无需用户编写。

基于 MapperFactoryBean 的整合

基于 MapperScannerConfigurer 的整合

原理概述

MyBatis 可自己写 Dao 实现类也可不写实现类。推荐不写实现类。不写实现类采用的是基于代理的 CRUD 操作。

MyBatis 用到了 OGNL 表达式

连接池及事务控制

连接池介绍

相当于实现分配好很多数据库连接在容器中,需要用时从容器中拿连接,不需要用时把连接归还到容器中,可避免频繁的打开关闭,节约资源(打开关闭连接很消耗资源)。

优点:减少获取连接所消耗的时间

缺点:初始化连接时速度慢

连接池

提供三种方式

事务

事务的四大特性 ACID

不考虑隔离性会产生的 3 个问题

解决办法:四种隔离级别

它是通过 sqlsession 对象的 commit 方法和 rollback 方法实现事务的提交和回滚

MyBatis生成Mapper

测试语句 select * from users where id=4;

方法代码 List<UserVO> findByCondition(UserVO vo);

MapperRegister 类的 getMapper 方法

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    // 从 hashmap 中看是否有此类型的,有就可以创建,无就抛出异常。
    // knownMappers 中以 Class 为 key,MapperProxyFactory 为 value 存储数据。
    // 因为可能不是每个 Mapper 都会被使用,因此 value 存储的是 Factory
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
        throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
        // 通过 sqlSession 创建代理对象<==查看源码
        return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
        throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
}

继续看 mapperProxyFactory.newInstance(sqlSession); 位于 MapperProxyFactory 类中

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

点进 new MapperProxy<T>(sqlSession, mapperInterface, methodCache) 一看

public class MapperProxy<T> implements InvocationHandler, Serializable {

  private static final long serialVersionUID = -6424540398559729838L;
  private final SqlSession sqlSession;
  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache;

  public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
    this.sqlSession = sqlSession;
    this.mapperInterface = mapperInterface;
    this.methodCache = methodCache;
  }
    // 当我们执行代理对象.method的时候会执行到这个方法
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            if (Object.class.equals(method.getDeclaringClass())) {
                return method.invoke(this, args);
            } else if (isDefaultMethod(method)) {
                return invokeDefaultMethod(proxy, method, args);
            }
        } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
        }
        // 查看缓存有没有,没有就添加,再从缓存中拿数据。
        final MapperMethod mapperMethod = cachedMapperMethod(method);
        // 这里 执行的sql语句。
        return mapperMethod.execute(sqlSession, args);
    }
  // ...
}

点击 mapperMethod.excute 方法一看。(MapperMethod 方法中的)

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;
            // 判断多条记录是 根据返回值来的?当前sql之能查询到一条数据,
            // returnsMany=True,应该是按返回值的类型来的。
            } 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);
            }
            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;
}

executeForMany 方法

private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
    List<E> result;
    Object param = method.convertArgsToSqlCommandParam(args);
    if (method.hasRowBounds()) {
        RowBounds rowBounds = method.extractRowBounds(args);
        result = sqlSession.<E>selectList(command.getName(), param, rowBounds);
    } else {
        result = sqlSession.<E>selectList(command.getName(), param);
    }
    // issue #510 Collections & arrays support
    if (!method.getReturnType().isAssignableFrom(result.getClass())) {
        if (method.getReturnType().isArray()) {
            return convertToArray(result);
        } else {
            return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
        }
    }
    return result;
}

点进 selectList 方法

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
        // 这段看不懂,没事
        MappedStatement ms = configuration.getMappedStatement(statement);
        // 这个是关键
        return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

点进 query 方法

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // 获得解析后的SQL语句
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}