文章目录
- 一、目标:完善增删改查
- 二、设计:完善增删改查
- 三、实现:完善增删改查
- 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.配置namespaceString namespace = element.attributeValue("namespace");if ("".equals(namespace)) {throw new RuntimeException("Mapper's namespace cannot be empty");}builderAssistant.setCurrentNamespace(namespace);// 2.配置select|insert|update|deletebuildStatementFromContext(element.elements("select"), element.elements("insert"), element.elements("update"), element.elements("delete"));}/*** 配置select|insert|update|delete** @param lists 元素列表*/@SafeVarargsprivate 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;}@Overridepublic int update(MappedStatement ms, Object parameter) throws SQLException {return doUpdate(ms, parameter);}@Overridepublic <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;@Overridepublic Transaction getTransaction() {if (closed) {throw new RuntimeException("Executor was closed.");}return transaction;}@Overridepublic void commit(boolean required) throws SQLException {if (closed) {throw new RuntimeException("Cannot commit, transaction is already closed.");}if (required) {transaction.commit();}}@Overridepublic void rollback(boolean required) throws SQLException {if (!closed) {if (required) {transaction.rollback();}}}@Overridepublic 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);}@Overrideprotected int doUpdate(MappedStatement ms, Object parameter) throws SQLException {Statement stmt = null;try {Configuration configuration = ms.getConfiguration();// 新建一个 StatementHandlerStatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);// 准备语句stmt = prepareStatement(handler);// StatementHandler.updatereturn handler.update(stmt);} finally {closeStatement(stmt);}}@Overrideprotected <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {Statement stmt = null;try {Configuration configuration = ms.getConfiguration();// 新建一个 StatementHandlerStatementHandler 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#getBoundSqlreturn 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);}@Overridepublic Statement prepare(Connection connection) {Statement statement = null;try {// 实例化 Statementstatement = 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);}@Overrideprotected Statement instantiateStatement(Connection connection) throws SQLException {String sql = boundSql.getSql();return connection.prepareStatement(sql);}@Overridepublic void parameterize(Statement statement) throws SQLException {parameterHandler.setParameters((PreparedStatement) statement);}@Overridepublic int update(Statement statement) throws SQLException {PreparedStatement ps = (PreparedStatement) statement;ps.execute();return ps.getUpdateCount();}@Overridepublic <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);}@Overrideprotected Statement instantiateStatement(Connection connection) throws SQLException {return connection.createStatement();}@Overridepublic void parameterize(Statement statement) {}@Overridepublic int update(Statement statement) throws SQLException {String sql = boundSql.getSql();statement.execute(sql);return statement.getUpdateCount();}@Overridepublic <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;}@Overridepublic <T> T selectOne(String statement) {return this.selectOne(statement, null);}@Overridepublic <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;}}@Overridepublic <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);}}@Overridepublic int insert(String statement, Object parameter) {// 在 Mybatis 中 insert 调用的是 updatereturn update(statement, parameter);}@Overridepublic 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);}}@Overridepublic Object delete(String statement, Object parameter) {return update(statement, parameter);}@Overridepublic void commit() {try {executor.commit(true);} catch (SQLException e) {throw new RuntimeException("Error committing transaction. Cause: " + e);}}@Overridepublic <T> T getMapper(Class<T> type) {return configuration.getMapper(type, this);}@Overridepublic 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 compatibilityfinal 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;@Overridepublic 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, userHeadFROM userWHERE id = #{id}</select><select id="queryUserInfo" parameterType="com.lino.mybatis.test.po.User" resultType="com.lino.mybatis.test.po.User">SELECT id, userId, userName, userHeadFROM userwhere id = #{id}and userId = #{userId}</select><select id="queryUserInfoList" resultType="com.lino.mybatis.test.po.User">SELECT id, userId, userName, userHeadFROM user</select><update id="updateUserInfo" parameterType="com.lino.mybatis.test.po.User">UPDATE userSET 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">DELETEFROM userWHERE 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 的全部主干流程串联实现完成了,可以执行对数据库的增删改查操作。
- 本章在原有的内容下进行扩展的时候是非常方便的,甚至不需要多大的代码改动。这主要也得益于框架在设计实现过程中,合理运用设计原则和设计模式的好处。