# 《Mybatis 手撸专栏》第12章:完善ORM框架,增删改查操作

作者:小傅哥
博客:https://bugstack.cn (opens new window)
原文:https://mp.weixin.qq.com/s/_-LYA6uZhB7GQ-50C0IZYA (opens new window)

沉淀、分享、成长,让自己和他人都能有所收获!😄

# 一、前言

时间再多一件事情也不可能做的完美,但总有时间做完一件事情!

在我们的生活中,要经历很多重要的阶段,包括;高考、求婚、面试、述职,在所有事情发生之前,我们都在做着大量的准备,甚至像为了高考的这样的时间点,要准备好几年。可即使有这样的大量的准备时间,我们几乎也没有办法把最终的结果做到完美,只能是把结果做完。没有遗憾的人生,才是遗憾!

其实我们的系统设计开发也是一样的,系统越做越复杂,功能越做越多,但人的智力是有上限的,永远无法完全评估未发生的事情。所以对于一个复杂的分布式系统,我们几乎永远不可能找到并修复所有的 bug,有时候解决方法也不是完全找出所有问题并消灭,而是能容忍部分小问题,并在这些问题发生时可以自动恢复,做到最终补偿处理。而这样的设计也称为高可用和弹性设计。

# 二、目标

前面各个章节的内容,渐进式的逐步为我们实现了一个基本的框架结构,能满足我们对一个 DAO 方法的查询操作,以及处理对应的参数和返回结果。

那么目前这个框架中所提供的 SQL 处理仅有一个 select 查询操作,还没有其他我们日常常用的 insert、update、delete,以及 select 查询返回的集合类型数据。

其实这一部分新增处理 SQL 的内容,也就是在 SqlSession 需要定义新的接口,通知让这些接口被映射器类方法 MapperMethod 进行调用处理。如图 12-1 所示 SqlSession 定义操作SQL方法

图 12-1  SqlSession 定义操作SQL方法

  • 结合着我们目前框架的开发结构,对于扩展 insert/update/delete 这部分功能来说,并不会太复杂的。因为从 XML 对方法的解析、参数的处理、结果的封装,都已经是成型的结构。而我们只是对把这部分新增逻辑从前到后串联到 ORM 框架中就可以实现对数据库的新增、修改和删除操作了。
  • 所以读者在阅读这部分代码的时候,可以结合 XMLMapperBuilder 新增解析 insert/update/delete 和 SqlSession 调用执行入口进行代码调试,这样就能串联出整个功能链路了。

# 三、设计

假定这就是你正在承接的业务功能需求,你要在现在有的框架中完成对 insert/update/delete 方法的扩展。那么这个时候你需要思考,哪里是这个流程的开始,之后从流程的开始进行梳理。

那么这里显而易见,我们需要首先解决的是对 XML 的解析,由于之前在 ORM 框架的开发中,仅是处理了 select 的 SQL 信息,现在则需要把 insert/update/delete 的语句也按照解析 select 的方式进行处理。如图 12-2 所示,新增解析内容

图 12-2 新增解析内容

在添加了解析新类型 SQL 操作前提下,后续 DefaultSqlSession 中新增的执行 SQL 方法 insert/update/delete 就可以通过 Configuration 配置项拿到对应的映射器语句,并执行后续的处理流程。具体设计,如图 12-3 所示,解析XML 并处理 SQL 语句

图 12-3 解析XML 并处理 SQL 语句

  • 在执行 sqlSession.getMapper(IUserDao.class) 获取 Mapper 以后,后续的流程会依次串联到映射器工厂、映射器,以及获取对应的映射器方法,从 MapperMethod 映射器方法开始,调用的就是 DefaultSqlSession 了。
  • 那么这里要注意,除了我们已经开发完的 DefaultSqlSession#select 方法,其他定义的 insert、delete、update,都是调用内部的 update 方法,这也是 Mybatis ORM 框架对此类语句处理的一个包装。因为除了 select 方法,insert、delete、update,都是共性处理逻辑,所以可以被包装成一个逻辑来处理。

