这篇文章研究下 Mybatis 配置主键回显相关功能。
本篇文章将以以下几个问题切入:
- Mybatis 如何 配置主键自增回显?
- JDBC 主键回显用法?
- 对于不支持自增主键数据库,Mybatis 有怎么解决这个问题?
- Mybatis 有哪几种主键生成方式?
例子
- JDBC 自增主键例子
对于数据库支持自增主键的库,例如:Mysql:
Connection conn = DriverManager.getConnection(url, "root", "123456");
String[] columnNames = {"ids", "name"};
PreparedStatement stmt = conn.prepareStatement(sql, columnNames);
stmt.setString(1, "test jdbc3 ");
stmt.executeUpdate();
ResultSet rs = stmt.getGeneratedKeys();
int id = 0;
if (rs.next()) {
id = rs.getInt(1);
System.out.println("----------" + id);
}
即当使用 prepareStatement 或者 Statement时候,可以通过getGeneratedKeys
获取 当条插入语句的自增而成的主键。
当然还有其他几种方式,原理是 数据库端返回一个LAST_INSERT_ID
,这个跟auto_increment_id
强相关。
其他几种方式可以参考oracle文档:https://docs.oracle.com/cd/E17952_01/connector-j-en/connector-j-usagenotes-last-insert-id.html
Mybtis 对于数据库主键回显,主要分为两种,一种是数据库支持自增主键字段,另一种是数据库不支持方式即手动指定方式。
- 数据库支持主键回显,例如
Mysql,postgresql
使用Jdbc3KeyGenerator
进行数据库回显:
在 insert 中配置useGeneratedKeys="true" keyColumn="id" keyProperty="id"
,即当插入或者批量插入后,成功后即可返回配置id:
<insert id="insertWithGenertoorKey" useGeneratedKeys="true" keyColumn="id" keyProperty="id">
insert into department (name) values (#{department.name})
</insert>
- 数据库不支持主键回显,例如
oracle,DB2
。则使用selectKey
先查出最大id,而后进行插入操作:
<insert id="insertWithSelectKeyGenertoorKey">
<selectKey keyProperty="id" resultType="long" order="BEFORE">
SELECT if(max(id) is null,1,max(id)+10) as newId FROM department
</selectKey>
insert into department (id, name) values (#{id}, #{department.name})
</insert>
分析
下面以 上面三个用法为基础,分析Mybatis 主键自增回显原理。
当 Mybatis 解析 xml节点是,读到 insert
有配置时,会判断是否 有配置 useGeneratedKeys
,如果有则会使用 Jdbc3KeyGenerator
作为sql回显,否则会以 NoKeyGenerator
作为主键回显。
当执行插入数据操作时,执行以下逻辑:
- 首先会进入
MapperMethod
的execute
方法,判断执行类型 - 当上一步判断是
INSERT
时,则会 进入DefaultSqlSession
的insert
方法,随后进入 其update
方法。 - 进入
Executor
的update
方法,配置完ErrorContext
后,进入doUpdate
方法:
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}
Mybatis
对KeyGenerator
使用是在PreparedStatementHandler
的update
中:
@Override
public int update(Statement statement) throws SQLException {
// 获取 preparedStatement
PreparedStatement ps = (PreparedStatement) statement;
// 执行 语句
ps.execute();
// 获取影响行
int rows = ps.getUpdateCount();
// 获取参数
Object parameterObject = boundSql.getParameterObject();
// 获取 KeyGenerator,自增则通过 jdbc获取,否则就通过selectKey 查询获取
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
// 回填 配置星到 参数中
keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
return rows;
}
Mybatis 对于 KeyGenerator
主要集中在上面的update
代码中,下面看看 三种 KeyGenerator
含义:
KeyGenerator
KeyGenerator
接口 中 只有 两个方法:
void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter);
processBefore
:主要用于 插入前操作,即获取和设置主键(不支持主键自增)
processAfter
:用于插入后对主键进行回显到参数中
- NoKeyGenerator
这个类 是对KeyGenerator
的空实现,主要是不配置generatorKey
,所以processAfter
和processBefore
事实上不需要进行任何操作 - Jdbc3KeyGenerator
使用 JDBC 方式获取自增主键,其processBefore
是空实现,只实现了processAfter
:
@Override
public void processAfter(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
processBatch(ms, stmt, parameter);
}
public void processBatch(MappedStatement ms, Statement stmt, Object parameter) {
final String[] keyProperties = ms.getKeyProperties();
if (keyProperties == null || keyProperties.length == 0) {
return;
}
try (ResultSet rs = stmt.getGeneratedKeys()) {
// 获取自增主键
final ResultSetMetaData rsmd = rs.getMetaData();
final Configuration configuration = ms.getConfiguration();
if (rsmd.getColumnCount() < keyProperties.length) {
// Error?
} else {
// 设值
assignKeys(configuration, rs, rsmd, keyProperties, parameter);
}
} catch (Exception e) {
throw new ExecutorException("Error getting generated key or setting result to parameter object. Cause: " + e, e);
}
}
上面代码主要就是获取自增主键并且设值:
private void assignKeys(Configuration configuration, ResultSet rs, ResultSetMetaData rsmd, String[] keyProperties,
Object parameter) throws SQLException {
if (parameter instanceof ParamMap || parameter instanceof StrictMap) {
// 多参数或者单参数使用 @Param Multi-param or single param with @Param
assignKeysToParamMap(configuration, rs, rsmd, keyProperties, (Map<String, ?>) parameter);
} else if (parameter instanceof ArrayList && !((ArrayList<?>) parameter).isEmpty()
&& ((ArrayList<?>) parameter).get(0) instanceof ParamMap) {
// 批量插入 操作 Multi-param or single param with @Param in batch operation
assignKeysToParamMapList(configuration, rs, rsmd, keyProperties, ((ArrayList<ParamMap<?>>) parameter));
} else {
// 没有 用 @Param注解的单参数 Single param without @Param
assignKeysToParam(configuration, rs, rsmd, keyProperties, parameter);
}
}
从上面看到,Mybatis
通过判断三种情况对 数据进行回填,从而使用不同回填,具体不在分析。
Jdbc mysql 连接驱动 中 getGeneratedKeys
获取有以下几个分析点:
- 为什么 支持 id 回显? 通过 返回的
LAST_INSERT_ID
返回。 - 为什么 批量插入可以 给所有对象返还id? 通过获取最后一个id,并且知道影响行数遍历设值
- 为什么批量
insert on duplicate key update
不能回填所有id? 因为insert on duplicate key update
这种 方式 有多种情况,分别会返回0(存在但数值一致),1(插入),2(更新), 不是固定的1,所以不能通过遍历设值。
具体可以看看这篇文章,关于 Jdbc Mysql 驱动的 getGeneratedKeys
: 深入分析Mybatis 使用useGeneratedKeys获取自增主键
- 如果数据库不支持 自增主键,
Mybatis
提供一种SelectKey
方式,即先查出,再将id填充进参数进行插入操作:
@Override
public void processBefore(Executor executor, MappedStatement ms, Statement stmt, Object parameter) {
if (executeBefore) {
// 执行前
processGeneratedKeys(executor, ms, parameter);
}
}
@Override
public 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) {
// Do not close keyExecutor.
// The transaction will be closed by parent executor.
// 获取一个Executor
Executor keyExecutor = configuration.newExecutor(executor.getTransaction(), ExecutorType.SIMPLE);
List<Object> values = keyExecutor.query(keyStatement, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
if (values.size() == 0) {
throw new ExecutorException("SelectKey returned no data.");
} else if (values.size() > 1) {
throw new ExecutorException("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 {
// no getter for the property - maybe just a single value object
// so try that
setValue(metaParam, keyProperties[0], values.get(0));
}
} else {
// 设置多个值
handleMultipleProperties(keyProperties, metaParam, metaResult);
}
}
}
}
} catch (ExecutorException e) {
throw e;
} catch (Exception e) {
throw new ExecutorException("Error selecting key or setting result to parameter object. Cause: " + e, e);
}
}
对于 SelectKeyGenerator
方式进行插入,则需要在 配置 SelectKey
时候指定 order
, 如果没有指定,则默认为 BEFORE
:
order="BEFORE"
, BEFORE
则为在 插入前设值,即 上述代码中 executeBefore
为 true
。
processGeneratedKeys
方法主要意思为 获取一个 Executor
,而后对 当前 MappedStatement
进行查询操作,最终返回到 List
的value中,并设值到 parameter
中。
以上,就是 Mybatis
中 KeyGenerator
主键回显原理。
觉得博主写的有用,不妨关注博主公众号: 六点A君。
哈哈哈,一起研究Mybatis: