1.目的
这一章的目的主要是插入语句以后返回插入记录的id,因为插入语句可分为要返回记录id,不要返回记录id的以及不同数据源类型执行的时机也不同(如:oracle不支持主键,需要先插入序列再增加,Mysql支持主键增加一条记录就会有索引)。
如下图,insert里包含selectKey,由selectKey去执行查询此次新增的id记录,我们看到selectKey标签上的属性有keyProperty、order、resultType。
- keyProperty这个是把返回索引的索引放入id里
- order为after则是在insert后执行selectKey
- resultType则是查询返回的类型,示例里为Long
我们最主要的目的就是解析这样的selectKey,然后存入MappedStatement里到时执行时使用,执行Update时则执行selectKey的方法,最后在activity实体id赋值执行完的记录id。
2.设计说明
标黄色的是修改的,其余都是新增的方法。
1.构建时(builder)
在构建(builder)时,我们需要解析下selectKey的标签,而selectKey是在语句里,所以需要更改XMLStatementBuilder类添加解析selectKey的标签。最后把解析好的标签放入到addMappedStatement(),这里的动作是两次addMappedStatement(),为什么是两次呢?ps:可以看下面两个图。
- 第一次,SqlCommandType是“SELECT”,代表selectKey本身的语句,需要存储一次MappedStatement
- 第二次,SqlCommandType是“INSERT”,代表是selectKey父级的INSERT标签,它下面要包含这个selectKey的MappedStatement信息,然后如果有selectKey则创建SelectKeyGenerator对象,需要再一次存储MappedStatement。
ps此处看不懂可以调试全局看下,多看几遍就懂了为什么这样设计了。
2.selectKey执行时(executor keygen)
2.1 定义接口:
针对上述情况,需要定义接口KeyGenerator,定义方法为processBefore()(针对Oracle不支持主键的)和processAfter()(支持主键的,如MySql)
2.2 实现接口:
NoKenGenerator和SelectKeyGenerator,不要求返回记录的就执行NoKenGenerator(实现方法什么都不操作)。SelectKeyGenerator的实现则是执行查询索引操作,并将结果添加到insert的操作的实体里,这样就得到了索引。
3.执行时(executor)
我们存储到MappedStatement以后就要流转到执行时,因为SelectKey是查询,所以在Executor时
添加了重载的query()方法。
BaseExecutor实现新的query()方法,拿到了sql语句后,调用原有的doQuery,由SimpleExecutor类实现,此时就会调用BaseStatementHandler构造方法,这里新添加了generateKeys()方法,主要是检查下是否需要在insert前执行selectKey(如:Mysql是insert后,Oracle是insert前)。
执行了新增方法时,则需要添加调用KeyGenerator的processAfter()方法,这个方法依然要检查是否在insert后执行selectKey。
4.结果集的更改(resultset)
由于之前的结果集是只处理bean对象, 一般返回selectKey需要的基本类型,如ID是Long,所以这里需要添加createPrimitiveResultObject()方法获取类型处理器。
在createResultObject()此方法里则需要判断是否是基本类型,如果基本类型则调用createPrimitiveResultObject()。其余类型则还按之前代码走。
5.connection,连接的更改
由于要两个语句用一个连接如果两个连接则无法返回id索引,所以需要在获取连接处判断下是否有连接,如果有就不创建,没有就创建。
3.代码
3.1 selectKey执行操作
包:package cn.bugstack.mybatis.executor.keygen;
添加接口KeyGenerator,主要是用来执行SelectKey查询操作的,此接口定义了两个方法,
processBefore:是在不支持主键需要获取序列时调用,执行insert语句前调用
processAfter:是在支持主键在执行insert语句后嗲用
public interface KeyGenerator {/*** 针对Sequence主键而言,在执行insert sql前必须指定一个主键值给要插入的记录,* 如Oracle、DB2,KeyGenerator提供了processBefore()方法。*/void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter);/*** 针对自增主键的表,在插入时不需要主键,而是在插入过程自动获取一个自增的主键,* 比如MySQL、PostgreSQL,KeyGenerator提供了processAfter()方法*/void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
}
实现类SelectKeyGenera,有selectKey时实现执行selectKey的逻辑。
// step13新增
public class SelectKeyGenerator implements KeyGenerator {public static final String SELECT_KEY_SUFFIX = "!selectKey";private boolean executeBefore;private MappedStatement keyStatement;public SelectKeyGenerator(MappedStatement keyStatement, boolean executeBefore) {this.executeBefore = executeBefore;this.keyStatement = keyStatement;}@Overridepublic void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {if (executeBefore) {processGeneratedKeys(executor, ms, parameter);}}@Overridepublic void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {if (!executeBefore) {processGeneratedKeys(executor, ms, parameter);}}/*** 执行selectKey的SQL语句,执行完毕后把返回的id结果放入对应实体中。*/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);}}private void handleMultipleProperties(String[] keyProperties,MetaObject metaParam, MetaObject metaResult) {String[] keyColumns = keyStatement.getKeyColumns();if (keyColumns == null || keyColumns.length == 0) {for (String keyProperty : keyProperties) {setValue(metaParam, keyProperty, metaResult.getValue(keyProperty));}} else {if (keyColumns.length != keyProperties.length) {throw new RuntimeException("If SelectKey has key columns, the number must match the number of key properties.");}for (int i = 0; i < keyProperties.length; i++) {setValue(metaParam, keyProperties[i], metaResult.getValue(keyColumns[i]));}}}private void setValue(MetaObject metaParam, String property, Object value) {if (metaParam.hasSetter(property)) {metaParam.setValue(property, value);} else {throw new RuntimeException("No setter found for the keyProperty '" + property + "' in " + metaParam.getOriginalObject().getClass().getName() + ".");}}
}
实现类NoKeyGenerator,在没有selectKey时的逻辑也就是不写具体的逻辑。
public class NoKeyGenerator implements KeyGenerator {@Overridepublic void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {// Do Nothing}@Overridepublic void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {// Do Nothing}
}
3.2 builder操作部分的修改
XMLStatementBuilder修改parseStatementNode(),添加了解析selectKey的节点方法processSelectKeyNodes();
如果有多个就遍历每个节点信息存储到MappedStatement下
public class XMLStatementBuilder extends BaseBuilder {// 省略其他public void parseStatementNode() {// 省略其他// 解析<selectKey> step-13 新增processSelectKeyNodes(id, parameterTypeClass, langDriver);}/*** 解析selectKey标签*/private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) {// 得到selectKey标签List<Element> selectKeyNodes = element.elements("selectKey");parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver);}/*** for循环遍历selectKey标签* 取出selectKey的父id拼接select的Key标识*/private void parseSelectKeyNodes(String parentId, List<Element> list, Class<?> parameterTypeClass, LanguageDriver langDriver) {for (Element nodeToHandle : list) {String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver);}}/*** 开始解析每一个selectKey标签的内容,并存储MappedStatement里* <selectKey keyProperty="id" order="AFTER" resultType="long">* SELECT LAST_INSERT_ID()* </selectKey>*/private void parseSelectKeyNode(String id, Element nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver) {String resultType = nodeToHandle.attributeValue("resultType");// 得到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);// SELECT标签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);// 将KeyGenerator放入map中configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));}
}
MapperAnnotationBuilder类,注解版的构建也是一样的,需要修改解析的语句,方法为parseStatement()
public class MapperAnnotationBuilder {private void parseStatement(Method method) {// 省略其他// step-13 新增-----------------------------------------------------------KeyGenerator keyGenerator;String keyProperty = "id";if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {keyGenerator = configuration.isUseGeneratedKeys() ? new Jdbc3KeyGenerator() : new NoKeyGenerator();} else {keyGenerator = new NoKeyGenerator();}// step-14 新增-----------------------------------------------------------boolean isSelect = sqlCommandType == SqlCommandType.SELECT;String resultMapId = null;if (isSelect) {resultMapId = parseResultMap(method);}// step-13 部分参数新增( keyGenerator,keyProperty,)-------------------------// 调用助手类assistant.addMappedStatement(mappedStatementId,sqlSource,sqlCommandType,parameterTypeClass,resultMapId,getReturnType(method),keyGenerator,keyProperty,languageDriver);}
}
因为MappedStatement要存储数据,所以MappedStatement要加相应的字段,keyGenerator以及keyProperties和keyColumns
public class MappedStatement {// 其余省略// step-13 新增private String resource;private KeyGenerator keyGenerator;private String[] keyProperties;private String[] keyColumns;public static class Builder {public Builder(Configuration configuration, String id, SqlCommandType sqlCommandType, SqlSource sqlSource, Class<?> resultType) {mappedStatement.keyGenerator = configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType) ? new Jdbc3KeyGenerator() : new NoKeyGenerator();}public Builder resource(String resource) {mappedStatement.resource = resource;return this;}public Builder keyGenerator(KeyGenerator keyGenerator) {mappedStatement.keyGenerator = keyGenerator;return this;}public Builder keyProperty(String keyProperty) {mappedStatement.keyProperties = delimitedStringToArray(keyProperty);return this;}}public String[] getKeyColumns() {return keyColumns;}public String[] getKeyProperties() {return keyProperties;}public KeyGenerator getKeyGenerator() {return keyGenerator;}
}
MapperBuilderAssistant类:此类修改了addMappedStatement()方法需要增加几个参数,并把参数放入MappedStatement中。
public MappedStatement addMappedStatement(String id,SqlSource sqlSource,SqlCommandType sqlCommandType,Class<?> parameterType,String resultMap,Class<?> resultType,KeyGenerator keyGenerator,String keyProperty,LanguageDriver lang) {// 给id加上namespace前缀:cn.bugstack.mybatis.test.dao.IUserDao.queryUserInfoByIdid = applyCurrentNamespace(id, false);MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlCommandType, sqlSource, resultType);// step-13新增/添加三个属性statementBuilder.resource(resource);statementBuilder.keyGenerator(keyGenerator);statementBuilder.keyProperty(keyProperty);// 结果映射,给 MappedStatement#resultMapssetStatementResultMap(resultMap, resultType, statementBuilder);MappedStatement statement = statementBuilder.build();// 映射语句信息,建造完存放到配置项中configuration.addMappedStatement(statement);return statement;}
3,3 insert执行SQL部分修改
包:package cn.bugstack.mybatis.executor
Executor接口需要新增个query方法,SelectKeyGenerator执行查询时使用。
// step-13新增-----------------------------------<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
BaseExecutor:基础实现类去实现这个query方法,然后调用原有的其他的query方法,然后顺着会调用SimpleExecutor的doQuery()这里的处理没有改变,所以就不提供代码拉。这块就和查询逻辑一样。
// step-14新增-----------------------------------------@Overridepublic <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {BoundSql boundSql = ms.getBoundSql(parameter);return query(ms, parameter, rowBounds, resultHandler, boundSql);}
在上面SimpleExecutor执行doQuery()时,需要调用到BaseStatementHandler的构造方法,此时就需要检查一下是否需要在insert前要执行,如果需要就执行SelectKey,selectKey又会调用上边的query进行查询操作,如果否则不会执行任何操作,判断则在keyGenerator.processBefore()处理。
public abstract class BaseStatementHandler implements StatementHandler {public BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {if (boundSql == null) {// 针对Sequence主键而言,在执行insert sql前必须指定一个主键值给要插入的记录。generateKeys(parameterObject);}}// 针对Sequence主键而言,在执行insert sql前必须指定一个主键值给要插入的记录。KeyGenerator#processBeforeprotected void generateKeys(Object parameter) {KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();keyGenerator.processBefore(executor, mappedStatement, null, parameter);}
}
然后就要修改下执行insert语句了,在Mybatis中所有的insert、del、update,都统一为update方法,所以我们需要修改下update(),修改为执行完insert操作后调用keyGenerator.processAfter(),后置处理(有主键情况下使用),修改如下:
PreparedStatementHandler类
包:package cn.bugstack.mybatis.executor.statement;
@Overridepublic int update(Statement statement) throws SQLException {PreparedStatement ps = (PreparedStatement) statement;ps.execute();// step-13新增----------------------------------------// 执行 selectKey 语句int rows = ps.getUpdateCount();Object parameterObject = boundSql.getParameterObject();KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);// step-13新增----------------------------------------return rows;}
最后最重要的是,执行insert再执行查询当前插入记录的主键,必须是一个连接,如果是两个连接结果就是空,所以需要更改获取连接的操作,这里很简单,判断下就行了。
修改的包:cn.bugstack.mybatis.transaction.jdbc
修改的类:JdbcTransaction
@Overridepublic Connection getConnection() throws SQLException {// step-13新增------------------------------------// 本章节新增;多个SQL在同一个JDBC连接下,才能完成事务特性if (connection != null) {return connection;}// step-13新增------------------------------------connection = dataSource.getConnection();connection.setTransactionIsolation(level.getLevel());connection.setAutoCommit(autoCommit);return connection;}
4.准备测试
xml如下:
<insert id="insert" parameterType="cn.bugstack.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>
单元测试如下:
private Logger logger = LoggerFactory.getLogger(ApiTest.class);private SqlSession sqlSession;@Beforepublic void init() throws IOException {// 1. 从SqlSessionFactory中获取SqlSessionSqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config-datasource.xml"));sqlSession = sqlSessionFactory.openSession();}@Testpublic void test_insert() {// 1. 获取映射器对象IActivityDao dao = sqlSession.getMapper(IActivityDao.class);Activity activity = new Activity();activity.setActivityId(10004L);activity.setActivityName("测试活动");activity.setActivityDesc("测试数据插入");activity.setCreator("xdf");// 2. 测试验证Integer res = dao.insert(activity);sqlSession.commit();logger.info("测试结果:count:{} idx:{}", res, JSON.toJSONString(activity.getId()));}
执行结果:
执行的两条都展示了正确的索引。