# 四、实现

# 1. 工程结构

mybatis-step-11
└── src
    ├── main
    │   └── java
    │       └── cn.bugstack.mybatis
    │           ├── binding
    │           │   ├── MapperMethod.java
    │           │   ├── MapperProxy.java
    │           │   ├── MapperProxyFactory.java
    │           │   └── MapperRegistry.java
    │           ├── builder
    │           │   ├── xml
    │           │   │   ├── XMLConfigBuilder.java
    │           │   │   ├── XMLMapperBuilder.java
    │           │   │   └── XMLStatementBuilder.java
    │           │   ├── BaseBuilder.java
    │           │   ├── MapperBuilderAssistant.java
    │           │   ├── ParameterExpression.java
    │           │   ├── SqlSourceBuilder.java
    │           │   └── StaticSqlSource.java
    │           ├── datasource
    │           ├── executor
    │           │   ├── parameter
    │           │   │   └── ParameterHandler.java
    │           │   ├── result
    │           │   │   ├── DefaultResultContext.java
    │           │   │   └── DefaultResultHandler.java
    │           │   ├── resultset
    │           │   │   ├── DefaultResultSetHandler.java
    │           │   │   └── ResultSetHandler.java
    │           │   │   └── ResultSetWrapper.java
    │           │   ├── statement
    │           │   │   ├── BaseStatementHandler.java
    │           │   │   ├── PreparedStatementHandler.java
    │           │   │   ├── SimpleStatementHandler.java
    │           │   │   └── StatementHandler.java
    │           │   ├── BaseExecutor.java
    │           │   ├── Executor.java
    │           │   └── SimpleExecutor.java
    │           ├── io
    │           ├── mapping
    │           │   ├── BoundSql.java
    │           │   ├── Environment.java
    │           │   ├── MappedStatement.java
    │           │   ├── ParameterMapping.java
    │           │   ├── ResultMap.java
    │           │   ├── ResultMapping.java
    │           │   ├── SqlCommandType.java
    │           │   └── SqlSource.java
    │           ├── parsing
    │           ├── reflection
    │           ├── scripting
    │           │   ├── defaults
    │           │   │   ├── DefaultParameterHandler.java
    │           │   │   └── RawSqlSource.java
    │           │   ├── xmltags
    │           │   │   ├── DynamicContext.java
    │           │   │   ├── MixedSqlNode.java
    │           │   │   ├── SqlNode.java
    │           │   │   ├── StaticTextSqlNode.java
    │           │   │   ├── XMLLanguageDriver.java
    │           │   │   └── XMLScriptBuilder.java
    │           │   ├── LanguageDriver.java
    │           │   └── LanguageDriverRegistry.java
    │           ├── session
    │           │   ├── defaults
    │           │   │   ├── DefaultSqlSession.java
    │           │   │   └── DefaultSqlSessionFactory.java
    │           │   ├── Configuration.java
    │           │   ├── ResultContext.java
    │           │   ├── ResultHandler.java
    │           │   ├── RowBounds.java
    │           │   ├── SqlSession.java
    │           │   ├── SqlSessionFactory.java
    │           │   ├── SqlSessionFactoryBuilder.java
    │           │   └── TransactionIsolationLevel.java
    │           ├── transaction
    │           └── type
    └── test
        ├── java
        │   └── cn.bugstack.mybatis.test.dao
        │       ├── dao
        │       │   └── IUserDao.java
        │       ├── po
        │       │   └── User.java
        │       └── ApiTest.java
        └── resources
            ├── mapper
            │   └──User_Mapper.xml
            └── mybatis-config-datasource.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90

工程源码公众号「bugstack虫洞栈」,回复:手写Mybatis,获取完整源码

完善ORM框架,增删改查操作核心类关系,如图 12-4 所示

