作者:小傅哥 博客:https://bugstack.cn
❝沉淀、分享、成长,让自己和他人都能有所收获!😜❞
时间再多一件事情也不可能做的完美,但总有时间做完一件事情!
在我们的生活中,要经历很多重要的阶段,包括;高考、求婚、面试、述职,在所有事情发生之前,我们都在做着大量的准备,甚至像为了高考的这样的时间点,要准备好几年。可即使有这样的大量的准备时间,我们几乎也没有办法把最终的结果做到完美,只能是把结果做完。没有遗憾的人生,才是遗憾!
其实我们的系统设计开发也是一样的,系统越做越复杂,功能越做越多,但人的智力是有上限的,永远无法完全评估未发生的事情。所以对于一个复杂的分布式系统,我们几乎永远不可能找到并修复所有的 bug,有时候解决方法也不是完全找出所有问题并消灭,而是能容忍部分小问题,并在这些问题发生时可以自动恢复,做到最终补偿处理。而这样的设计也称为高可用和弹性设计。
前面各个章节的内容,渐进式的逐步为我们实现了一个基本的框架结构,能满足我们对一个 DAO 方法的查询操作,以及处理对应的参数和返回结果。
那么目前这个框架中所提供的 SQL 处理仅有一个 select 查询操作,还没有其他我们日常常用的 insert、update、delete,以及 select 查询返回的集合类型数据。
其实这一部分新增处理 SQL 的内容,也就是在 SqlSession 需要定义新的接口,通知让这些接口被映射器类方法 MapperMethod 进行调用处理。如图 12-1 所示 SqlSession 定义操作SQL方法
图 12-1 SqlSession 定义操作SQL方法
insert/update/delete
这部分功能来说,并不会太复杂的。因为从 XML 对方法的解析、参数的处理、结果的封装,都已经是成型的结构。而我们只是对把这部分新增逻辑从前到后串联到 ORM 框架中就可以实现对数据库的新增、修改和删除操作了。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 了。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
│ ├── 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
工程源码:公众号「bugstack虫洞栈」,回复:手写Mybatis,获取完整源码
完善ORM框架,增删改查操作核心类关系,如图 12-4 所示
图 12-4 完善ORM框架,增删改查操作核心类关系
首先我们需要先解决新增 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();
}
}
}
}
element.elements("select")、element.elements("insert")、element.elements("update")、element.elements("delete")
四个类型的方法,就可以把配置到 Mapper XML 中的不同 SQL 解析存放起来了。在 Mybatis 的 ORM 框架中,DefaultSqlSession 中最终的 SQL 执行都会调用到 Executor 执行器的,所以这里我们先来看下关于执行器中新增方法的变化。
源码详见: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;
// ...省略部分代码
}
源码详见: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);
}
}
}
语句处理器的实现,主要变化在 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);
}
}
}
因为只有获取了 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);
}
// ... 省略部分代码
}
在 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);
}
// ... 省略部分代码
}
以上这些所实现的语句执行器、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指令和方法前面代码块,可以参考源码
}
图 12-5 方法签名获取方法的返回参数类型
创建一个数据库名称为 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', '小傅哥');
<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>
mybatis-config-datasource.xml
配置数据源信息,包括:driver、url、username、password<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>
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);
}
@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();
}
测试结果
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
@Test
public void test_queryUserInfoList() {
// 1. 获取映射器对象
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
// 2. 测试验证:对象参数
List<User> users = userDao.queryUserInfoList();
logger.info("测试结果:{}", JSON.toJSONString(users));
}
测试结果
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
sqlSession.selectList(command.getName(), param);
是测试通过的。读者伙伴也可以根据这个测试的代码,进行断掉调试。@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();
}
测试结果
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
@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();
}
测试结果
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
- END -
你好,我是小傅哥。一线互联网java
工程师、架构师,开发过交易&营销、写过运营&活动、设计过中间件也倒腾过中继器、IO板卡。不只是写Java语言,也搞过C#、PHP,是一个技术活跃的折腾者。
2020年写了一本PDF《重学Java设计模式》,全网下载量50万+,帮助很多同学成长,现已出书。同年 github 的两个项目,CodeGuide
、itstack-demo-design
,持续霸榜 Trending,成为全球热门项目。