文章目录
- 一、目标:完善增删改查
- 二、设计:完善增删改查
- 三、实现:完善增删改查
- 3.1 工程结构
- 3.2 完善增删改查类图
- 3.3 扩展解析元素
- 3.4 新增执行方法
- 3.4.1 执行器接口添加update
- 3.4.2 执行器抽象基类
- 3.4.3 简单执行器
- 3.5 语句处理器实现
- 3.5.1 语句处理器接口
- 3.5.2 修改映射器语句类
- 3.5.3 抽象语句处理器基类
- 3.5.4 预处理语句处理器
- 3.5.5 简单语句处理器
- 3.6 SqlSession 定义和实现CRUD接口
- 3.6.1 SqlSession接口
- 3.6.2 默认SqlSession实现类
- 3.7 映射器命令执行调度
- 四、测试:完善增删改查
- 4.1 测试环境配置
- 4.1.1 配置数据源配置
- 4.1.2 修改User实体类
- 4.1.3 修改IUserDao用户持久层
- 4.1.4 修改User_Mapper配置文件
- 4.2 单元测试
- 4.2.1 插入测试
- 4.2.2 查询测试(多条数据)
- 4.2.3 修改测试
- 4.2.4 删除测试
- 五、总结:完善增删改查
一、目标:完善增删改查
💡 目前框架中所提供的 SQL 处理仅有一个 select 查询操作.
还没有其他我们日常常用的 insert、update、delete,以及 select 查询返回的集合类型数据?
- 这部分新增处理 SQL 的内容,也就是在 SqlSession 需要定义新的接口,通知让这些接口被映射器类方法 MappedMethod 进行调用处理。
- 结合目前框架的开发结构,对于扩展
insert/update/delete
这部分功能来说,并不会太复杂。- 因为从 XML 对方法的解析、参数的处理、结果的封装,都是已经成型的结构。
- 而我们只需要把这部分新增逻辑从前到后串联到 ORM 框架中就可是实现对数据库的新增、修改和删除。
二、设计:完善增删改查
💡 在现有的框架中完成对 insert/update/delete 方法的扩展?
思考:哪里是流程的开始?
- 首先解决对 XML 的解析,之前的 ORM 框架的开发中,仅是处理了
select
的 SQL 信息,现在需要把insert/update/delete
的语句也按照解析select
的方式进行处理。
- 在添加解析新类型 SQL 操作前提下,后续
DefaultSqlSession
中新增的执行 SQL 方法insert/update/delete
就可以通过 Configuration 配置项拿到对应的映射器语句,并执行后续的处理流程。
- 在执行
sqlSession.getMapper(IUserDao.class)
获取 Mapper 以后。 - 后续的流程会依次串联到映射器工厂、映射器,以及获取对应的映射器方法开始,调用的就是 DefaultSqlSession。
- 注意:定义的
insert/update/delete
,都是调用内部的update
方法,这是 Mybatis ORM 框架对此类语句处理的一个包装。- 因为除了
select
方法,insert、update、delete
,都是共性处理逻辑,所以可以被包装成一个逻辑来处理。
- 因为除了
三、实现:完善增删改查
3.1 工程结构
mybatis-step-11
|-src
|-main
| |-java
| |-com.lino.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
| | |-druid
| | | |-DruidDataSourceFacroty.java
| | |-pooled
| | | |-PooledConnection.java
| | | |-PooledDataSource.java
| | | |-PooledDataSourceFacroty.java
| | | |-PoolState.java
| | |-unpooled
| | | |-UnpooledDataSource.java
| | | |-UnpooledDataSourceFacroty.java
| | |-DataSourceFactory.java
| |-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
| | |-Resources.java
| |-mapping
| | |-BoundSql.java
| | |-Environment.java
| | |-MappedStatement.java
| | |-ParameterMapping.java
| | |-ResultMap.java
| | |-ResultMapping.java
| | |-SqlCommandType.java
| | |-SqlSource.java
| |-parsing
| | |-GenericTokenParser.java
| | |-TokenHandler.java
| |-reflection
| | |-factory
| | | |-DefaultObjectFactory.java
| | | |-ObjectFactory.java
| | |-invoker
| | | |-GetFieldInvoker.java
| | | |-Invoker.java
| | | |-MethodInvoker.java
| | | |-SetFieldInvoker.java
| | |-property
| | | |-PropertyNamer.java
| | | |-PropertyTokenizer.java
| | |-wrapper
| | | |-BaseWrapper.java
| | | |-BeanWrapper.java
| | | |-CollectionWrapper.java
| | | |-DefaultObjectWrapperFactory.java
| | | |-MapWrapper.java
| | | |-ObjectWrapper.java
| | | |-ObjectWrapperFactory.java
| | |-MetaClass.java
| | |-MetaObject.java
| | |-Reflector.java
| | |-SystemMetaObject.java
| |-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
| | |-jdbc
| | | |-JdbcTransaction.java
| | | |-JdbcTransactionFactory.java
| | |-Transaction.java
| | |-TransactionFactory.java
| |-type
| | |-BaseTypeHandler.java
| | |-IntegerTypeHandler.java
| | |-JdbcType.java
| | |-LongTypeHandler.java
| | |-StringTypeHandler.java
| | |-TypeAliasRegistry.java
| | |-TypeHandler.java
| | |-TypeHandlerRegistry.java
|-test
|-java
| |-com.lino.mybatis.test
| |-dao
| | |-IUserDao.java
| |-po
| | |-User.java
| |-ApiTest.java
|-resources
|-mapper
| |-User_Mapper.xml
|-mybatis-config-datasource.xml
3.2 完善增删改查类图
- 首先在 XML 映射器构建中,扩展
XMLMapperBuilder#configurationElement
方法,添加对insert/update/delete
的解析操作。- 添加解析类型。
- 同时解析信息都会存放到 Configuration 配置项的映射语句 Map 集合 mappedStatement 中,供后续 DefaultSqlSession 执行 SQL 获取配置信息时使用。
- 接下来是对 MappedMethod 映射器方法的改造,在这里扩展
INSERT、UPDATE、DELETE
,同时还需要对SELECT
进行扩展查询出多个结果集的方法。 - 所需要扩展的信息,都是由 DefaultSqlSession 调用执行器 Executor 进行处理的。
- 这里你会看到 Executor 中只有
update
这个新增方法,并没有insert、delete
,因为这两个方法也是调用的update
进行处理的。
- 这里你会看到 Executor 中只有
- 以上这些内容实现完成后,所有新增方法的调用,都会按照前面章节的实现:语句执行、结果封装等步骤,把流程执行完毕,并返回最终的结果。
3.3 扩展解析元素
- 新增 SQL 类型的 XML 语句,把
insert、update、delete
,几种类型的 SQL 解析完成后,存放到 Configuration 配置项的映射器语句中。
XMLMapperBuilder.java
package com.lino.mybatis.builder.xml;
import com.lino.mybatis.builder.BaseBuilder;
import com.lino.mybatis.builder.MapperBuilderAssistant;
import com.lino.mybatis.io.Resources;
import com.lino.mybatis.session.Configuration;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.InputStream;
import java.util.List;
/**
* @description: XML映射构建器
*/
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>
*
* @param element 元素
*/
private void configurationElement(Element element) {
// 1.配置namespace
String namespace = element.attributeValue("namespace");
if ("".equals(namespace)) {
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
*
* @param lists 元素列表
*/
@SafeVarargs
private final void buildStatementFromContext(List<Element>... lists) {
for (List<Element> list : lists) {
for (Element element : list) {
final XMLStatementBuilder statementBuilder = new XMLStatementBuilder(configuration, builderAssistant, element);
statementBuilder.parseStatementNode();
}
}
}
}
- 这里改造
buildStatementFromContext
方法的入参类型为list
的集合,也就是处理所传递到方法中的所有语句的集合。 - 之后在
XMLMapperBuilder#configurationElement
调用层,传递element.elements("select")
element.elements("insert")
element.elements("update")
element.elements("delete")
- 四个类型的方法,就可以配置到 Mapper XML 中的不同 SQL 解析存放起来了。
3.4 新增执行方法
- 在 Mybatis 的 ORM 框架中,
DefaultSqlSession
中最终的 SQL 执行都会调用到 Executor 执行器的。
3.4.1 执行器接口添加update
Executor.java
package com.lino.mybatis.executor;
import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.session.ResultHandler;
import com.lino.mybatis.session.RowBounds;
import com.lino.mybatis.transaction.Transaction;
import java.sql.SQLException;
import java.util.List;
/**
* @description: 执行器
*/
public interface Executor {
...
/**
* 更新
*
* @param ms 映射器语句
* @param parameter 参数
* @return 返回的是受影响的行数
* @throws SQLException SQL异常
*/
int update(MappedStatement ms, Object parameter) throws SQLException;
/**
* 查询
*
* @param ms 映射器语句
* @param parameter 参数
* @param rowBounds 分页记录限制
* @param resultHandler 结果处理器
* @param boundSql SQL对象
* @param <E> 返回的类型
* @return List<E>
* @throws SQLException SQL异常
*/
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException;
...
}
update
是 Executor 执行接口新增的方法。因为其他两个方法insert、delete
的调用,也都是调用update
就够了。
3.4.2 执行器抽象基类
BaseExecutor.java
package com.lino.mybatis.executor;
import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.session.ResultHandler;
import com.lino.mybatis.session.RowBounds;
import com.lino.mybatis.transaction.Transaction;
import org.slf4j.LoggerFactory;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
/**
* @description: 执行器抽象基类
*/
public abstract class BaseExecutor implements Executor {
private org.slf4j.Logger logger = LoggerFactory.getLogger(BaseExecutor.class);
protected Configuration configuration;
protected Transaction transaction;
protected Executor wrapper;
private boolean closed;
public BaseExecutor(Configuration configuration, Transaction transaction) {
this.configuration = configuration;
this.transaction = transaction;
this.wrapper = this;
}
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
return doUpdate(ms, parameter);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
if (closed) {
throw new RuntimeException("Executor was closed.");
}
return doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
}
/**
* 更新方法
*
* @param ms 映射器语句
* @param parameter 参数
* @return 返回的是受影响的行数
* @throws SQLException SQL异常
*/
protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;
/**
* 查询方法
*
* @param ms 映射器语句
* @param parameter 参数
* @param rowBounds 分页记录限制
* @param resultHandler 结果处理器
* @param boundSql SQL对象
* @param <E> 返回的类型
* @return List<E>
* @throws SQLException SQL异常
*/
protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException;
@Override
public Transaction getTransaction() {
if (closed) {
throw new RuntimeException("Executor was closed.");
}
return transaction;
}
@Override
public void commit(boolean required) throws SQLException {
if (closed) {
throw new RuntimeException("Cannot commit, transaction is already closed.");
}
if (required) {
transaction.commit();
}
}
@Override
public void rollback(boolean required) throws SQLException {
if (!closed) {
if (required) {
transaction.rollback();
}
}
}
@Override
public void close(boolean forceRollback) {
try {
try {
rollback(forceRollback);
} finally {
transaction.close();
}
} catch (SQLException e) {
logger.warn("Unexpected exception on closing transaction. Cause: " + e);
} finally {
transaction = null;
closed = true;
}
}
/**
* 关闭语句
*
* @param statement 语句
*/
protected void closeStatement(Statement statement) {
if (statement != null) {
try {
statement.close();
} catch (SQLException ignore) {
}
}
}
}
3.4.3 简单执行器
SimpleExecutor.java
package com.lino.mybatis.executor;
import com.lino.mybatis.executor.statement.StatementHandler;
import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.session.ResultHandler;
import com.lino.mybatis.session.RowBounds;
import com.lino.mybatis.transaction.Transaction;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
import static ch.qos.logback.core.db.DBHelper.closeStatement;
/**
* @description: 简单执行器
* @author: lingjian
* @createDate: 2022/11/8 13:42
*/
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);
}
}
private Statement prepareStatement(StatementHandler handler) throws SQLException {
Statement stmt;
Connection connection = transaction.getConnection();
// 准备语句
stmt = handler.prepare(connection);
handler.parameterize(stmt);
return stmt;
}
}
SimpleExecutor#doUpdate
方法是 BeanExecutor 抽象类实现Executor#update
接口后,定义的抽象方法。- 这个抽象方法中,和
doQuery
方法几乎类似,都是创建一个新的 StatementHandler 语句处理器,之后准备语句,执行处理。 - 注意:
doUpdate
创建 StatementHandler 语句处理器的时候,是没有resultHandler、boundSql
两个参数的。- 所以在创建的过程中,是需要对有必要使用的 boundSql 进行判断处理的。
- 这部分内容主要体现在 BaseStatementHandler 的构造函数中,关于 boundSql 的判断和实例化处理。
3.5 语句处理器实现
3.5.1 语句处理器接口
StatementHandler.java
package com.lino.mybatis.executor.statement;
import com.alibaba.druid.support.spring.stat.annotation.Stat;
import com.lino.mybatis.session.ResultHandler;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
/**
* @description: 语句处理器
*/
public interface StatementHandler {
/**
* 准备语句
*
* @param connection 链接
* @return Statement语句
*/
Statement prepare(Connection connection);
/**
* 参数化
*
* @param statement 语句
* @throws SQLException SQL异常
*/
void parameterize(Statement statement) throws SQLException;
/**
* 执行更新
*
* @param statement 语句
* @return 返回受影响的行数
* @throws SQLException SQL异常
*/
int update(Statement statement) throws SQLException;
/**
* 执行查询
*
* @param statement 语句
* @param resultHandler 结果处理器
* @param <E> 泛型类型
* @return 泛型集合
* @throws SQLException SQL异常
*/
<E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException;
}
3.5.2 修改映射器语句类
MappedStatement.java
package com.lino.mybatis.mapping;
import com.lino.mybatis.scripting.LanguageDriver;
import com.lino.mybatis.session.Configuration;
import java.util.List;
/**
* @description: 映射器语句类
*/
public class MappedStatement {
private Configuration configuration;
private String id;
private SqlCommandType sqlCommandType;
private SqlSource sqlSource;
Class<?> resultType;
private LanguageDriver lang;
private List<ResultMap> resultMaps;
public MappedStatement() {
}
/**
* 获取SQL对象
*
* @param parameterObject 参数
* @return SQL对象
*/
public BoundSql getBoundSql(Object parameterObject) {
// 调用 SqlSource#getBoundSql
return sqlSource.getBoundSql(parameterObject);
}
...
}
3.5.3 抽象语句处理器基类
- 主要变化在 BaseStatementHandler 的构造函数中添加了 boundSql 的初始化。
BaseStatementHandler.java
package com.lino.mybatis.executor.statement;
import com.lino.mybatis.executor.Executor;
import com.lino.mybatis.executor.parameter.ParameterHandler;
import com.lino.mybatis.executor.resultset.ResultSetHandler;
import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.session.ResultHandler;
import com.lino.mybatis.session.RowBounds;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
/**
* @description: 语句处理器抽象基类
*/
public abstract class BaseStatementHandler implements StatementHandler {
protected final Configuration configuration;
protected final Executor executor;
protected final MappedStatement mappedStatement;
protected final Object parameterObject;
protected final ResultSetHandler resultSetHandler;
protected final ParameterHandler parameterHandler;
protected final RowBounds rowBounds;
protected BoundSql boundSql;
public BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds,
ResultHandler resultHandler, BoundSql boundSql) {
this.configuration = mappedStatement.getConfiguration();
this.executor = executor;
this.mappedStatement = mappedStatement;
this.parameterObject = parameterObject;
this.rowBounds = rowBounds;
// 新增判断,因为update不会传入boundSql参数,这里做初始化处理
if (boundSql == null) {
boundSql = mappedStatement.getBoundSql(parameterObject);
}
this.boundSql = boundSql;
this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, resultHandler, boundSql);
}
@Override
public Statement prepare(Connection connection) {
Statement statement = null;
try {
// 实例化 Statement
statement = instantiateStatement(connection);
// 参数设置,可以被抽取,提供配置
statement.setQueryTimeout(350);
statement.setFetchSize(10000);
return statement;
} catch (Exception e) {
throw new RuntimeException("Error prepare statement. Cause: " + e, e);
}
}
/**
* 初始化语句
*
* @param connection 连接
* @return 语句
* @throws SQLException SQL异常
*/
protected abstract Statement instantiateStatement(Connection connection) throws SQLException;
}
- 因为只有获取了 boundSql 的参数,才能方便的执行后续对 SQL 处理的操作。
- 所以在执行
update
方法,没有传入 boundSql 的时候,则需要这里进行判断以及自己获取的处理操作。
3.5.4 预处理语句处理器
PreparedStatementHandler.java
package com.lino.mybatis.executor.statement;
import com.lino.mybatis.executor.Executor;
import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.session.ResultHandler;
import com.lino.mybatis.session.RowBounds;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
/**
* @description: 预处理语句处理器(PREPARED)
*/
public class PreparedStatementHandler extends BaseStatementHandler {
public PreparedStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject,
RowBounds rowBounds, ResultHandler resultSetHandler, BoundSql boundSql) {
super(executor, mappedStatement, parameterObject, rowBounds, resultSetHandler, boundSql);
}
@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
String sql = boundSql.getSql();
return connection.prepareStatement(sql);
}
@Override
public void parameterize(Statement statement) throws SQLException {
parameterHandler.setParameters((PreparedStatement) statement);
}
@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.handleResultSets(ps);
}
}
- 在
PreparedStatementHandler
预处理语句处理器中,实现类update
方法。 - 相对于
query
方法的实现,其实只是相当于 JDBC 操作数据库返回结果集的变化,update
处理要返回 SQL 的操作影响了多少条数据的数量。
3.5.5 简单语句处理器
SimpleStatementHandler.java
package com.lino.mybatis.executor.statement;
import com.lino.mybatis.executor.Executor;
import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.session.ResultHandler;
import com.lino.mybatis.session.RowBounds;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
/**
* @description: 简单语句处理器(STATEMENT)
*/
public class SimpleStatementHandler extends BaseStatementHandler {
public SimpleStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject,
RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
super(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
}
@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
return connection.createStatement();
}
@Override
public void parameterize(Statement statement) {
}
@Override
public int update(Statement statement) throws SQLException {
String sql = boundSql.getSql();
statement.execute(sql);
return statement.getUpdateCount();
}
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
String sql = boundSql.getSql();
statement.execute(sql);
return resultSetHandler.handleResultSets(statement);
}
}
3.6 SqlSession 定义和实现CRUD接口
3.6.1 SqlSession接口
- 在 SqlSession 中需要新增出处理数据库的接口,包括
selectList、insert、update、delete
。
SqlSession.java
package com.lino.mybatis.session;
import java.util.List;
/**
* @description: SqlSession 用来执行SQL,获取映射器,管理事务
*/
public interface SqlSession {
/**
* 根据指定的sqlID获取一条记录的封装对象
*
* @param statement sqlID
* @param <T> 封装之后的对象类型
* @return 封装之后的对象
*/
<T> T selectOne(String statement);
/**
* 根据指定的sqlID获取一条记录的封装对象,只不过这个方法容许我们给sql传递一些参数
*
* @param statement sqlID
* @param parameter 传递参数
* @param <T> 封装之后的对象类型
* @return 封装之后的对象
*/
<T> T selectOne(String statement, Object parameter);
/**
* 获取多条基类,这个方法容许我们可以传递一些参数
*
* @param statement sqlID
* @param parameter 传递参数
* @param <E>封装之后的对象列表
* @return 封装之后的对象列表
*/
<E> List<E> selectList(String statement, Object parameter);
/**
* 插入记录,容许传递参数
*
* @param statement sqlID
* @param parameter 传递参数
* @return 返回的是受影响的行数
*/
int insert(String statement, Object parameter);
/**
* 插入记录
*
* @param statement sqlID
* @param parameter 传递参数
* @return 返回的是受影响的行数
*/
int update(String statement, Object parameter);
/**
* 删除记录
*
* @param statement sqlID
* @param parameter 传递参数
* @return 返回的是受影响的行数
*/
Object delete(String statement, Object parameter);
/**
* 以下是事务控制方法 commit,rollback
*/
void commit();
/**
* 得到映射器,使用泛型,使得类型安全
*
* @param type 对象类型
* @param <T> 封装之后的对象类型
* @return 封装之后的对象
*/
<T> T getMapper(Class<T> type);
/**
* 得到配置
*
* @return Configuration
*/
Configuration getConfiguration();
}
3.6.2 默认SqlSession实现类
DefaultSqlSession.java
package com.lino.mybatis.session.defaults;
import com.alibaba.fastjson.JSON;
import com.lino.mybatis.executor.Executor;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.session.RowBounds;
import com.lino.mybatis.session.SqlSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.SQLException;
import java.util.List;
/**
* @description: 默认sqlSession实现类
*/
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.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("Errot updating database. Cause: " + e);
}
}
@Override
public Object delete(String statement, Object parameter) {
return update(statement, parameter);
}
@Override
public void commit() {
try {
executor.commit(true);
} catch (SQLException e) {
throw new RuntimeException("Error committing transaction. Cause: " + e);
}
}
@Override
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
@Override
public Configuration getConfiguration() {
return configuration;
}
}
- 在 DefaultSqlSession 的具体实现中可以看到,
update
方法调用了具体的执行器封装成方法后,insert、delete
都是调用的这个update
方法进行操作的。- 接口定义的是单一执行,接口实现是做了适配封装。
- 另外这里单独提供了
selectList
方法,所以把之前在selectOne
关于executor.query
的执行处理,都迁移到selectList
方法中。 - 之后在
selectOne
中调用selectList
方法,并给出相应的判断处理。
3.7 映射器命令执行调度
- 以上这些所实现的语句执行器、SqlSession 包装,最终都会交给 MapperMethod 映射器方法根据不同的 SQL 命令调用不同的 SqlSession 方法进行执行。
MapperMethod.java
package com.lino.mybatis.binding;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.mapping.SqlCommandType;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.session.SqlSession;
import java.lang.reflect.Method;
import java.util.*;
/**
* @description: 映射器方法
*/
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 指令
*/
public static class SqlCommand {
private final String name;
private final SqlCommandType type;
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
String statementName = mapperInterface.getName() + "." + method.getName();
MappedStatement ms = configuration.getMappedStatement(statementName);
this.name = ms.getId();
this.type = ms.getSqlCommandType();
}
public String getName() {
return name;
}
public SqlCommandType getType() {
return type;
}
}
/**
* 方法签名
*/
public static class MethodSignature {
private final boolean returnsMany;
private final Class<?> returnType;
private final SortedMap<Integer, String> params;
public MethodSignature(Configuration configuration, Method method) {
this.returnType = method.getReturnType();
this.returnsMany = (configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray());
this.params = Collections.unmodifiableSortedMap(getParams(method));
}
public Object convertArgsToSqlCommandParam(Object[] args) {
final int paramCount = params.size();
if (args == null || paramCount == 0) {
// 如果没参数
return null;
} else if (paramCount == 1) {
return args[params.keySet().iterator().next().intValue()];
} else {
// 否则,返回一个ParamMap, 修改参数名,参数名就是其位置
final Map<String, Object> param = new ParamMap<>();
int i = 0;
for (Map.Entry<Integer, String> entry : params.entrySet()) {
// 1.先加一个#{0},#{1},#{2}...参数
param.put(entry.getValue(), args[entry.getKey().intValue()]);
// issue #71, add param names as param1, param2...but ensure backward compatibility
final String genericParamName = "param" + (i + 1);
if (!param.containsKey(genericParamName)) {
/*
2.再加一个#{param1},#{param2}...参数
你可以传递多个参数给一个映射器方法。
默认情况下它们将会以它们在参数列表中的位置来命名,比如:#{param1},#{param2}等
如果你想改变参数的名称(只在多参数情况下),那么你可以在参数上使用@Param("paramName")注解
*/
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
private SortedMap<Integer, String> getParams(Method method) {
// 用一个TreeMap,这样就保证还是按参数的先后顺序
final SortedMap<Integer, String> params = new TreeMap<>();
final Class<?>[] argTypes = method.getParameterTypes();
for (int i = 0; i < argTypes.length; i++) {
String paramName = String.valueOf(params.size());
// 不做 Param 的实现,这部分不处理。
params.put(i, paramName);
}
return params;
}
public boolean returnsMany() {
return returnsMany;
}
}
/**
* 参数map,静态内部类,更严格的get方法,如果没有相应的key,报错
*/
public static class ParamMap<V> extends HashMap<String, V> {
private static final long serialVersionUID = -2212268410512043556L;
@Override
public V get(Object key) {
if (!super.containsKey(key)) {
throw new RuntimeException("Parameter '" + key + "' not found. Available parameters are " + keySet());
}
return super.get(key);
}
}
}
- 映射器方法
MapperMethod#execute
会根据不同的 SqlCommand 指令调用到不同的方法上去,INSERT、UPDATE、DELETE
分别按照对应的方法调用即可。- 这里
SELECT
进行了扩展,因为需要按照不同的方法出参类型,调用不同的方法,主要是selectList、selectOne
的区别。
- 这里
- 另外这里
method.returnsMany
来自于MapperMethod.MethodSignature
方法签名中进行通过返回类型进行获取。
四、测试:完善增删改查
4.1 测试环境配置
4.1.1 配置数据源配置
mybatis-config-datasource.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<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>
<mappers>
<mapper resource="mapper/User_Mapper.xml"/>
</mappers>
</configuration>
- 修改 url 地址信息,添加
characterEncoding=utf8
中文处理
4.1.2 修改User实体类
User.java
package com.lino.mybatis.test.po;
import java.util.Date;
/**
* @description: 用户实例类
*/
public class User {
private Long id;
/**
* 用户ID
*/
private String userId;
/**
* 头像
*/
private String userHead;
/**
* 用户名称
*/
private String userName;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
public User() {
}
public User(Long id, String userId) {
this.id = id;
this.userId = userId;
}
public User(Long id, String userId, String userName) {
this.id = id;
this.userId = userId;
this.userName = userName;
}
...getter/setter
}
4.1.3 修改IUserDao用户持久层
IUserDao.java
package com.lino.mybatis.test.dao;
import com.lino.mybatis.test.po.User;
import java.util.List;
/**
* @Description: 用户持久层
*/
public interface IUserDao {
/**
* 根据ID查询用户信息
*
* @param uId ID
* @return User 用户
*/
User queryUserInfoById(Long uId);
/**
* 根据用户对象查询用户信息
*
* @param user 用户
* @return User 用户
*/
User queryUserInfo(User user);
/**
* 查询用户对象列表
*
* @return List<User> 用户列表
*/
List<User> queryUserInfoList();
/**
* 更新用户信息
*
* @param user 用户对象
* @return 受影响的行数
*/
int updateUserInfo(User user);
/**
* 新增用户信息
*
* @param user 用户对象
* @return 受影响的行数
*/
int insertUserInfo(User user);
/**
* 根据ID删除用户信息
*
* @param uId ID
* @return 受影响的行数
*/
int deleteUserInfoByUserId(String uId);
}
4.1.4 修改User_Mapper配置文件
User_Mapper.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.lino.mybatis.test.dao.IUserDao">
<select id="queryUserInfoById" parameterType="java.lang.Long" resultType="com.lino.mybatis.test.po.User">
SELECT id, userId, userName, userHead
FROM user
WHERE id = #{id}
</select>
<select id="queryUserInfo" parameterType="com.lino.mybatis.test.po.User" resultType="com.lino.mybatis.test.po.User">
SELECT id, userId, userName, userHead
FROM user
where id = #{id}
and userId = #{userId}
</select>
<select id="queryUserInfoList" resultType="com.lino.mybatis.test.po.User">
SELECT id, userId, userName, userHead
FROM user
</select>
<update id="updateUserInfo" parameterType="com.lino.mybatis.test.po.User">
UPDATE user
SET userName = #{userName}
WHERE id = #{id}
</update>
<insert id="insertUserInfo" parameterType="com.lino.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>
</mapper>
4.2 单元测试
4.2.1 插入测试
ApiTest.java
@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();
}
测试结果
16:32:32.510 [main] INFO c.l.mybatis.builder.SqlSourceBuilder - 构建参数映射 property:userId propertyType:class java.lang.String
16:32:32.510 [main] INFO c.l.mybatis.builder.SqlSourceBuilder - 构建参数映射 property:userName propertyType:class java.lang.String
16:32:32.510 [main] INFO c.l.mybatis.builder.SqlSourceBuilder - 构建参数映射 property:userHead propertyType:class java.lang.String
16:32:33.171 [main] INFO c.l.m.d.pooled.PooledDataSource - Created connention 597190999.
16:32:33.213 [main] INFO c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"10002"
16:32:33.213 [main] INFO c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"张三"
16:32:33.213 [main] INFO c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"1_05"
16:32:33.213 [main] INFO com.lino.mybatis.test.ApiTest - 测试结果:Insert OK
- 从测试日志信息和数据库的截图,可以看到数据已经插入到数据库,验证通过
- 注意:执行完 SQL 以后,还执行一次
sqlSession.commit()
。- 这是因为在
DefaultSqlSessionFactory#openSession
开启Session
创建事务工厂的时候,传入给事务工厂构造函数的事务是否自动提交为 false。 - 所以这里就需要我们去手动提交事务,否则是不会插入到数据库中的。
- 这是因为在
4.2.2 查询测试(多条数据)
ApiTest.java
@Test
public void test_queryUserInfoList() {
// 1.获取映射器对象
IUserDao userDao = sqlSession.getMapper(IUserDao.class);
// 2.测试验证: 对象参数
List<User> users = userDao.queryUserInfoList();
logger.info("测试结果:{}", JSON.toJSONString(users));
}
测试结果
16:40:47.699 [main] INFO c.l.m.s.defaults.DefaultSqlSession - 执行查询 statement:com.lino.mybatis.test.dao.IUserDao.queryUserInfoList parameter:null
16:40:48.361 [main] INFO c.l.m.d.pooled.PooledDataSource - Created connention 1192171522.
16:40:48.395 [main] INFO com.lino.mybatis.test.ApiTest - 测试结果:[{"id":1,"userHead":"1_04","userId":"10001","userName":"小零哥"},{"id":4,"userHead":"1_05","userId":"10002","userName":"张三"}]
- 现在我们再查询结果的时候,就可以查询到2条记录的集合了,这说明我们添加的
MapperMethod#execute
调用sqlSession.selectList(command.getName(), param)
是测试通过的。
4.2.3 修改测试
ApiTest.java
@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();
}
测试结果
16:44:11.979 [main] INFO c.l.m.d.pooled.PooledDataSource - Created connention 597190999.
16:44:12.027 [main] INFO c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"小灵哥"
16:44:12.028 [main] INFO c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:1
16:44:12.037 [main] INFO com.lino.mybatis.test.ApiTest - 测试结果:1
- 这里测试验证把
ID=1
的用户,userName
修改为 小灵哥,通过测试日志和数据库截图,测试通过。
4.2.4 删除测试
ApiTest.java
@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();
}
测试结果
16:47:54.591 [main] INFO c.l.m.d.pooled.PooledDataSource - Created connention 597190999.
16:47:54.633 [main] INFO c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"10002"
16:47:54.643 [main] INFO com.lino.mybatis.test.ApiTest - 测试结果:true
- 这里把数据表中
userId = '10002'
的用户删除掉,通过测试日志和数据库截图,测试通过。
五、总结:完善增删改查
- 现在手写的 Mybatis 的全部主干流程串联实现完成了,可以执行对数据库的增删改查操作。
- 本章在原有的内容下进行扩展的时候是非常方便的,甚至不需要多大的代码改动。这主要也得益于框架在设计实现过程中,合理运用设计原则和设计模式的好处。