图 12-4 完善ORM框架,增删改查操作核心类关系

  • 首先在 XML 映射器构建器中,扩展 XMLMapperBuilder#configurationElement 方法,添加对 insert/update/delete 的解析操作。这部分不需要太多的处理,只要添加上解析类型,就能满足当前章节的诉求。同样这里的解析信息都会存放到 Configuration 配置项的映射语句Map集合 mappedStatements 中,供后续 DefaultSqlSession 执行SQL获取配置信息时使用。
  • 接下来是对 MapperMethod 映射器方法的改造,在前面章节我们只是处理了 MapperMethod#execute 中 SELECT 类型的语句,这一章节需要在这里扩展 INSERT、DELETE、UPDATE,同时还需要对 SELECT 进行扩展查询出多个结果集的方法。
  • 所需要扩展的这些信息,都是有 DefaultSqlSession 调用执行器 Executor 进行处理的,这里你会看到 Executor 中只有 update 这个新增方法,并没有 insert、delete,因为这两个方法也是调用的 update 进行处理的。
  • 以上这些内容实现完成后,所有新增方法的调用,都会按照前面章节实现的语句执行、参数处理、结果封装等步骤,把流程执行完毕,并返回最终的结果。

# 2. 扩展解析元素

首先我们需要先解决新增 SQL 类型的 XML 语句,把 insert、update、delete,几种类型的 SQL 解析完成后,存放到 Configuration 配置项的映射器语句中。

源码详见cn.bugstack.mybatis.builder.xml.XMLMapperBuilder

public class XMLMapperBuilder extends BaseBuilder {

    // 省略部分未改变代码,可参考对应的源码学习...

    // 配置mapper元素
    // <mapper namespace="org.mybatis.example.BlogMapper">
    //   <select id="selectBlog" parameterType="int" resultType="Blog">
    //    select * from Blog where id = #{id}
    //   </select>
    // </mapper>
    private void configurationElement(Element element) {
        // 1.配置namespace
        String namespace = element.attributeValue("namespace");
        if (namespace.equals("")) {
            throw new RuntimeException("Mapper's namespace cannot be empty");
        }
        builderAssistant.setCurrentNamespace(namespace);

        // 2.配置select|insert|update|delete
        buildStatementFromContext(element.elements("select"),
                element.elements("insert"),
                element.elements("update"),
                element.elements("delete")
        );
    }

    // 配置select|insert|update|delete
    @SafeVarargs
    private final void buildStatementFromContext(List<Element>... lists) {
        for (List<Element> list : lists) {
            for (Element element : list) {
                final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, element);
                statementParser.parseStatementNode();
            }
        }
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
  • 与前面章节相比,这里改造 buildStatementFromContext 方法的入参类型为 list 的集合,也就是处理所传递到方法中的所有语句的集合。
  • 之后在 XMLMapperBuilder#configurationElement 调用层,传递 element.elements("select")、element.elements("insert")、element.elements("update")、element.elements("delete") 四个类型的方法,就可以把配置到 Mapper XML 中的不同 SQL 解析存放起来了。

# 3. 新增执行方法

在 Mybatis 的 ORM 框架中,DefaultSqlSession 中最终的 SQL 执行都会调用到 Executor 执行器的,所以这里我们先来看下关于执行器中新增方法的变化。

# 3.1 update接口定义

源码详见cn.bugstack.mybatis.executor.Executor

public interface Executor {

    ResultHandler NO_RESULT_HANDLER = null;

    int update(MappedStatement ms, Object parameter) throws SQLException;

    <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException;
		
		// ...省略部分代码

}
1
2
3
4
5
6
7
8
9
10
11
  • update 是 Executor 执行接口新增的方法,在这次功能扩展中,Executor 执行器也就只增加了这么一个 update 方法。因为其他两个方法 insert、delete 的调用,也都是调用 update 就够了,所以这里 Mybatis 并没有在执行器中定义新的方法。

# 3.3 update接口实现

源码详见cn.bugstack.mybatis.executor.SimpleExecutor

public class SimpleExecutor extends BaseExecutor {

