手写Mybatis:第15章-返回Insert操作自增索引值
文章目录
- 一、目标:Insert自增索引值
- 二、设计:Insert自增索引值
- 三、实现:Insert自增索引值
- 3.1 工程结构
- 3.2 Insert自增索引值类图
- 3.3 修改执行器
- 3.3.1 修改执行器接口
- 3.3.2 抽象执行器基类
- 3.4 键值生成器
- 3.4.1 键值生成器接口
- 3.4.2 不用键值生成器
- 3.4.3 使用JDBC3键值生成器
- 3.4.4 不自增键值生成器
- 3.5 配置和映射中添加键值生成器
- 3.5.1 映射器语句类
- 3.5.2 配置项
- 3.5.3 默认Map结果处理器
- 3.6 解析selectkey
- 3.6.1 修改语句处理器抽象基类
- 3.6.2 预处理语句处理器
- 3.6.3 注解配置构建器
- 3.6.4 映射构建器助手
- 3.6.5 XML语句构建器
- 3.7 JDBC类型和类型处理器注册机修改
- 3.7.1 JDBC类型
- 3.7.2 类型处理器注册机
- 3.8 JDBC链接获取
- 四、测试:Insert自增索引值
- 4.1 测试环境配置
- 4.1.1 修改DAO持久层
- 4.1.2 修改mapper配置文件
- 4.2 单元测试
- 4.2.1 单元测试
- 4.2.2 插入查询测试
- 五、总结:Insert自增索引值
一、目标:Insert自增索引值
💡 在执行插入 SQL 后要返回此条插入语句后的自增索引?
- 当一次数据库操作有2条执行 SQL 语句的时候,重点在于必须在同一个 DB 连接下,否则将会失去事务的特性。
- 也就表示着,如果不是同一 DB 连接下,返回的自增 ID 将会是一个
0
值。
- 也就表示着,如果不是同一 DB 连接下,返回的自增 ID 将会是一个
- 目标:在解析 Mapper 配置文件以后,调用预处理语句处理器执行 SQL 时,需要在同一个链接下进行处理。
二、设计:Insert自增索引值
💡 在执行插入 SQL 后返回插入的索引值。
- 需要在
insert
标签中新增selectKey
标签,并在selectKey
标签中执行查询数据库索引的操作。 - 所以基于这样的新增标签,则会在 XML 语句构建器中添加对
selectKey
标签的解析,并同样也需要把新增的解析映射器语句存放到配置项中。 - 最终到执行 SQL 时,在一个 DB 连接下,完成两个 SQL 的操作。
- 以解析 Mapper XML 为入口处理
insert/delete/update/select
类型的 SQL 为入口,获取selectKey
标签,并对此标签内的 SQL 进行解析封装。- 把它当成一个查询操作,封装成映射语句。
- 注意:这里只会对
insert
标签起作用,其他标签并不会配置selectKey
的操作。
- 当把
selectKey
解析完成以后,也是像解析其他类型的标签一样,按照 MappedStatement 映射器语句存放到 Configuration 配置项中。- 这样后面执行 DefaultSqlSession 获取 SQL 的时候就可以从配置项获取,并在执行器完成 SQL 的操作。
- 注意:对于键值的处理,是单独包装的 KeyGenerator 键值生成器,完成 SQL 的调用和结果封装的。
- 另外由于这里执行了2条 SQL 语句,一条插入,一条查询。而2条 SQL 必须在同一个 DB 连接下,才能把正确的索引值返回回来。
- 因为这样是保证了一个事务的特性,否则是查询不到插入的索引值的。
- 注意:获取链接是在
JdbcTransaction#getConnection
方法获取的,只有获取的链接是已经存在的同一个才能正确执行返回结果。
三、实现:Insert自增索引值
3.1 工程结构
mybatis-step-14
|-src|-main| |-java| |-com.lino.mybatis| |-annotations| | |-Delete.java| | |-Insert.java| | |-Select.java| | |-Update.java| |-binding| | |-MapperMethod.java| | |-MapperProxy.java| | |-MapperProxyFactory.java| | |-MapperRegistry.java| |-builder| | |-annotations| | | |-MapperAnnotationBuilder.java| | |-xml| | | |-XMLConfigBuilder.java| | | |-XMLMapperBuilder.java| | | |-XMLStatementBuilder.java| | |-BaseBuilder.java| | |-MapperBuilderAssistant.java| | |-ParameterExpression.java| | |-ResultMapResolver.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| | |-keygen| | | |-Jdbc3KeyGenerator.java| | | |-KeyGenerator.java| | | |-NoKeyGenerator.java| | | |-SelectKeyGenerator.java| | |-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| | |-ResultFlag.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| | |-DateTypeHandler.java| | |-IntegerTypeHandler.java| | |-JdbcType.java| | |-LongTypeHandler.java| | |-StringTypeHandler.java| | |-TypeAliasRegistry.java| | |-TypeHandler.java| | |-TypeHandlerRegistry.java|-test|-java| |-com.lino.mybatis.test| |-dao| | |-IActivityDao.java| |-po| | |-Activity.java| |-ApiTest.java|-resources|-mapper| |-Activity_Mapper.xml|-mybatis-config-datasource.xml
3.2 Insert自增索引值类图
- 在整个核心实现类中,以 XMLStatmentBuilder 新增
processSelectKeyNodes
节点解析,构建 MappedStatement 映射类语句。 - 此处的映射类语句会通过 KeyGenerator 键值生成器实现类包装。
- 它的包装主要用于负责对
selectkey
标签中 SQL 语句的查询和结果封装。 - 类似于 DefaultSqlSession 调用 Executor 执行器的过程。
- 它的包装主要用于负责对
- 另外整个
insert
的执行,需要在 PreparedStatementHandler 预处理语句处理器的update
方法进行扩充,执行完插入操作,再执行查询处理。- 方法的返回值返回的是 SQL 影响的条数,入参中的对象属性与
selectKey
配置的一样的名称字段,会被填充索引值返回。 - 注意:在 Mybatis 框架中,所有的 SQL 操作,在执行器中只有
select
和update
,也就是insert/delete/update
都被update
封装处理了。
- 方法的返回值返回的是 SQL 影响的条数,入参中的对象属性与
3.3 修改执行器
3.3.1 修改执行器接口
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 参数* @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;/*** 查询** @param ms 映射器语句* @param parameter 参数* @param rowBounds 分页记录限制* @param resultHandler 结果处理器* @param <E> 返回的类型* @return List<E>* @throws SQLException SQL异常*/<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;...
}
3.3.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 {...@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);}@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);}/*** 更新方法** @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;...
}
3.4 键值生成器
- 键值生成器 KeyGenerator 接口和对应的实现类,用于包装对 Mapper XML insert 标签中
selectKey
下语句的处理。 - 这个接口有3个实现类。
NoKeyGenerator
:默认空实现不对主键单独处理。Jdbc3keyGenerator
:主要用于数据库的自增主键,比如 MySQL、PostgreSQL。SelectKeyGenerator
:主要用于数据库不支持自增主键的情况,比如 Oracle、DB2
3.4.1 键值生成器接口
KeyGenerator.java
package com.lino.mybatis.executor.keygen;import com.lino.mybatis.executor.Executor;
import com.lino.mybatis.mapping.MappedStatement;
import java.sql.Statement;/*** @description: 键值生成器接口*/
public interface KeyGenerator {/*** 针对Sequence主键而言,在执行insert sql前必须指定一个主键值给要插入的记录* 如Oracle、DB2,KeyGenerator提供了processBefore()方法。** @param executor 执行器* @param ms 映射器语句类* @param stmt 语句类* @param parameter 参数*/void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter);/*** 针对自增主键主键的表,在插入时不需要主键,而是在插入过程自动获取一个自增的主键* 比如MySQL、PostgreSQL,KeyGenerator提供了processAfter()方法** @param executor 执行器* @param ms 映射器语句类* @param stmt 语句类* @param parameter 参数*/void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
}
- 在键值生成器接口中定义了2个方法:
processBefore、processAfter
processBefore
:针对 Sequence 主键而言,在执行insert sql
前必须指定一个主键值给要插入的记录。- 比如:Oracle、DB2,KeyGenerator 提供了
processBefore()
方法。
- 比如:Oracle、DB2,KeyGenerator 提供了
processAfter
:针对自增主键的表,在插入时不需要主键,而是在插入过程自动获取一个自增的主键。- 比如:MySQL、PostgreSQL,KeyGenerator 提供了
processAfter()
方法。
- 比如:MySQL、PostgreSQL,KeyGenerator 提供了
3.4.2 不用键值生成器
NoKeyGenerator.java
package com.lino.mybatis.executor.keygen;import com.lino.mybatis.executor.Executor;
import com.lino.mybatis.mapping.MappedStatement;
import java.sql.Statement;/*** @description: 不用键值生成器*/
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.4.3 使用JDBC3键值生成器
Jdbc3KeyGeneration.java
package com.lino.mybatis.executor.keygen;import com.lino.mybatis.executor.Executor;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.reflection.MetaObject;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.type.TypeHandler;
import com.lino.mybatis.type.TypeHandlerRegistry;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;/*** @description: 使用 JDBC3 Statement.getGeneratedKeys*/
public class Jdbc3KeyGenerator 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) {}public void processBatch(MappedStatement ms, Statement stmt, List<Object> parameters) {try (ResultSet rs = stmt.getGeneratedKeys()) {final Configuration configuration = ms.getConfiguration();final TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();final String[] keyProperties = ms.getKeyProperties();final ResultSetMetaData rsmd = rs.getMetaData();TypeHandler<?>[] typeHandlers = null;if (keyProperties != null && rsmd.getColumnCount() >= keyProperties.length) {for (Object parameter : parameters) {// there should be one row for each statement (also one for each parameter)if (!rs.next()) {break;}final MetaObject metaParam = configuration.newMetaObject(parameter);if (typeHandlers == null) {// 先取得类型处理器typeHandlers = getTypeHandlers(typeHandlerRegistry, metaParam, keyProperties);}// 填充键值populateKeys(rs, metaParam, keyProperties, typeHandlers);}}} catch (Exception e) {throw new RuntimeException("Error getting generated key or setting result to parameter object. Cause: " + e, e);}}private TypeHandler<?>[] getTypeHandlers(TypeHandlerRegistry typeHandlerRegistry, MetaObject metaParam, String[] keyProperties) {TypeHandler<?>[] typeHandlers = new TypeHandler<?>[keyProperties.length];for (int i = 0; i < keyProperties.length; i++) {if (metaParam.hasSetter(keyProperties[i])) {Class<?> keyPropertyType = metaParam.getSetterType(keyProperties[i]);TypeHandler<?> th = typeHandlerRegistry.getTypeHandler(keyPropertyType, null);typeHandlers[i] = th;}}return typeHandlers;}private void populateKeys(ResultSet rs, MetaObject metaParam, String[] keyProperties, TypeHandler<?>[] typeHandlers) throws SQLException {for (int i = 0; i < keyProperties.length; i++) {TypeHandler<?> th = typeHandlers[i];if (th != null) {Object value = th.getResult(rs, i + 1);metaParam.setValue(keyProperties[i], value);}}}
}
3.4.4 不自增键值生成器
SelectKeyGeneration.java
package com.lino.mybatis.executor.keygen;import com.lino.mybatis.executor.Executor;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.reflection.MetaObject;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.session.RowBounds;
import java.sql.Statement;
import java.util.List;/*** @description: 键值生成器*/
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);}}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() + ".");}}
}
- SelectKeyGenerator 核心实现主要体现在
processAfter
方法对processGeneratorKeys
的调用处理。- 这个方法的调用过程中,通过从配置项中获取 JDBC 链接和 Executor 执行器。
- 之后使用执行器对传入进来的 MappedStatement 执行处理,也就是对应的
keyStatement
参数。
- 和执行
select
语句一样,在通过执行器keyExecutor.query
获取到结果以后,使用 MetaObject 反射工具类,向对象属性设置查询结果。- 这个封装的结果,就是封装到了入参对象中对应的字段上,比如:用户对象的
id
字段。
- 这个封装的结果,就是封装到了入参对象中对应的字段上,比如:用户对象的
3.5 配置和映射中添加键值生成器
3.5.1 映射器语句类
MappedStatement.java
package com.lino.mybatis.mapping;import com.lino.mybatis.executor.keygen.Jdbc3KeyGenerator;
import com.lino.mybatis.executor.keygen.KeyGenerator;
import com.lino.mybatis.executor.keygen.NoKeyGenerator;
import com.lino.mybatis.scripting.LanguageDriver;
import com.lino.mybatis.session.Configuration;
import java.util.Collections;
import java.util.List;/*** @description: 映射器语句类*/
public class MappedStatement {private String resource;private Configuration configuration;private String id;private SqlCommandType sqlCommandType;private SqlSource sqlSource;Class<?> resultType;private LanguageDriver lang;private List<ResultMap> resultMaps;private KeyGenerator keyGenerator;private String[] keyProperties;private String[] keyColumns;public MappedStatement() {}/*** 获取SQL对象** @param parameterObject 参数* @return SQL对象*/public BoundSql getBoundSql(Object parameterObject) {// 调用 SqlSource#getBoundSqlreturn sqlSource.getBoundSql(parameterObject);}public static class Builder {private MappedStatement mappedStatement = new MappedStatement();public Builder(Configuration configuration, String id, SqlCommandType sqlCommandType, SqlSource sqlSource, Class<?> resultType) {mappedStatement.configuration = configuration;mappedStatement.id = id;mappedStatement.sqlCommandType = sqlCommandType;mappedStatement.sqlSource = sqlSource;mappedStatement.resultType = resultType;mappedStatement.keyGenerator = configuration.isUseGeneratedKeys()&& SqlCommandType.INSERT.equals(sqlCommandType) ? new Jdbc3KeyGenerator() : new NoKeyGenerator();mappedStatement.lang = configuration.getDefaultScriptingLanguageInstance();}public MappedStatement build() {assert mappedStatement.configuration != null;assert mappedStatement.id != null;mappedStatement.resultMaps = Collections.unmodifiableList(mappedStatement.resultMaps);return mappedStatement;}public Builder resource(String resource) {mappedStatement.resource = resource;return this;}public String id() {return mappedStatement.id;}public Builder resultMaps(List<ResultMap> resultMaps) {mappedStatement.resultMaps = resultMaps;return this;}public Builder keyGenerator(KeyGenerator keyGenerator) {mappedStatement.keyGenerator = keyGenerator;return this;}public Builder keyProperty(String keyProperty) {mappedStatement.keyProperties = delimitedStringToArray(keyProperty);return this;}}private static String[] delimitedStringToArray(String in) {if (in == null || in.trim().length() == 0) {return null;} else {return in.split(",");}}public Configuration getConfiguration() {return configuration;}public String getId() {return id;}public SqlCommandType getSqlCommandType() {return sqlCommandType;}public SqlSource getSqlSource() {return sqlSource;}public Class<?> getResultType() {return resultType;}public LanguageDriver getLang() {return lang;}public List<ResultMap> getResultMaps() {return resultMaps;}public String[] getKeyProperties() {return keyProperties;}public KeyGenerator getKeyGenerator() {return keyGenerator;}public String getResource() {return resource;}public String[] getKeyColumns() {return keyColumns;}
}
- 添加
keyGenerator
、keyProperties
、keyColumns
键值生成器
3.5.2 配置项
Configuration.java
package com.lino.mybatis.session;import com.lino.mybatis.binding.MapperRegistry;
import com.lino.mybatis.datasource.druid.DruidDataSourceFactory;
import com.lino.mybatis.datasource.pooled.PooledDataSourceFactory;
import com.lino.mybatis.datasource.unpooled.UnpooledDataSourceFactory;
import com.lino.mybatis.executor.Executor;
import com.lino.mybatis.executor.SimpleExecutor;
import com.lino.mybatis.executor.keygen.KeyGenerator;
import com.lino.mybatis.executor.parameter.ParameterHandler;
import com.lino.mybatis.executor.resultset.DefaultResultSetHandler;
import com.lino.mybatis.executor.resultset.ResultSetHandler;
import com.lino.mybatis.executor.statement.PreparedStatementHandler;
import com.lino.mybatis.executor.statement.StatementHandler;
import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.Environment;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.mapping.ResultMap;
import com.lino.mybatis.reflection.MetaObject;
import com.lino.mybatis.reflection.factory.DefaultObjectFactory;
import com.lino.mybatis.reflection.factory.ObjectFactory;
import com.lino.mybatis.reflection.wrapper.DefaultObjectWrapperFactory;
import com.lino.mybatis.reflection.wrapper.ObjectWrapperFactory;
import com.lino.mybatis.scripting.LanguageDriver;
import com.lino.mybatis.scripting.LanguageDriverRegistry;
import com.lino.mybatis.scripting.xmltags.XMLLanguageDriver;
import com.lino.mybatis.transaction.Transaction;
import com.lino.mybatis.transaction.jdbc.JdbcTransactionFactory;
import com.lino.mybatis.type.TypeAliasRegistry;
import com.lino.mybatis.type.TypeHandlerRegistry;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;/*** @description: 配置项*/
public class Configuration {/*** 环境*/protected Environment environment;/*** 是否使用自动生成键值对*/protected boolean useGeneratedKeys = false;/*** 映射注册机*/protected MapperRegistry mapperRegistry = new MapperRegistry(this);/*** 映射的语句,存在Map里*/protected final Map<String, MappedStatement> mappedStatements = new HashMap<>(16);/*** 结果映射,存在Map里*/protected final Map<String, ResultMap> resultMaps = new HashMap<>(16);/*** 键值生成器,存在Map里*/protected final Map<String, KeyGenerator> keyGenerators = new HashMap<>(16);...public boolean isUseGeneratedKeys() {return useGeneratedKeys;}public void setUseGeneratedKeys(boolean useGeneratedKeys) {this.useGeneratedKeys = useGeneratedKeys;}...public void addResultMap(ResultMap resultMap) {resultMaps.put(resultMap.getId(), resultMap);}public void addKeyGenerator(String id, KeyGenerator keyGenerator) {keyGenerators.put(id, keyGenerator);}public KeyGenerator getKeyGenerator(String id) {return keyGenerators.get(id);}public boolean hasKeyGenerators(String id) {return keyGenerators.containsKey(id);}
}
- 添加
keyGenerators
键值生成器列表
3.5.3 默认Map结果处理器
DefaultResultSetHandler.java
package com.lino.mybatis.executor.resultset;import com.lino.mybatis.executor.Executor;
import com.lino.mybatis.executor.result.DefaultResultContext;
import com.lino.mybatis.executor.result.DefaultResultHandler;
import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.mapping.ResultMap;
import com.lino.mybatis.mapping.ResultMapping;
import com.lino.mybatis.reflection.MetaClass;
import com.lino.mybatis.reflection.MetaObject;
import com.lino.mybatis.reflection.factory.ObjectFactory;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.session.ResultHandler;
import com.lino.mybatis.session.RowBounds;
import com.lino.mybatis.type.TypeHandler;
import com.lino.mybatis.type.TypeHandlerRegistry;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;/*** @description: 默认Map结果处理器*/
public class DefaultResultSetHandler implements ResultSetHandler {...private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, String columnPrefix) throws SQLException {final Class<?> resultType = resultMap.getType();final MetaClass metaType = MetaClass.forClass(resultType);if (typeHandlerRegistry.hasTypeHandler(resultType)) {// 基本类型return createPrimitiveResultObject(rsw, resultMap, columnPrefix);} else if (resultType.isInterface() || metaType.hasDefaultConstructor()) {// 普通的Bean对象类型return objectFactory.create(resultType);}throw new RuntimeException("Do not know how to create an instance of " + resultType);}private Object createPrimitiveResultObject(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {final Class<?> resultType = resultMap.getType();final String columnName;if (!resultMap.getResultMappings().isEmpty()) {final List<ResultMapping> resultMappingList = resultMap.getResultMappings();final ResultMapping mapping = resultMappingList.get(0);columnName = prependPrefix(mapping.getColumn(), columnPrefix);} else {columnName = rsw.getColumnNames().get(0);}final TypeHandler<?> typeHandler = rsw.getTypeHandler(resultType, columnName);return typeHandler.getResult(rsw.getResultSet(), columnName);}private String prependPrefix(String columnName, String prefix) {if (columnName == null || columnName.length() == 0 || prefix == null || prefix.length() == 0) {return columnName;}return prefix + columnName;}...
}
3.6 解析selectkey
selectKey
标签主要用在 Mapper XML 中的insert
语句里,所以我们主要是对 XMLStatementBuilder XML 语句构建器的解析过程进行扩展。
3.6.1 修改语句处理器抽象基类
BaseStatementHandler.java
package com.lino.mybatis.executor.statement;import com.lino.mybatis.executor.Executor;
import com.lino.mybatis.executor.keygen.KeyGenerator;
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: 语句处理器抽象基类* @author: lingjian* @createDate: 2022/11/8 13:53*/
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.rowBounds = rowBounds;// 新增判断,因为update不会传入boundSql参数,这里做初始化处理if (boundSql == null) {generateKeys(parameterObject);boundSql = mappedStatement.getBoundSql(parameterObject);}this.boundSql = boundSql;this.parameterObject = parameterObject;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;/*** 生成键值对** @param parameter 参数*/protected void generateKeys(Object parameter) {KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();keyGenerator.processBefore(executor, mappedStatement, null, parameter);}
}
3.6.2 预处理语句处理器
- StatementHandler 语句处理器接口所定义的方法,在 SQL 执行上只有
update
和query
。 - 所以我们要扩展的
insert
操作,也是对update
方法的扩展操作处理。
PreparedStatementHandler.java
package com.lino.mybatis.executor.statement;import com.lino.mybatis.executor.Executor;
import com.lino.mybatis.executor.keygen.KeyGenerator;
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();int rows = ps.getUpdateCount();Object parameterObject = boundSql.getParameterObject();KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);return rows;}@Overridepublic <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {PreparedStatement ps = (PreparedStatement) statement;ps.execute();return resultSetHandler.handleResultSets(ps);}
}
- 在
update
方法中扩展对selectKey
标签中语句的处理。 - 其实这部分内容就是获取 MapperStatement 映射语句类中 KeyGenerator,之后调用
KeyGenerator#processAfter
方法。 - 这部分的调用就是前面对
SelectKeyGenerator#processGeneratorKeys
键值生成器方法的调用讲解。
3.6.3 注解配置构建器
MapperAnnotationBuilder.java
package com.lino.mybatis.builder.annotation;import com.lino.mybatis.annotations.Delete;
import com.lino.mybatis.annotations.Insert;
import com.lino.mybatis.annotations.Select;
import com.lino.mybatis.annotations.Update;
import com.lino.mybatis.binding.MapperMethod;
import com.lino.mybatis.builder.MapperBuilderAssistant;
import com.lino.mybatis.executor.keygen.Jdbc3KeyGenerator;
import com.lino.mybatis.executor.keygen.KeyGenerator;
import com.lino.mybatis.executor.keygen.NoKeyGenerator;
import com.lino.mybatis.mapping.SqlCommandType;
import com.lino.mybatis.mapping.SqlSource;
import com.lino.mybatis.scripting.LanguageDriver;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.session.ResultHandler;
import com.lino.mybatis.session.RowBounds;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;/*** @description: 注解配置构建器 Mapper*/
public class MapperAnnotationBuilder {private final Set<Class<? extends Annotation>> sqlAnnotationTypes = new HashSet<>();private Configuration configuration;private MapperBuilderAssistant assistant;private Class<?> type;public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {String resource = type.getName().replace(".", "/") + ".java (best guess)";this.assistant = new MapperBuilderAssistant(configuration, resource);this.configuration = configuration;this.type = type;sqlAnnotationTypes.add(Select.class);sqlAnnotationTypes.add(Insert.class);sqlAnnotationTypes.add(Update.class);sqlAnnotationTypes.add(Delete.class);}public void parse() {String resource = type.toString();if (!configuration.isResourceLoaded(resource)) {assistant.setCurrentNamespace(type.getName());Method[] methods = type.getMethods();for (Method method : methods) {if (!method.isBridge()) {// 解析语句parseStatement(method);}}}}/*** 解析语句** @param method 方法*/private void parseStatement(Method method) {Class<?> parameterTypeClass = getParameterType(method);LanguageDriver languageDriver = getLanguageDriver(method);SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);if (sqlSource != null) {final String mappedStatementId = type.getName() + "." + method.getName();SqlCommandType sqlCommandType = getSqlCommandType(method);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();}boolean isSelect = sqlCommandType == SqlCommandType.SELECT;String resultMapId = null;if (isSelect) {resultMapId = parseResultMap(method);}// 调用助手类assistant.addMappedStatement(mappedStatementId,sqlSource,sqlCommandType,parameterTypeClass,resultMapId,getReturnType(method),keyGenerator,keyProperty,languageDriver);}}...}
3.6.4 映射构建器助手
MapperBuilderAssistant.java
package com.lino.mybatis.builder;import com.lino.mybatis.executor.keygen.KeyGenerator;
import com.lino.mybatis.mapping.*;
import com.lino.mybatis.reflection.MetaClass;
import com.lino.mybatis.scripting.LanguageDriver;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.type.TypeHandler;
import java.util.ArrayList;
import java.util.List;/*** @description: 映射构建器助手,建造者*/
public class MapperBuilderAssistant extends BaseBuilder {private String currentNamespace;private String resource;public MapperBuilderAssistant(Configuration configuration, String resource) {super(configuration);this.resource = resource;}public ResultMapping buildResultMapping(Class<?> resultType, String property, String column, List<ResultFlag> flags) {Class<?> javaTypeClass = resolveResultJavaType(resultType, property, null);TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, null);ResultMapping.Builder builder = new ResultMapping.Builder(configuration, property, column, javaTypeClass);builder.typeHandler(typeHandlerInstance);builder.flags(flags);return builder.build();}private Class<?> resolveResultJavaType(Class<?> resultType, String property, Class<?> javaType) {if (javaType == null && property != null) {try {MetaClass metaResultType = MetaClass.forClass(resultType);javaType = metaResultType.getSetterType(property);} catch (Exception ignore) {}}if (javaType == null) {javaType = Object.class;}return javaType;}public String getCurrentNamespace() {return currentNamespace;}public void setCurrentNamespace(String currentNamespace) {this.currentNamespace = currentNamespace;}public String applyCurrentNamespace(String base, boolean isReference) {if (base == null) {return null;}if (isReference) {if (base.contains(".")) {return base;}} else {if (base.startsWith(currentNamespace + ".")) {return base;}if (base.contains(".")) {throw new RuntimeException("Dots are not allowed in element names, please remove it from " + base);}}return currentNamespace + "." + base;}/*** 添加映射器语句*/public MappedStatement addMappedStatement(String id, SqlSource sqlSource, SqlCommandType sqlCommandType,Class<?> parameterType, String resultMap, Class<?> resultType,KeyGenerator keyGenerator, String keyProperty, LanguageDriver lang) {// 给id加上namespace前缀:com.lino.mybatis.test.dao.IUserDao.queryUserInfoByIdid = applyCurrentNamespace(id, false);MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlCommandType, sqlSource, resultType);statementBuilder.resource(resource);statementBuilder.keyGenerator(keyGenerator);statementBuilder.keyProperty(keyProperty);// 结果映射, 给 MappedStatement#resultMapssetStatementResultMap(resultMap, resultType, statementBuilder);MappedStatement statement = statementBuilder.build();// 映射语句信息,建造完存放到配置项中configuration.addMappedStatement(statement);return statement;}private void setStatementResultMap(String resultMap, Class<?> resultType, MappedStatement.Builder statementBuilder) {// 因为暂时还没有在 Mapper XML 中配置 Map 返回结果,所以这里返回的是 nullresultMap = applyCurrentNamespace(resultMap, true);List<ResultMap> resultMaps = new ArrayList<>();if (resultMap != null) {String[] resultMapNames = resultMap.split(",");for (String resultMapName : resultMapNames) {resultMaps.add(configuration.getResultMap(resultMapName.trim()));}}/** 通常使用 resultType 即可满足大部分场景* <select id="queryUserInfoById" resultType="com.lino.mybatis.test.po.User">* 使用 resultType 的情况下,Mybatis 会自动创建一个 ResultMap,基于属性名称映射列到 JavaBean 的属性上。*/else if (resultType != null) {ResultMap.Builder inlineResultMapBuilder = new ResultMap.Builder(configuration, statementBuilder.id() + "-Inline", resultType, new ArrayList<>());resultMaps.add(inlineResultMapBuilder.build());}statementBuilder.resultMaps(resultMaps);}public ResultMap addResultMap(String id, Class<?> type, List<ResultMapping> resultMappings) {// 补全ID全路径,如:com.lino.mybatis.test.dao.IActivityDao + activityMapid = applyCurrentNamespace(id, false);ResultMap.Builder inlineResultMapBuilder = new ResultMap.Builder(configuration, id, type, resultMappings);ResultMap resultMap = inlineResultMapBuilder.build();configuration.addResultMap(resultMap);return resultMap;}
}
3.6.5 XML语句构建器
XMLStatementBuilder.java
package com.lino.mybatis.builder.xml;import com.lino.mybatis.builder.BaseBuilder;
import com.lino.mybatis.builder.MapperBuilderAssistant;
import com.lino.mybatis.executor.keygen.Jdbc3KeyGenerator;
import com.lino.mybatis.executor.keygen.KeyGenerator;
import com.lino.mybatis.executor.keygen.NoKeyGenerator;
import com.lino.mybatis.executor.keygen.SelectKeyGenerator;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.mapping.SqlCommandType;
import com.lino.mybatis.mapping.SqlSource;
import com.lino.mybatis.scripting.LanguageDriver;
import com.lino.mybatis.session.Configuration;
import org.dom4j.Element;
import java.util.List;
import java.util.Locale;/*** @description: XML语言构建器*/
public class XMLStatementBuilder extends BaseBuilder {private MapperBuilderAssistant builderAssistant;private Element element;public XMLStatementBuilder(Configuration configuration, MapperBuilderAssistant builderAssistant, Element element) {super(configuration);this.builderAssistant = builderAssistant;this.element = element;}/*** 解析语句(select|insert|update|delete)* <select* id="selectPerson"* parameterType="int"* parameterMap="deprecated"* resultType="hashmap"* resultMap="personResultMap"* flushCache="false"* useCache="true"* timeout="10000"* fetchSize="256"* statementType="PREPARED"* resultSetType="FORWARD_ONLY">* SELECT * FROM PERSON WHERE ID = #{id}* </select>*/public void parseStatementNode() {String id = element.attributeValue("id");// 参数类型String parameterType = element.attributeValue("parameterType");Class<?> parameterTypeClass = resolveAlias(parameterType);// 外部应用 resultMapString resultMap = element.attributeValue("resultMap");// 结果类型String resultType = element.attributeValue("resultType");Class<?> resultTypeClass = resolveAlias(resultType);// 获取命令类型(select|insert|update|delete)String nodeName = element.getName();SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));// 获取默认语言驱动器Class<?> langClass = configuration.getLanguageRegistry().getDefaultDriverClass();LanguageDriver langDriver = configuration.getLanguageRegistry().getDriver(langClass);// 解析<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.hasKeyGenerators(keyStatementId)) {keyGenerator = configuration.getKeyGenerator(keyStatementId);} else {keyGenerator = configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType) ? new Jdbc3KeyGenerator() :new NoKeyGenerator();}// 调用助手类builderAssistant.addMappedStatement(id,sqlSource,sqlCommandType,parameterTypeClass,resultMap,resultTypeClass,keyGenerator,keyProperty,langDriver);}private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) {List<Element> selectKeyNodes = element.elements("selectKey");parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver);}private void parseSelectKeyNodes(String parentId, List<Element> list, Class<?> parameterTypeClass, LanguageDriver languageDriver) {for (Element nodeToHandle : list) {String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, languageDriver);}}/*** <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");Class<?> resultTypeClass = resolveClass(resultType);boolean executeBefore = "BEFORE".equals(nodeToHandle.attributeValue("order", "AFTER"));String keyProperty = nodeToHandle.attributeValue("keyProperty");// 默认String 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));}
}
- 通过
parseStatementNode
解析insert/delete/update/select
标签方法,扩展对selectkey
标签的处理。 processSelectKeyNodes
方法是专门用于处理selectKey
标签下的语句。- 另外是对
keyProperty
的初识操作。- 因为很多时候对 SQL 的解析里面并没有
selectKey
以及获取自增主键结果的返回处理。 - 那么这个时候会采用默认的
keyGenerator
获取处理,通常都会是实例化 NoKeyGenerator 赋值。
- 因为很多时候对 SQL 的解析里面并没有
<selectKey keyProperty="id" order="AFTER" resultType="long">SELECT LAST_INSERT_ID()
</selectKey>
- 在
processSelectKeyNode
中先进行对selectKey
标签上,resultType
、keyProperty
等属性的解析,之后解析 SQL 封装成SqlSession
,最后阶段是对解析信息的保存处理。- 分别存放成 MappedStatement 映射器语句、SelectKeyGenerator 键值生成器。
3.7 JDBC类型和类型处理器注册机修改
3.7.1 JDBC类型
JdbcType.java
package com.lino.mybatis.type;import java.sql.Types;
import java.util.HashMap;
import java.util.Map;/*** @description: JDBC枚举类型*/
public enum JdbcType {// JDBC枚举类型INTEGER(Types.INTEGER),BIGINT(Types.BIGINT),FLOAT(Types.FLOAT),DOUBLE(Types.DOUBLE),DECIMAL(Types.DECIMAL),VARCHAR(Types.VARCHAR),CHAR(Types.CHAR),TIMESTAMP(Types.TIMESTAMP);public final int TYPE_CODE;private static Map<Integer, JdbcType> codeLookup = new HashMap<>();static {for (JdbcType type : JdbcType.values()) {codeLookup.put(type.TYPE_CODE, type);}}JdbcType(int code) {this.TYPE_CODE = code;}public static JdbcType forCode(int code) {return codeLookup.get(code);}
}
- 添加
BIGINT
类型
3.7.2 类型处理器注册机
TypeHandlerRegistry.java
package com.lino.mybatis.type;import java.lang.reflect.Type;
import java.util.Date;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;/*** @description: 类型处理器注册机*/
public final class TypeHandlerRegistry {private final Map<JdbcType, TypeHandler<?>> JDBC_TYPE_HANDLER_MAP = new EnumMap<>(JdbcType.class);private final Map<Type, Map<JdbcType, TypeHandler<?>>> TYPE_HANDLER_MAP = new HashMap<>(16);private final Map<Class<?>, TypeHandler<?>> ALL_TYPE_HANDLER_MAP = new HashMap<>(16);public TypeHandlerRegistry() {register(Long.class, new LongTypeHandler());register(long.class, new LongTypeHandler());register(Integer.class, new IntegerTypeHandler());register(int.class, new IntegerTypeHandler());register(String.class, new StringTypeHandler());register(String.class, JdbcType.CHAR, new StringTypeHandler());register(String.class, JdbcType.VARCHAR, new StringTypeHandler());register(Date.class, new DateTypeHandler());}private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {register(javaType, null, typeHandler);}public void register(JdbcType jdbcType, TypeHandler<?> typeHandler) {JDBC_TYPE_HANDLER_MAP.put(jdbcType, typeHandler);}private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {if (null != javaType) {Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.computeIfAbsent(javaType, k -> new HashMap<>(16));map.put(jdbcType, handler);}ALL_TYPE_HANDLER_MAP.put(handler.getClass(), handler);}@SuppressWarnings("unchecked")public TypeHandler<?> getTypeHandler(Class<?> type, JdbcType jdbcType) {return getTypeHandler((Type) type, jdbcType);}public boolean hasTypeHandler(Class<?> javaType) {return hasTypeHandler(javaType, null);}public boolean hasTypeHandler(Class<?> javaType, JdbcType jdbcType) {return javaType != null && getTypeHandler((Type) javaType, jdbcType) != null;}private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = TYPE_HANDLER_MAP.get(type);TypeHandler<?> handler = null;if (jdbcHandlerMap != null) {handler = jdbcHandlerMap.get(jdbcType);if (handler == null) {handler = jdbcHandlerMap.get(null);}}// type driver generics herereturn (TypeHandler<T>) handler;}public TypeHandler<?> getMappingTypeHandler(Class<? extends TypeHandler<?>> handlerType) {return ALL_TYPE_HANDLER_MAP.get(handlerType);}
}
- 添加
registry
注册方法。
3.8 JDBC链接获取
- 由于是在同一个操作下,处理两条 SQL,分别是插入和返回索引。
- 那么这两条 SQL 其实要在同一个链接下才能正确的获取到结果,也就是保证了一个事务的特性。
JdbcTransaction.java
package com.lino.mybatis.transaction.jdbc;import com.lino.mybatis.session.TransactionIsolationLevel;
import com.lino.mybatis.transaction.Transaction;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;/*** @description: JDBC 事务,直接利用 JDBC 的commit、rollback。依赖于数据源获得的连接管理事务范围*/
public class JdbcTransaction implements Transaction {protected Connection connection;protected DataSource dataSource;protected TransactionIsolationLevel level = TransactionIsolationLevel.NONE;protected boolean autoCommit;public JdbcTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit) {this.dataSource = dataSource;this.level = level;this.autoCommit = autoCommit;}public JdbcTransaction(Connection connection) {this.connection = connection;}@Overridepublic Connection getConnection() throws SQLException {if (null != connection) {return connection;}connection = dataSource.getConnection();connection.setTransactionIsolation(level.getLevel());connection.setAutoCommit(autoCommit);return connection;}@Overridepublic void commit() throws SQLException {if (connection != null && !connection.getAutoCommit()) {connection.commit();}}@Overridepublic void rollback() throws SQLException {if (connection != null && !connection.getAutoCommit()) {connection.rollback();}}@Overridepublic void close() throws SQLException {if (connection != null && !connection.getAutoCommit()) {connection.close();}}
}
- 也就是
JdbcTransaction#getConnection
方法。- 在前面,我们实现时只是一个
dataSource.getConnection
获取链接。这样就相当于每次获得的链接都是一个新的连接。 - 那么两条 SQL 的执行分别在各自的 JDBC 连接下,则不会正确的返回插入后的索引值。
- 在前面,我们实现时只是一个
- 所以我们这里进行判断,如果连接不为空,则不在创建新的 JDBC 连接,使用当前连接即可。
四、测试:Insert自增索引值
4.1 测试环境配置
4.1.1 修改DAO持久层
IActivityDao.java
package com.lino.mybatis.test.dao;import com.lino.mybatis.test.po.Activity;/*** @description: 活动持久层*/
public interface IActivityDao {/*** 根据活动ID查询活动** @param activityId 活动ID* @return 活动对象*/Activity queryActivityById(Long activityId);/*** 新增** @param activity 活动类* @return Integer*/Integer insert(Activity activity);
}
4.1.2 修改mapper配置文件
Activity_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.IActivityDao"><resultMap id="activityMap" type="com.lino.mybatis.test.po.Activity"><id column="id" property="id"/><result column="activity_id" property="activityId"/><result column="activity_name" property="activityName"/><result column="activity_desc" property="activityDesc"/><result column="create_time" property="createTime"/><result column="update_time" property="updateTime"/></resultMap><select id="queryActivityById" parameterType="java.lang.Long" resultMap="activityMap">SELECT activity_id, activity_name, activity_desc, create_time, update_timeFROM activityWHERE activity_id = #{activityId}</select><insert id="insert" parameterType="com.lino.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>
</mapper>
- 在
insert
标签下,添加selectKey
标签,并使用SELECT LAST_INSERT_ID()
查询方法返回自增索引值。
4.2 单元测试
4.2.1 单元测试
ApiTest.java
@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("xiaolingge");// 2.测试验证Integer result = dao.insert(activity);logger.info("测试结果:count: {} index: {}", result, JSON.toJSONString(activity.getId()));sqlSession.commit();
}
测试结果
14:53:54.803 [main] INFO c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:10004
14:53:54.803 [main] INFO c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"测试活动"
14:53:54.803 [main] INFO c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"测试数据插入"
14:53:54.809 [main] INFO com.lino.mybatis.test.ApiTest - 测试结果:count: 1 index: 129
- 通过测试结果
index: 129
可以看到,我们已经可以在插入数据后,返回数据库自增字段的结果。
4.2.2 插入查询测试
ApiTest.java
@Test
public void test_insert_select() throws IOException {// 解析 XMLReader reader = Resources.getResourceAsReader("mybatis-config-datasource.xml");XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder(reader);Configuration configuration = xmlConfigBuilder.parse();// 获取 DefaultSqlSessionfinal Environment environment = configuration.getEnvironment();TransactionFactory transactionFactory = environment.getTransactionFactory();Transaction tx = transactionFactory.newTransaction(configuration.getEnvironment().getDataSource(), TransactionIsolationLevel.READ_COMMITTED, false);// 创建执行器final Executor executor = configuration.newExecutor(tx);SqlSession sqlSession = new DefaultSqlSession(configuration, executor);// 执行查询,默认是一个集合参数Activity activity = new Activity();activity.setActivityId(10005L);activity.setActivityName("测试活动");activity.setActivityDesc("测试数据插入");activity.setCreator("xiaolingge");int result = sqlSession.insert("com.lino.mybatis.test.dao.IActivityDao.insert", activity);Object obj = sqlSession.selectOne("com.lino.mybatis.test.dao.IActivityDao.insert!selectKey");logger.info("测试结果:count: {} index: {}", result, JSON.toJSONString(obj));sqlSession.commit();
}
测试结果
14:57:10.348 [main] INFO c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:10005
14:57:10.349 [main] INFO c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"测试活动"
14:57:10.349 [main] INFO c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:"测试数据插入"
14:57:10.364 [main] INFO c.l.m.s.defaults.DefaultSqlSession - 执行查询 statement:com.lino.mybatis.test.dao.IActivityDao.insert!selectKey parameter:null
14:57:10.365 [main] INFO com.lino.mybatis.test.ApiTest - 测试结果:count: 1 index: 7
- 从测试结果看,记录正常插入到数据库,并返回
index:7
,测试结果通过
五、总结:Insert自增索引值
- 在原有的 Mapper XML 对各类标签语句的解析中,对 insert 操作进行扩展,添加新的标签
selectKey
并通过这样一个标签的解析、执行、封装处理把最终的插入索引结果返回到入参对象的对应属性字段上。- 那么同时我们所处理的是类似 mysql 这样带有自增索引的数据库,用这样的方式来串联整个流程。
- 注意:本章节是首次在一个操作中执行2条 SQL 语句,为了能让最后可以查询到自增索引,那么这两天 SQL 必须是在同一个链接下。
相关文章:
![](https://img-blog.csdnimg.cn/7636e7c42af94cd5a574c20f2f2f34c1.png#pic_center)
手写Mybatis:第15章-返回Insert操作自增索引值
文章目录 一、目标:Insert自增索引值二、设计:Insert自增索引值三、实现:Insert自增索引值3.1 工程结构3.2 Insert自增索引值类图3.3 修改执行器3.3.1 修改执行器接口3.3.2 抽象执行器基类 3.4 键值生成器3.4.1 键值生成器接口3.4.2 不用键值…...
![](https://www.ngui.cc/images/no-images.jpg)
【数据结构】动态数组(vector)的基本操作,包括插入、删除、扩容、输出、释放内存等。以下是代码的解释和注释:
这段C代码实现了一个动态数组(vector)的基本操作,包括插入、删除、扩容、输出、释放内存等。以下是代码的解释和注释: // 引入标准输入输出库和标准库函数,用于后续的内存分配和打印输出等操作 #include <stdio.…...
![](https://img-blog.csdnimg.cn/00190330434348ff9d9cb5d6c7ca87a0.gif)
[unity]三角形顶点顺序
序 详见官方文档:Unity - Manual: Mesh data (unity3d.com) Topology:拓扑结构 翻译: 拓扑描述网格具有的面类型。 网格的拓扑定义了索引缓冲区的结构,索引缓冲区又描述了顶点位置如何组合成面。每种类型的拓扑都使用索引数组中…...
![](https://img-blog.csdnimg.cn/802990a0ce1a42e69cf2e5fb7865ed21.png)
【python爬虫】14.Scrapy框架讲解
文章目录 前言Scrapy是什么Scrapy的结构Scrapy的工作原理 Scrapy的用法明确目标与分析过程代码实现——创建项目代码实现——编辑爬虫代码实现——定义数据代码实操——设置代码实操——运行 复习 前言 前两关,我们学习了能提升爬虫速度的进阶知识——协程…...
![](https://img-blog.csdnimg.cn/img_convert/74a913a96c5fc0354bcd425ab8d3ec65.png)
功率放大器主要作用是什么呢
功率放大器是一种电子设备,主要作用是将输入信号的功率增加到更高的水平,以便能够驱动高功率负载。在许多应用中,信号源产生的信号往往具有较低的功率,无法直接满足一些要求较高的设备或系统的需求。而功率放大器则可以增强信号的…...
![](https://www.ngui.cc/images/no-images.jpg)
SpringBoot ApplicationEvent详解
ApplicationStartingEvent 阶段 LoggingApplicationListener#onApplicationStartingEvent 初始化日志工厂,LoggingSystemFactory接口,可以通过spring.factories进行定制 可以通过System.setProperty("org.springframework.boot.logging.LoggingSystem",&q…...
![](https://www.ngui.cc/images/no-images.jpg)
WebSocket 报java.io.IOException: 远程主机强迫关闭了一个现有的连接。
在客户端强制关闭时,或者窗口强制关闭时,后端session没有关闭。 有时还会报:java.io.EOFException: 这个异常 前端心跳没有收到信息,还在心跳。 CloseReason close new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, &…...
![](https://img-blog.csdnimg.cn/cc01b723b3ff4d0aa318ee407d38049b.png)
关于git约定式提交IDEA
背景 因为git提交的消息不规范导致被乱喷,所以领导统一规定了约定式提交 官话 约定式提交官网地址 约定式提交规范是一种基于提交信息的轻量级约定。 它提供了一组简单规则来创建清晰的提交历史; 这更有利于编写自动化工具。 通过在提交信息中描述功能…...
![](https://img-blog.csdnimg.cn/0aab773f8cfd4bfa9f3e6a2be4cde27a.png)
【计算机网络】http协议
目录 前言 认识URL URLEncode和URLDecode http协议格式 http方法 GET POST GET与POST的区别 http状态码 http常见header 简易的http服务器 前言 我们在序列化和反序列化这一章中,实现了一个网络版的计算器。这个里面设计到了对协议的分析与处…...
![](https://www.ngui.cc/images/no-images.jpg)
仓库太大,clone 后,git pull 老分支成功,最新分支失败
由于 git 仓库太大,新加入的小伙伴在拉取时,无法切换到最新的分支,报错如下: fetch-pack: unexpected disconnect while reading sideband packet fatal: early EOF fatal: fetch-pack: invalid index-pack output在此记录解决步…...
![](https://www.ngui.cc/images/no-images.jpg)
javafx Dialog无法关闭
// 生成二维码图片String qrCodeText "https://example.com";DialogPane grid new DialogPane();grid.setPadding(new Insets(5));VBox vBox new VBox();vBox.setAlignment(Pos.CENTER);Image qrCodeImage generateQRCodeImage(qrCodeText);ImageView customImag…...
![](https://img-blog.csdnimg.cn/c2dcee5eda92455699338f97ca5d9fb8.png)
vue3中TCplayer应用
环境win10:vitevue3elementUI 1 安装 npm install tcplayer.js2 使用 <template><div><video id"player-container-id" width"414" height"270" preload"auto" playsinline webkit-playsinline></video>&l…...
![](https://img-blog.csdnimg.cn/9820a0308eb74dcfaaa842084dbbc7ec.png)
算法通关村14关 | 数据流中位数问题
1. 数据流中位数问题 题目 LeetCode295: 中位数是有序列表中间的数,如果列表长度是偶数,中位数是中间两个数的平均值, 例如:[2,3,4]的中位数是3, [2,3]中位数是(23)/ 2 2.5 设计一个数据结构: …...
![](https://www.ngui.cc/images/no-images.jpg)
工厂模式 与 抽象工厂模式 的区别
工厂模式: // 抽象产品接口 interface Product {void showInfo(); }// 具体产品A class ConcreteProductA implements Product {Overridepublic void showInfo() {System.out.println("This is Product A");} }// 具体产品B class ConcreteProductB impl…...
![](https://img-blog.csdnimg.cn/cc16d5fd094d460aa3a0d271ed5d8e5a.png)
安装虚拟机+安装/删除镜像
安装虚拟机 注意,官网可能无法登录,导致无法从官网下载,就自己去网上搜靠谱的下载,我用的16.2.3 删除镜像 Vm虚拟机怎么删除已经创建的系统?Vm虚拟机创建好之后iso删除方法 - 系统之家 (xitongzhijia.net) 安装镜像…...
![](https://img-blog.csdnimg.cn/img_convert/861f6d487b3170d76ff619b9684813df.png)
MySQL的内置函数复合查询内外连接
文章目录 内置函数时间函数字符串函数数学函数其他函数 复合查询多表笛卡尔积自连接在where中使用子查询多列子查询在from中使用子查询 内连接外连接左外连接右外连接 内置函数 时间函数 函数描述current_date()当前日期current_time()当前时间current_timestamp()当前时间戳…...
![](https://img-blog.csdnimg.cn/img_convert/f7d8aec002d78841f72e60c951609f1c.png)
操作系统(OS)与系统进程
操作系统(OS)与系统进程 冯诺依曼体系结构操作系统(Operator System)进程基本概念进程的描述(PCB)查看进程通过系统调用获取进程标示符(PID)通过系统调用创建进程(fork)进程状态&…...
![](https://www.ngui.cc/images/no-images.jpg)
防重复提交:自定义注解 + 拦截器(HandlerInterceptor)
防重复提交:自定义注解 拦截器(HandlerInterceptor) 一、思路: 1、首先自定义注解; 2、创建拦截器实现类(自定义类名称),拦截器(HandlerInterceptor); 3…...
![](https://img-blog.csdnimg.cn/e81b6c864634479db0445c205ad7abe0.png)
Excel中将文本格式的数值转换为数字
在使用excel时,有时需要对数字列进行各种计算,比如求平均值,我们都知道应该使用AVERAGE()函数,但是很多时候结果却“不尽如人意”。 1 问题: 使用AVERAGE函数: 结果: 可以看到单元格左上角有个…...
![](https://img-blog.csdnimg.cn/5d3e953c079e4ba3a11cb6d1f64f4b73.png)
uni-app开发小程序中遇到的map地图的点聚合以及polygon划分区域问题
写一篇文章来记录以下我在开发小程序地图过程中遇到的两个小坑吧,一个是点聚合,用的是joinCluster这个指令,另一个是polygon在地图上划分多边形的问题: 1.首先说一下点聚合问题,由于之前没有做过小程序地图问题&#…...
![](https://img-blog.csdnimg.cn/a1e9fbce740a484a928cf627c7823c1e.png)
【笔记】软件测试的艺术
软件测试的心理学和经济学 测试是为发现错误而执行程序的过程,所以它是一个破坏性的过程,测试是一个“施虐”的过程。 软件测试的10大原则 1、测试用例需要对预期输出的结果有明确的定义 做这件事的前提是能够提前知晓需求和效果图,如果不…...
![](https://img-blog.csdnimg.cn/b94ccbe06b3c4c68b14170df34a136ac.png)
配置本地maven
安装maven安装包 修改环境变量 vim ~/.bash_profile export JMETER_HOME/Users/yyyyjinying/apache-jmeter-5.4.1 export GOROOT/usr/local/go export GOPATH/Users/yyyyjinying/demo-file/git/backend/go export GROOVY_HOME/Users/yyyyjinying/sortware/groovy-4.0.14 exp…...
![](https://www.ngui.cc/images/no-images.jpg)
C# 按钮的AcceptButton和CancelButton属性
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System...
![](https://www.ngui.cc/images/no-images.jpg)
SMT贴片制造:专业、现代、智能的未来之选
在现代科技的快速发展下,SMT贴片制造作为电子元器件的核心工艺之一,正以其专业、现代和智能的特点成为未来的首选。 随着电子产品越来越小型化,传统的手工焊接已经无法满足高速、高精度、高稳定性的要求。而SMT贴片制造作为一种先进的表面贴…...
![](https://www.ngui.cc/images/no-images.jpg)
python sqlalchemy db.session 的commit()和colse()对session中的对象的影响
实验一:commit()之后查看stu的属性id,查看db.session是否改变 db_test.route("/db_test",methods["GET"]) def db_test():stuStuTest()stu.stu_age22stu.stu_name"nnannns"stu.stu_class11print("sessio…...
![](https://img-blog.csdnimg.cn/26d8de6a4c2a41ba9c5fc95745013110.png)
python读取图像小工具
一、和图像交互获得图像的坐标和像素值 import cv2 import numpy as np import signal import threading import timeif __name__ __main__:img cv2.imread(XXX,0)#读取图片font_face,font_scale,thicknesscv2.FONT_HERSHEY_SIMPLEX,0.5,1#鼠标交互def mouseHandler(event,x…...
![](https://www.ngui.cc/images/no-images.jpg)
【ES6】JavaScript中Reflect
Reflect是JavaScript中的一个内建对象,它提供了一组方法,用于对对象和函数进行操作和检查。这些方法与内建对象的方法非常相似,但具有更高的灵活性。 以下是Reflect对象的一些常用方法: 1、Reflect.apply(target, thisArgument,…...
![](https://img-blog.csdnimg.cn/d44eaf109ecb4813a60939603b3e3cda.png#pic_center)
Ajax + Promise复习简单小结simple
axios使用 先看看老朋友 axios axios是基于Ajaxpromise封装的 看一下他的简单使用 安装:npm install axios --save 引入:import axios from axios GitHub地址 基本使用 axios({url: http://hmajax.itheima.net/api/province}).then(function (result…...
![](https://img-blog.csdnimg.cn/img_convert/8f661138d6d6c892f4c77d8411e9088d.jpeg)
WebDAV之π-Disk派盘 + 小书匠
小书匠是一款功能丰富,强大的知识管理工具。全平台覆盖,离线数据存储,自定义数据服务器,所见即所得的 markdown 编辑体验。 小书匠提供了多种实用的编辑模式,例如:栏编辑、双栏编辑、三栏编辑、全屏写作、全屏阅读等。并且该软件还提供了许多有用的扩展语法,比如Latex公…...
![](https://img-blog.csdnimg.cn/f1c56affffe943bf8afc02b76d65d34a.png)
LTE ATTACH流程、PDN流程、PGW地址分配介绍
1、S-GW\P-GW选择 MME根据S-GW和P-GW的拓扑信息进行S-GW/P-GW的选择,在S-GW的候选序列和P-GW的候选序列中比较,寻找是否有合一的S-GW/P-GW,并且根据S-GW的优先级和权重信息进行排序,得到S-GW/P-GW的候选组。 2、SGW>PGW连接 PD…...
![](https://static.geekbang.org/infoq/5c4e6e683ff65.png?imageView2/0/w/800)
厦门购买域名以后搭建网站/资源网站排名优化seo
关键要点 通过创建和维护架构图来提供准确且有价值的内容并非易事。大多数情况下,我们要么创建了太多的文档,要么太少,或者不相关,因为我们没能准确地定位文档的受益人及其实际的需求。我们常犯的最大的一个错误是为系统中具有高波…...
![](https://img-blog.csdnimg.cn/20201229141651825.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MTI5MDg2Mw==,size_16,color_FFFFFF,t_70)
平面设计套用模板网站/十大最靠谱培训机构
IDEA 字体设置 file ->setting...
![](https://img-blog.csdnimg.cn/2637393ec88a4c45ad2a781cc984438f.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl81MzgxMTg1OQ==,size_16,color_FFFFFF,t_70)
近两年成功的网络营销案例及分析/厦门站长优化工具
牛客网 编程初学者入门训练 BC69 空心正方形图案 思路: 大体思路:我们需要引用一个while循环,两个for循环,以及用if,else语句进行判断输出。具体思路:由于题目中出现了关键字眼——多组输入,所以一般都是…...
![](/images/no-images.jpg)
网站404页面源码/seo 重庆
1. 安装yum install asciinema 2. 使用录制 asciinema rec filename(可选,方便进行后期的回放play)同时生成一个url 地址方便传递https://asciinema.org/a/xxxxxxx同时绑定账户之后,可以存储历史的信息 asciinema play filename 3. 参考地址https://a…...
![](https://img-blog.csdnimg.cn/20190331131647801.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pob3VoYW84ODQxMDIzNA==,size_16,color_FFFFFF,t_70)
wordpress animate css/厦门网络推广
前言 Zuul 是Netflix 提供的一个开源组件,致力于在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。也有很多公司使用它来作为网关的重要组成部分,碰巧今年公司的架构组决定自研一个网关产品,集动态路由&#…...
![](https://img-blog.csdnimg.cn/img_convert/a5c9404e60aeb493ef9f0aebd6d18bd5.png)
西安手机网站定制网站建设/郴州网络推广公司排名
一、准备工作微信公众平台:https://mp.weixin.qq.com/申请测试账号:https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?actionshowinfo&tsandbox/index微信推送消息模板不需要发布服务器,也不需要填写授权回调域名,只需要…...