前言
技术的把控,往往都是体现在细节上!
如果说能用行,复制粘贴就能完成需求,出错了就手忙脚乱。那你一定不是一个高级开发,对很多的技术细节也都不了解。
目标
在前面所有的章节内容对 ORM 框架的实现中,其中对 SQL 的 insert/delete/update/select 操作都是一条执行语句。
那这样有什么问题吗?这里到没有什么问题,主要的特征在于与本章节要实现的内容上想对照来看,本章节要实现的是在执行插入 SQL 后要返回此条插入语句后的自增索引。
当一次数据库操作有2条执行 SQL 语句的时候,重点在于必须在同一个 DB 连接下,否则将会失去事务的特性。也就表示着,如果不是同一 DB 连接下,那么返回的自增ID将会是一个 0 值。
以解析 Mapper XML 为入口处理 insert/delete/update/select 类型的 SQL 为入口,获取 selectKey 标签,并对此标签内的 SQL 进行解析封装。把它也当成一个查询操作,封装成映射器语句。注意:这里只会对 insert 标签起作用,其他标签并不会配置 selectKey 的操作。
当把 selectKey 解析完成以后,也是像解析其他类型的标签一样,按照 MappedStatement 映射器语句存放到 Configuration 配置项中,这样后面执行 DefaultSqlSession 获取 SQL 的时候就可以从配置项获取了,并在执行器中完成 SQL 的操作。这里要注意,对于键值的处理,是单独包装的 KeyGenerator 键值生成器,完成 SQL 的调用和结果封装的。
创建键值生成器
键值生成器 KeyGenerator 接口和对于的实现类,是用于包装对 Mapper XML insert 标签中 selectKey 下语句的处理。这个接口由3个实现类,包括 :NoKeyGenerator、Jdbc3KeyGenerator、SelectKeyGenerator,不过我们本章节只会用到 SelectKeyGenerator 以及在默认没有 selectKey 标签的情况下,使用 NoKeyGenerator 进行替代。
1:NoKeyGenerator:默认空实现不对主键单独处理。
2:Jdbc3KeyGenerator:主要用于数据库的自增主键,比如 MySQL、PostgreSQL
3:SelectKeyGenerator:主要用于数据库不支持自增主键的情况,比如 Oracle、DB2。
接口定义
public interface KeyGenerator {void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter);void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter);}
接口实现
public class SelectKeyGenerator implements KeyGenerator {public static final String SELECT_KEY_SUFFIX = "!selectKey";private boolean executeBefore;private MappedStatement keyStatement;// ... 省略方法@Overridepublic void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {if (!executeBefore) {processGeneratedKeys(executor, ms, parameter);}}private void processGeneratedKeys(Executor executor, MappedStatement ms, Object parameter) {try {if (parameter != null && keyStatement != null && keyStatement.getKeyProperties() != null) {String[] keyProperties = keyStatement.getKeyProperties();final Configuration configuration = ms.getConfiguration();final MetaObject metaParam = configuration.newMetaObject(parameter);if (keyProperties != null) {Executor keyExecutor = configuration.newExecutor(executor.getTransaction());List<Object> values = keyExecutor.query(keyStatement, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);if (values.size() == 0) {throw new RuntimeException("SelectKey returned no data.");} else if (values.size() > 1) {throw new RuntimeException("SelectKey returned more than one value.");} else {MetaObject metaResult = configuration.newMetaObject(values.get(0));if (keyProperties.length == 1) {if (metaResult.hasGetter(keyProperties[0])) {setValue(metaParam, keyProperties[0], metaResult.getValue(keyProperties[0]));} else {setValue(metaParam, keyProperties[0], values.get(0));}} else {handleMultipleProperties(keyProperties, metaParam, metaResult);}}}}} catch (Exception e) {throw new RuntimeException("Error selecting key or setting result to parameter object. Cause: " + e);}}}
SelectKeyGenerator 核心实现主要体现在 processAfter 方法对 processGeneratedKeys 的调用处理。在这个方法的调用过程中,通过从配置项中获取 JDBC 链接和 Executor 执行器。之后使用执行器对传入进来的 MappedStatement 执行处理,也就是对应的 keyStatement 参数。
同和前面章节讲解执行 select 语句一样,在通过执行器 keyExecutor.query 获取到结果以后,使用 MetaObject 反射工具类,向对象的属性设置查询结果。这个封装的结果,就是封装到了入参对象中对应的字段上,比如用户对象的id字段
解析selectKey
selectKey 标签主要用在 Mapper XML 中的 insert 语句里,所以我们在解析这段内容的时候,主要是对 XMLStatementBuilder XML 语句构建器的解析过程进行扩展。
public void parseStatementNode() {// ... 省略部分处理 // 解析<selectKey> 本章节新增内容processSelectKeyNodes(id, parameterTypeClass, langDriver);// 解析成SqlSource,DynamicSqlSource/RawSqlSourceSqlSource sqlSource = langDriver.createSqlSource(configuration, element, parameterTypeClass);// 属性标记【仅对 insert 有用】, MyBatis 会通过 getGeneratedKeys 或者通过 insert 语句的 selectKey 子元素设置它的值 本章节新增String keyProperty = element.attributeValue("keyProperty");KeyGenerator keyGenerator = null;String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);if (configuration.hasKeyGenerator(keyStatementId)) {keyGenerator = configuration.getKeyGenerator(keyStatementId);} else {keyGenerator = configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType) ? new Jdbc3KeyGenerator() : new NoKeyGenerator();}// 调用助手类 builderAssistant.addMappedStatement(...)
}
通过 parseStatementNode 解析 insert/delete/update/select 标签方法,扩展对 selectKey 标签的处理。processSelectKeyNodes 方法是专门用于处理 selectKey 标签下的语句的。
另外是对 keyProperty 的初始操作,因为很多时候对 SQL 的解析里面并没有 selectKey 以及获取自增主键结果的返回处理,那么这个时候会采用默认的 keyGenerator 获取处理,通常都会是实例化 NoKeyGenerator 赋值。
selectKey 处理
<selectKey keyProperty="id" order="AFTER" resultType="long">
SELECT LAST_INSERT_ID()
</selectKey>
XMLStatementBuilder#parseSelectKeyNode XML语句构建器对应的 parseSelectKeyNode 专门用于解析 selectKey 标签下的 SQL 以及返回类型进行封装。
private void parseSelectKeyNode(String id, Element nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver) {String resultType = nodeToHandle.attributeValue("resultType");Class<?> resultTypeClass = resolveClass(resultType);boolean executeBefore = "BEFORE".equals(nodeToHandle.attributeValue("order", "AFTER"));String keyProperty = nodeToHandle.attributeValue("keyProperty");// defaultString resultMap = null;KeyGenerator keyGenerator = new NoKeyGenerator();// 解析成SqlSource,DynamicSqlSource/RawSqlSourceSqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);SqlCommandType sqlCommandType = SqlCommandType.SELECT;// 调用助手类builderAssistant.addMappedStatement(id,sqlSource,sqlCommandType,parameterTypeClass,resultMap,resultTypeClass,keyGenerator,keyProperty,langDriver);// 给id加上namespace前缀id = builderAssistant.applyCurrentNamespace(id, false);// 存放键值生成器配置MappedStatement keyStatement = configuration.getMappedStatement(id);configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
}
在 parseSelectKeyNode 中先进行对 selectKey 标签上,resultType、keyProperty 等属性的解析,之后解析 SQL 封装成 SqlSource,最后阶段是对解析信息的保存处理。分别存放成 MappedStatement 映射器语句、SelectKeyGenerator 键值生成器。
扩展预处理语句处理器
StatementHandler 语句处理器接口所定义的方法,在 SQL 执行上只有 update 和 query,所以我们要扩展的 insert 操作,也是对 update 方法的扩展操作处理。
public int update(Statement statement) throws SQLException {// 1. 执行 insert/delete/updatePreparedStatement ps = (PreparedStatement) statement;ps.execute();int rows = ps.getUpdateCount();// 2. 执行 selectKey 语句Object parameterObject = boundSql.getParameterObject();KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);return rows;
}
JDBC链接获取
由于是在同一个操作下,处理两条SQL,分别是插入和返回索引值。那么这两条 SQL 其实要在同一个链接下才能正确的获取到结果,也就是保证了一个事务的特性。
@Override
public Connection getConnection() throws SQLException {// 本章节新增;多个SQL在同一个JDBC连接下,才能完成事务特性if (null != connection) {return connection;}connection = dataSource.getConnection();connection.setTransactionIsolation(level.getLevel());connection.setAutoCommit(autoCommit);return connection;
}
也就是 JdbcTransaction#getConnection 方法,在前面章节中,我们实现时候只是一个 dataSource.getConnection() 获取链接,这样就相当于每次获得的连接都是一个新的连接。那么两条SQL的执行分别在各自的JDBC连接下,则不会正确的返回插入后的索引值。
所以这里我们进行判断,如果连接不为空,则不在创建新的JDBC连接,使用当前连接即可。这里的情况和 Spring 中的事务处理是一样的,Spring中需要在 ThreadLocal 保存连接
测试
配置Mapper XML 语句
<insert id="insert" parameterType="com.lm.mybatis.test.po.Activity">INSERT INTO activity(activity_id, activity_name, activity_desc, create_time, update_time)VALUES (#{activityId}, #{activityName}, #{activityDesc}, now(), now())<selectKey keyProperty="id" order="AFTER" resultType="long">SELECT LAST_INSERT_ID()</selectKey>
</insert>
在 insert 标签下,添加 selectKey 标签,并使用 SELECT LAST_INSERT_ID() 查询方法返回自增索引值。这个值会返回到入参对象 Activity.id 中
单元测试
@Test
public void test_insert() {// 1. 获取映射器对象IActivityDao dao = sqlSession.getMapper(IActivityDao.class);Activity activity = new Activity();activity.setActivityId(10004L);activity.setActivityName("测试活动");activity.setActivityDesc("测试数据插入");activity.setCreator("xiaofuge");// 2. 测试验证Integer res = dao.insert(activity);sqlSession.commit();logger.info("测试结果:count:{} idx:{}", res, JSON.toJSONString(activity.getId()));
}
总结
是在原有的 Mapper XML 对各类标签语句的解析中,对 insert 操作进行扩展,添加新的标签 selectKey 并通过这样一个标签的解析、执行、封装处理把最终的插入索引结果返回到入参对象的对应属性字段上。那么同时我们所处理的是类似 Mysql 这样带有自增索引的数据库,用这样的方式来串联起整个流程。
另外这里要注意,我们本章节是首次在一个操作中执行2条SQL语句,为了能让最后可以查询到自增索引,那么这两条 SQL 必须是在同一个链接下。读者在学习的过程中,可以尝试将 JdbcTransaction#getConnection 方法中的判断是否获取新的 JDBC 连接去掉,每次都获取最新的连接,运行测试看是否还能获得到插入后的索引值。
好了到这里就结束了手写mybatis之返回Insert操作自增索引值的学习,大家一定要跟着动手操作起来。需要源码的 可si我获取;