    public SimpleExecutor(Configuration configuration, Transaction transaction) {
        super(configuration, transaction);
    }

    @Override
    protected int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
        Statement stmt = null;
        try {
            Configuration configuration = ms.getConfiguration();
            // 新建一个 StatementHandler
            StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
            // 准备语句
            stmt = prepareStatement(handler);
            // StatementHandler.update
            return handler.update(stmt);
        } finally {
            closeStatement(stmt);
        }
    }

    @Override
    protected <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        Statement stmt = null;
        try {
            Configuration configuration = ms.getConfiguration();
            // 新建一个 StatementHandler
            StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, rowBounds, resultHandler, boundSql);
            // 准备语句
            stmt = prepareStatement(handler);
            // 返回结果
            return handler.query(stmt, resultHandler);
        } finally {
            closeStatement(stmt);
        }
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
  • SimpleExecutor#doUpdate 方法是 BaseExecutor 抽象类实现 Executor#update 接口后,定义的抽象方法。
  • 这个抽象方法中,和 doQuery 方法几乎类似,都是创建一个新的 StatementHandler 语句处理器,之后准备语句,执行处理。
  • 但这里有一点需要注意,doUpdate 创建 StatementHandler 语句处理器的时候,是没有 resultHandler、boundSql 两个参数的,所以在创建的过程中,是需要对有必要使用的 boundSql 进行判断处理的。这部分内容主要体现在 BaseStatementHandler 的构造函数中,关于 boundSql 的判断和实例化处理

# 3.4 语句处理器实现

语句处理器的实现,主要变化在 BaseStatementHandler 的构造函数中添加了 boundSql 的初始化,代码如下;

public abstract class BaseStatementHandler implements StatementHandler {

		// ... 省略部分代码
    protected BoundSql boundSql;

    public BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
      
        // 新增判断,因为 update 不会传入 boundSql 参数,所以这里要做初始化处理
        if (boundSql == null) {
            boundSql = mappedStatement.getBoundSql(parameterObject);
        }

    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

因为只有获取了 BoundSql 的参数,才能方便的执行后续对 SQL 处理的操作。所以在执行 update 方法,没有传入 BoundSql 的时候,则需要这里进行判断以及自己获取的处理操作。接下来是对抽象类的实现,具体的处理 update 方法。

源码详见cn.bugstack.mybatis.executor.statement.PreparedStatementHandler

public class PreparedStatementHandler extends BaseStatementHandler{

    @Override
    public int update(Statement statement) throws SQLException {
        PreparedStatement ps = (PreparedStatement) statement;
        ps.execute();
        return ps.getUpdateCount();
    }

    @Override
    public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
        PreparedStatement ps = (PreparedStatement) statement;
        ps.execute();
        return resultSetHandler.<E> handleResultSets(ps);
    }
    
    // ... 省略部分代码

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 在 PreparedStatementHandler 预处理语句处理器中,实现了 update 方法,相对于 query 方法的实现,其实只是相当于 JDBC 操作数据库返回结果集的变化,update 处理要返回 SQL 的操作影响了多少条数据的数量。

# 4. SqlSession 定义和实现CRUD接口

在 SqlSession 中需要新增出处理数据库的接口,包括:selectList、insert、update、delete,这里我们来看下 DefaultSqlSession 对 SqlSession 接口方法的具体实现。

源码详见cn.bugstack.mybatis.session.defaults.DefaultSqlSession

public class DefaultSqlSession implements SqlSession {

    private Logger logger = LoggerFactory.getLogger(DefaultSqlSession.class);

    private Configuration configuration;
    private Executor executor;

    public DefaultSqlSession(Configuration configuration, Executor executor) {
        this.configuration = configuration;
        this.executor = executor;
    }

    @Override
    public <T> T selectOne(String statement) {
        return this.selectOne(statement, null);
    }

    @Override
    public <T> T selectOne(String statement, Object parameter) {
        List<T> list = this.<T>selectList(statement, parameter);
        if (list.size() == 1) {
            return list.get(0);
        } else if (list.size() > 1) {
            throw new RuntimeException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
        } else {
            return null;
        }
    }

    @Override
    public <E> List<E> selectList(String statement, Object parameter) {
        logger.info("执行查询 statement:{} parameter:{}", statement, JSON.toJSONString(parameter));
        MappedStatement ms = configuration.getMappedStatement(statement);
        try {
            return executor.query(ms, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER, ms.getSqlSource().getBoundSql(parameter));
        } catch (SQLException e) {
            throw new RuntimeException("Error querying database.  Cause: " + e);
        }
    }

    @Override
    public int insert(String statement, Object parameter) {
        // 在 Mybatis 中 insert 调用的是 update
        return update(statement, parameter);
    }

    @Override
    public int update(String statement, Object parameter) {
        MappedStatement ms = configuration.getMappedStatement(statement);
        try {
            return executor.update(ms, parameter);
        } catch (SQLException e) {
            throw new RuntimeException("Error updating database.  Cause: " + e);
        }
    }

    @Override
    public Object delete(String statement, Object parameter) {
        return update(statement, parameter);
    }
		
		// ... 省略部分代码

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
  • 在 DefaultSqlSession 的具体实现中可以看到,update 方法调用了具体的执行器封装成方法以后,insert、delete 都是调用的这个 update 方法进行操作的。接口定义的是单一执行,接口实现是做了适配封装
  • 另外这里单独提供了 selectList 方法,所以把之前在 selectOne 关于 executor.query 的执行处理,都迁移到 selectList 方法中。之后在 selectOne 中调用 selectList 方法,并给出相应的判断处理。

# 5. 映射器命令执行调度

以上这些所实现的语句执行器、SqlSession 包装,最终都会交给 MapperMethod 映射器方法根据不同的 SQL 命令调用不同的 SqlSession 方法进行执行。

public class MapperMethod {

    private final SqlCommand command;
    private final MethodSignature method;

    public MapperMethod(Class<?> mapperInterface, Method method, Configuration configuration) {
        this.command = new SqlCommand(configuration, mapperInterface, method);
        this.method = new MethodSignature(configuration, method);
    }

    public Object execute(SqlSession sqlSession, Object[] args) {
        Object result = null;
        switch (command.getType()) {
            case INSERT: {
                Object param = method.convertArgsToSqlCommandParam(args);
                result = sqlSession.insert(command.getName(), param);
                break;
            }
            case DELETE: {
                Object param = method.convertArgsToSqlCommandParam(args);
                result = sqlSession.delete(command.getName(), param);
                break;
            }
            case UPDATE: {
                Object param = method.convertArgsToSqlCommandParam(args);
                result = sqlSession.update(command.getName(), param);
                break;
            }
            case SELECT: {
                Object param = method.convertArgsToSqlCommandParam(args);
                if (method.returnsMany) {
                    result = sqlSession.selectList(command.getName(), param);
                } else {
                    result = sqlSession.selectOne(command.getName(), param);
                }
                break;
            }
            default:
                throw new RuntimeException("Unknown execution method for: " + command.getName());
        }
        return result;
    }

    // 省略 SQL指令和方法前面代码块,可以参考源码
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
  • 映射器方法 MapperMethod#execute 会根据不同的 SqlCommand 指令调用到不同的方法上去,INSERT、DELETE、UPDATE 分别按照对应的方法调用即可。这里 SELECT 进行了扩展,因为需要按照不同的方法出参类型,调用不同的方法,主要是 selectList、selectOne 的区别。

  • 另外这里 method.returnsMany 来自于 MapperMethod.MethodSignature 方法签名中进行通过,返回类型进行获取的,代码如图 12-5 所示。

    图 12-5 方法签名获取方法的返回参数类型

# 五、测试

# 1. 事先准备

# 1.1 创建库表

创建一个数据库名称为 mybatis 并在库中创建表 user 以及添加测试数据,如下:

CREATE TABLE
    USER
    (
        id bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID',
        userId VARCHAR(9) COMMENT '用户ID',
        userHead VARCHAR(16) COMMENT '用户头像',
        createTime TIMESTAMP NULL COMMENT '创建时间',
        updateTime TIMESTAMP NULL COMMENT '更新时间',
        userName VARCHAR(64),
        PRIMARY KEY (id)
    )
    ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
insert into user (id, userId, userHead, createTime, updateTime, userName) values (1, '10001', '1_04', '2022-04-13 00:00:00', '2022-04-13 00:00:00', '小傅哥');    
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 1.2 配置数据源

<environments default="development">
    <environment id="development">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <property name="driver" value="com.mysql.jdbc.Driver"/>
            <property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatis?useUnicode=true&characterEncoding=utf8"/>
            <property name="username" value="root"/>
            <property name="password" value="123456"/>
        </dataSource>
    </environment>
</environments>
1
2
3
4
5
6
7
8
9
10
11
  • 通过 mybatis-config-datasource.xml 配置数据源信息,包括:driver、url、username、password
  • 在这里 dataSource 可以按需配置成 DRUID、UNPOOLED 和 POOLED 进行测试验证。

# 1.3 配置Mapper

<select id="queryUserInfoById" parameterType="java.lang.Long" resultType="cn.bugstack.mybatis.test.po.User">
    SELECT id, userId, userName, userHead
    FROM user
    where id = #{id}
</select>

<select id="queryUserInfo" parameterType="cn.bugstack.mybatis.test.po.User" resultType="cn.bugstack.mybatis.test.po.User">
    SELECT id, userId, userName, userHead
    FROM user
    where id = #{id} and userId = #{userId}
</select>

<select id="queryUserInfoList" resultType="cn.bugstack.mybatis.test.po.User">
    SELECT id, userId, userName, userHead
    FROM user
</select>

<update id="updateUserInfo" parameterType="cn.bugstack.mybatis.test.po.User">
    UPDATE user
    SET userName = #{userName}
    WHERE id = #{id}
</update>

<insert id="insertUserInfo" parameterType="cn.bugstack.mybatis.test.po.User">
   INSERT INTO user
   (userId, userName, userHead, createTime, updateTime)
   VALUES (#{userId}, #{userName}, #{userHead}, now(), now())
</insert>

<delete id="deleteUserInfoByUserId" parameterType="java.lang.String">
    DELETE FROM user WHERE userId = #{userId}
</delete>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
  • 本章节已经把 ORM 框架的基本功能全部开发完成了,所以这里可以分别配置测试不同类型的 SQL 语句,包括:insert、delete、update、select

# 2. 单元测试

IUserDao 配置相应的方法,与 Mapper XML 匹配。

public interface IUserDao {

    User queryUserInfoById(Long id);

    User queryUserInfo(User req);

    List<User> queryUserInfoList();

    int updateUserInfo(User req);

    void insertUserInfo(User req);

    int deleteUserInfoByUserId(String userId);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 2.1 插入测试

@Test
public void test_insertUserInfo() {
    // 1. 获取映射器对象
    IUserDao userDao = sqlSession.getMapper(IUserDao.class);

    // 2. 测试验证
    User user = new User();
    user.setUserId("10002");
    user.setUserName("小白");
    user.setUserHead("1_05");
    userDao.insertUserInfo(user);
    logger.info("测试结果:{}", "Insert OK");

    // 3. 提交事务
    sqlSession.commit();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

测试结果

14:45:25.166 [main] INFO  c.b.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"10002"
14:45:25.166 [main] INFO  c.b.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"小白"
14:45:25.166 [main] INFO  c.b.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"1_05"
14:45:25.171 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 测试结果:Insert OK

Process finished with exit code 0
1
2
3
4
5
6

  • 从测试日志信息和数据库的截图,可以看到数据已经插入到数据库,验证通过。
  • 另外这里需要注意,我们执行完 SQL 以后,还执行力一次 sqlSession.commit(); 这是因为在 DefaultSqlSessionFactory#openSession 开启 Session 创建事务工厂的时候,传入给事务工厂构造函数的事务是否自动提交为 false 所以这里就需要我们自己去手动提交事务,否则是不会插入到数据库的。下面几个测试也是同样的处理方式。

# 2.2 查询测试(多条数据)

@Test
public void test_queryUserInfoList() {
    // 1. 获取映射器对象
    IUserDao userDao = sqlSession.getMapper(IUserDao.class);
    // 2. 测试验证:对象参数
    List<User> users = userDao.queryUserInfoList();
    logger.info("测试结果:{}", JSON.toJSONString(users));
}
1
2
3
4
5
6
7
8

测试结果

14:50:19.063 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 测试结果:[{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"},{"id":4,"userHead":"1_05","userId":"10002","userName":"小白"}]

Process finished with exit code 0
1
2
3
  • 现在我们再查询结果的时候,就可以查询到2条记录的集合了,这说明我们添加的 MapperMethod#execute 调用 sqlSession.selectList(command.getName(), param); 是测试通过的。读者伙伴也可以根据这个测试的代码,进行断掉调试。

# 2.3 修改测试

@Test
public void test_updateUserInfo() {
    // 1. 获取映射器对象
    IUserDao userDao = sqlSession.getMapper(IUserDao.class);
    // 2. 测试验证
    int count = userDao.updateUserInfo(new User(1L, "10001", "叮当猫"));
    logger.info("测试结果:{}", count);
    // 3. 提交事务
    sqlSession.commit();
}
1
2
3
4
5
6
7
8
9
10

测试结果

14:52:09.550 [main] INFO  c.b.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"叮当猫"
14:52:09.550 [main] INFO  c.b.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:1
14:52:09.553 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 测试结果:1
1
2
3

  • 这里测试验证把ID=1的用户,userName 修改为 叮当猫,通过测试日志和数据库截图,可以看出,测试已经通过。

# 2.4 删除测试

@Test
public void test_deleteUserInfoByUserId() {
    // 1. 获取映射器对象
    IUserDao userDao = sqlSession.getMapper(IUserDao.class);
    // 2. 测试验证
    int count = userDao.deleteUserInfoByUserId("10002");
    logger.info("测试结果:{}", count == 1);
    // 3. 提交事务
    sqlSession.commit();
}
1
2
3
4
5
6
7
8
9
10

测试结果

14:57:39.536 [main] INFO  c.b.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"10002"
14:57:39.539 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 测试结果:true

Process finished with exit code 0
1
2
3
4

  • 这里我们把数据库表中 userId = “10002” 的用户删除掉,通过测试日志和数据库截图,可以看出测试通过。

# 六、总结

  • 到本章节我们就把 Mybatis 的全部主干流程串联实现完成了,可以执行对数据库的增删改查操作,读者伙伴也可以发现,本章节在原有的内容下,进行扩展的时候也是非常方便的,甚至不要多大的代码改动。这主要也得益于框架在设计实现过程中,合理运用设计原则和设计模式的好处。
  • 读者在学习的过程中,可以调试源码中的一些参数,比如像事务是否自动提交,查询出来的参数是否可以添加其他类型,在增删改查中,是否还有其他情况的处理。这些小的功能点,都可以进行添加练习,如果你能正确的添加并走完流程验证公国,那么说明你是真的学习会了。
  • 在本章节全部基础功能链路串联完毕以后,关于 Mybatis 的框架中,还有一些额外扩展的知识点,比如:插入时返回当前ID、Map 类型映射、一级二级缓存、插件模块等,后续的章节中我们会找一些有代表性的内容,进行扩展开发学习。读者伙伴也可以按照目前的框架结构,自行添加这写模块进行练习学习。

# 七、优秀作业