`
frank1998819
  • 浏览: 758269 次
  • 性别: Icon_minigender_1
  • 来自: 南京
文章分类
社区版块
存档分类

myBatis 防止批量update delete select(转)原作者 程明东

 
阅读更多

原作者 程明东

http://my.oschina.net/mingdongcheng/blog/52440

 

昨天支付中心发起退款回调时,引起了我们这边一个bug: 有两笔退款异常,支付中心发起第一笔异常的回调的时候就把我们这边两笔退款异常对应的订单的状态全部给修改 了。当支付中心对第二笔异常回调的时候回调程序发现订单状态已经改变发出了一个异常邮件,然后回调就终止 了,所以数据库呈一个不一致状态:订单状态已改变,但没有记录订单日志,也没有记录退款状态。然后大家就来寻找这个bug,过程挺有意思的。


首先我们请DBA从订单号,订单Id和时间多个条件查数据库的日志,想找出是哪个系统发出的这个更新订单状态的log,最后没找到。

后来从退款回调里发现下面的代码:
checkUpdateResult(updateDAO.execute(
"order.updateStatus",
ImmutableMap.of("orderId", order.getId(),
"updateTs", TsUtils.now(),
"preStatus", currStatus.getStatus(),
"currentStatus",nextStatus.getStatus()))
)
这是用于更新订单状态的代码,传入了参数 orderId, updateTs,preStatus对应的mybatis mapper:
<update id="updateStatus" parameterType="java.util.HashMap"> 

    <![CDATA[ 

    update  

 `orders` 

    set  

 `status` = #{currentStatus}, 

 update_ts = #{updateTs} 

    ]]> 

    <where> 

 <if test="id != null and id != ''"> 

     and id = #{id, jdbcType=INTEGER} 

 </if> 

 <if test="paymentNo != null and paymentNo != ''"> 

     and order_no = (select order_no from payment where payment_no=#{paymentNo}) 

 </if> 

 <if test="orderNo != null and orderNo != ''"> 

     and order_no = #{orderNo, jdbcType=VARCHAR} 

 </if> 

 <if test="preStatus != null and preStatus != ''"> 

     and `status` = #{preStatus, jdbcType=INTEGER} 

 </if> 

    </where> 

</update> 

很遗憾,mapper里不是用orderId,用的是id。导致 and id = #{id,jdbcType=INTERGER}这个where条件根本没加上去,最后结果就是把 所有status = preStatus的订单全部更新了,幸运的是这个preStatus是110(退款失败,当时只有两个订单)。  
后来我想了想针对这样的bug我们如何测 试 呢?如何防止呢?
预防
1.禁止使用map作为mybatis的 参数,都用对象,这样你在mapper里写的参数如果在这个对象里没有,mybatis是会报错的。
2.但是我们现在系统里存在大量使用map的情况,而且也挺好用的,这个不好弄.那么mybatis是否提供一种机制,即发现如果我传入的参数在mapper里 并没有使用的话即抛出异常?是否可以通过修改代码解决?
3.检查update所影响的行数,如果更新不止一条则抛出异常 事务 回滚(这个在有事务的时候有用,如果没事务抛出异常也能快速让人知道也不错)。实际上看看上面的代码已经有 一个checkUpdateResult:
private void checkUpdateResult(int affectNum) throws RuntimeErrorException {
if (affectNum < 1) {
throw new RuntimeErrorException(null, "update fail! affectNum: " + affectNum);
}
}
悲催的是这个checkUpdateResult只 检查了影响行数是否小于1,如果这里的检查 条件 是 affectNum == 1也能检查这个bug啊!!!
测试
测试的时候,不管QA测试还是单元测试我 们往 往关 注我们被测对象,比如测试某订单,我们就关注这个订单,对其他订单很少关注。所以测试方面貌似很难发现这样的bug。特别是QA测试方面,多人测试我们很难知道到底是谁影响的。 在单元测试上我们能发现这个bug, 但也要我们想到了这个case,还是挺困难的。
我们的解决方案是针对3.0.6版本写了一个防止批量更新的插件。 另外参照该插件,还可以写一些防止delete,select无limit 条数限制的插件。 通过这些插件可以避免批量更新、delete操作以及无limit限制的select操作(防止查询整个表的所有记录,尤其是大表)。
用法:
(1)在MapperConfig.xml中定义插件
<plugins>
<plugin
interceptor=" com.qunar.base.mybatis.ext.interceptor .BatchUpdateForbiddenPlugin">
</plugin>
</plugins>
(2)在mapper文件中修改update的动态sql
在update语句的最后面添加了[presentColumn="orderNo"],表示解析后的where条件中必须带有orderNo。因为orderNo在业务中可以标识一条记录,因此where条件带有orderNo的话,就可以保证是单条更新,
而不是批量更新。
实例:不一样的地方是添加了[presentColumn="orderNo"]
<update id="updateStatus" parameterType="java.util.HashMap">
<![CDATA[
update
ibtest.orders
set
status = #{currentStatus}
]]>
<where>
<if test="orderNo != null and orderNo != ''">
and orderNo = #{orderNo, jdbcType=VARCHAR}
</if>
<if test="preStatus != null and preStatus != ''">
and status = #{preStatus, jdbcType=INTEGER}
</if>
</where>
[presentColumn="orderNo"]
</update>
异常:
当解析后的update语句如果是批量更新的sql时,会直接抛异常:
org.apache.ibatis.exceptions.PersistenceException:
 ### Cause: java.lang.IllegalArgumentException: 
 该update语句:update    ibtest.orders   set  status = ?    WHERE status = ?
 是批量更新sql,不允许执行。因为它的的where条件中未包含能表示主键的字段orderNo,所以会导致批量更新。
 at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:8)
 at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:124)
 at org.apache.ibatis.submitted.dynsql.nullparameter.DynSqlOrderTest.testDynamicSelectWithTypeHandler(DynSqlOrderTest.java:66)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
 at java.lang.reflect.Method.invoke(Method.java:597)
 at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
 at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
 at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
 at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
 at org.junit.runners.BlockJUnit4ClassRunner.runNotIgnored(BlockJUnit4ClassRunner.java:79)
 at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:71)
 at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:49)
 at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
 at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
 at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
 at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
 at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
 at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28)
 at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
 at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
 at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)
 Caused by: java.lang.IllegalArgumentException: 
 该update语句:update  ibtest.orders  set    status = ?  WHERE status = ?
 是批量更新sql,不允许执行。因为它的的where条件中未包含能表示主键的字段orderNo,所以会导致批量更新。
 at org.apache.ibatis.submitted.dynsql.nullparameter.BatchUpdateForbiddenPlugin.doCheckAndResetSQL(BatchUpdateForbiddenPlugin.java:132)
 at org.apache.ibatis.submitted.dynsql.nullparameter.BatchUpdateForbiddenPlugin.checkAndResetSQL(BatchUpdateForbiddenPlugin.java:103)
 at org.apache.ibatis.submitted.dynsql.nullparameter.BatchUpdateForbiddenPlugin.intercept(BatchUpdateForbiddenPlugin.java:65)
 at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:42)
 at $Proxy7.update(Unknown Source)
 at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:122)
 ... 25 more
源码:
package com.qunar.base.mybatis.ext.interceptor ;


import java.util.Properties;


import org.apache.commons.lang.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.MappedStatement.Builder;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;


/**
 * <p>
 * 禁止批量更新的插件,只允许更新单条记录
 * </p>
 * 
 * <pre>
 * mapper示例:必须在update语句的最后面定义[presentColumn="orderNo"],其中orderNo是能标识orders表的主键(逻辑主键或者业务主键)
 * <update id="updateOrder" parameterType="java.util.HashMap">
 *         <![CDATA[
 *         update 
 *             orders
 *         set 
 *             status = #{currentStatus}
 *         ]]>
 * <where>
 * <if test="orderNo != null and orderNo != ''">
 * and orderNo = #{orderNo, jdbcType=VARCHAR}
 * </if>
 * <if test="preStatus != null and preStatus != ''">
 * and status = #{preStatus, jdbcType=INTEGER}
 * </if>
 * </where>
 * [presentColumn="orderNo"]
 * </update>
 * </pre>
 * 
 * @author yi.chen@qunar.com
 * @version 0.0.1
 * @createTime 2012-04-03 18:25
 */
@Intercepts({ @Signature(type = Executor.class, method = "update", args = {
MappedStatement.class, Object.class }) })
public class BatchUpdateForbiddenPlugin implements Interceptor {


private final static String presentColumnTag = "presentColumn";// 定义where条件中必须出现的字段


/**
* <p>
* 只对update语句进行拦截
* </p>
* 
* @see org.apache.ibatis.plugin.Interceptor#intercept(org.apache.ibatis.plugin
*      .Invocation)
*/
public Object intercept(Invocation invocation) throws Throwable {
// 只拦截update
if (isUpdateMethod(invocation)) {
invocation.getArgs()[0] = checkAndResetSQL(invocation);
}
return invocation.proceed();
}


/**
* <p>
* 判断该操作是否是update操作
* </p>
* 
* @param invocation
* @return 是否是update操作
*/
private boolean isUpdateMethod(Invocation invocation) {
if (invocation.getArgs()[0] instanceof MappedStatement) {
MappedStatement mappedStatement = (MappedStatement) invocation
.getArgs()[0];
return SqlCommandType.UPDATE.equals(mappedStatement
.getSqlCommandType());
}
return false;
}


/**
* <p>
* 检查update语句中是否定义了presentColumn,并且删除presentColumn后重新设置update语句
* </p>
* 
* @param invocation
*            invocation实例
* @return MappedStatement 返回删除presentColumn之后的MappedStatement实例
*/
private Object checkAndResetSQL(Invocation invocation) {
MappedStatement mappedStatement = (MappedStatement) invocation
.getArgs()[0];
Object parameter = invocation.getArgs()[1];
mappedStatement.getSqlSource().getBoundSql(parameter);
BoundSql boundSql = mappedStatement.getBoundSql(parameter);
String resetSql = doCheckAndResetSQL(boundSql.getSql());
return getMappedStatement(mappedStatement, boundSql, resetSql);
}


/**
* <p>
* 检查update语句中是否定义了presentColumn,并且删除presentColumn后重新设置update语句
* </p>
* 
* @param sql
*            mapper中定义的sql语句(带有presentColumn的定义)
* @return 删除presentColumn之后的sql
*/
private String doCheckAndResetSQL(String sql) {
if (sql.indexOf(presentColumnTag) > 0) {
// presentColumn的定义是否在sql的最后面
if (sql.indexOf("]") + 1 == sql.length()) {
int startIndex = sql.indexOf("[");
int endIndex = sql.indexOf("]");
String presentColumnText = sql.substring(startIndex,
endIndex + 1);// [presentColumn="orderNo"]
// 剔除标记逻辑主键相关内容之后的sql,该sql才是真正执行update的sql语句
sql = StringUtils.replace(sql, presentColumnText, "");
String[] subSqls = sql.toLowerCase().split("where");
String[] keyWords = presentColumnText.split("\"");
// 获取主键,比如orderNo
String keyWord = keyWords[1];
// 判断是否带有where条件并且在where条件中是否存在主键keyWord
if (subSqls.length == 2 && subSqls[1].indexOf(keyWord) == -1) {
throw new IllegalArgumentException("该update语句:" + sql
+ "是批量更新sql,不允许执行。因为它的的where条件中未包含能表示主键的字段"
+ keyWord + ",所以会导致批量更新。");
}
} else {
throw new IllegalArgumentException("[" + presentColumnTag
+ "=\"xxx\"\"]必须定义在update语句的最后面.");
}
} else {
throw new IllegalArgumentException("在mapper文件中定义的update语句必须包含"
+ presentColumnTag + ",它用于定义该sql的主键(逻辑主键或者业务主键),比如id");
}
return sql;
}


/**
* <p>
* 通过验证关键字段不能为空之后的sql重新构建mappedStatement
* </p>
* 
* @param mappedStatement
*            重新构造sql之前的mappedStatement实例
* @param boundSql
*            重新构造sql之前的boundSql实例
* @param resetSql
*            验证关键字段不能为空之后的sql
* @return 重新构造之后的mappedStatement实例
*/
private Object getMappedStatement(MappedStatement mappedStatement,
BoundSql boundSql, String resetSql) {
final BoundSql newBoundSql = new BoundSql(
mappedStatement.getConfiguration(), resetSql,
boundSql.getParameterMappings(), boundSql.getParameterObject());


Builder builder = new MappedStatement.Builder(
mappedStatement.getConfiguration(), mappedStatement.getId(),
new SqlSource() {
public BoundSql getBoundSql(Object parameterObject) {
return newBoundSql;
}
}, mappedStatement.getSqlCommandType());


builder.cache(mappedStatement.getCache());
builder.fetchSize(mappedStatement.getFetchSize());
builder.flushCacheRequired(mappedStatement.isFlushCacheRequired());
builder.keyGenerator(mappedStatement.getKeyGenerator());
builder.keyProperty(mappedStatement.getKeyProperty());
builder.resource(mappedStatement.getResource());
builder.resultMaps(mappedStatement.getResultMaps());
builder.resultSetType(mappedStatement.getResultSetType());
builder.statementType(mappedStatement.getStatementType());
builder.timeout(mappedStatement.getTimeout());
builder.useCache(mappedStatement.isUseCache());
return builder.build();
}


public Object plugin(Object target) {
return Plugin.wrap(target, this);
}


public void setProperties(Properties properties) {


}


}


分享到:
评论

相关推荐

    MyBatis防止批量更新1

    MyBatis 插件机制防止批量更新 MyBatis 是一个流行的持久层框架,它提供了插件机制来拦截 SQL 操作,以便于开发者可以在执行 SQL 语句前进行预处理或后续处理。本文将详细介绍 MyBatis 插件机制的实现原理和应用...

    MyBatis批量插入Update

    MyBatis批量插入Update MyBatis批量插入是一种高效的数据插入方式,通过将多条数据一次性插入数据库,可以大大提高插入速度。在实际测试中,使用MyBatis批量插入可以达到至少快一倍的执行效率。 MyBatis批量插入的...

    mybatis 批量插入 嵌套select.pdf

    这个问题的描述是关于如何在MyBatis中正确地执行一个批量插入操作,其中一个字段的值依赖于对同一张表的SELECT查询结果。 原始的XML映射文件中的SQL插入语句尝试在FROM子句中直接更新目标表'chat_messages',这是不...

    mybatis执行批量更新batch update 的方法(oracle,mysql两种)

    在MyBatis中,批量更新(batch update)是一种提高数据库操作效率的重要手段,尤其是在处理大量数据更新时。这里我们将详细探讨如何在Oracle和MySQL数据库中使用MyBatis进行批量更新。 1. **Oracle数据库的批量更新...

    mybatis plus 5种批量操作性能测试

    MyBatis Plus 是 MyBatis 的一个扩展,它在 MyBatis 的基础上提供了更多的便捷功能,包括但不限于批量操作。在数据库交互中,批量操作能够显著提高效率,减少数据库连接的开销。本测试主要探讨了 MyBatis Plus 中的...

    mybatis数据操作(增删改查+批量操作)

    在"mybatis数据操作(增删改查+批量操作)"这个主题中,我们将深入探讨如何利用MyBatis进行基本的数据操作,包括插入(Insert)、删除(Delete)、更新(Update)和查询(Select),以及如何执行批量操作。...

    Mybatis批量foreach merge into的用法

    Mybatis批量foreach merge into的用法 Mybatis批量foreach merge into的用法是通过Mybatis的动态SQL语法foreach循环插入实现的,这种方法可以批量插入时间价格表数据。如果某个套餐的某天的价格存在,则更新,不...

    mybatis批量增删改.txt

    mybatis 框架批量增删改代码 示例 含java与xml

    Mybatis Plus 自定义批量插入或批量更新(根据唯一索引)

    Mybatis Plus 是 Mybatis 的一个扩展工具,它在 Mybatis 的基础上提供了许多便捷的功能,包括自动 CRUD 操作、条件构造器、批量操作等。在实际的项目开发中,批量插入和批量更新是常见的数据处理场景,尤其是在处理...

    Mybatis与JDBC批量插入MySQL数据库性能测试

    本文将探讨Mybatis和JDBC在批量插入MySQL数据库时的性能差异,并提供相关的测试资源。 首先,JDBC(Java Database Connectivity)是Java平台中用于与数据库交互的一种规范,它允许程序员使用SQL语句直接操作数据库...

    Mybatis中select、insert、update、delete用法总结

    一、select用法示例 SELECT ST.STUDENT_ID, ST.STUDENT_NAME, ST.STUDENT_SEX, ST.STUDENT_BIRTHDAY, ST.CLASS_ID FROM STUDENT_TBL ST WHERE ST.STUDENT_ID = #{studentID} 这条语句就叫做‘getStudent...

    mybatis 批量插入返回主键

    只提供代码,自己去下载相关jar包谢谢只提供代码,自己去下载相关jar包谢谢只提供代码,自己去下载相关jar包谢谢只提供代码,自己去下载相关jar包谢谢只提供代码,自己去下载相关jar包谢谢

    Mybatis框架(批量添加)

    Mybatis框架(批量添加)

    mybatis批量添加

    mybatis批量添加的时候报错总结报错 parameter'_frch_item_0 not found

    mybatis如何防止SQL注入

    ### MyBatis如何防止SQL注入 #### SQL注入简介与危害 SQL注入是一种常见的Web应用程序安全漏洞,攻击者可以通过提交恶意的SQL代码到应用程序的输入字段中(如表单、URL等),利用这些输入来操控数据库执行非预期的...

    spring boot 整合mybatis,基于@Select注解方式

    本教程将详细介绍如何在Spring Boot项目中整合MyBatis,并使用@Select注解来实现数据库查询。 首先,我们需要在Spring Boot项目中引入MyBatis的依赖。在`pom.xml`文件中添加如下依赖: ```xml &lt;groupId&gt;org....

    Java实现mybatis批量插入数据到Oracle

    本篇文章将详细介绍如何利用Java和MyBatis实现批量插入数据到Oracle数据库中。 首先,我们需要理解Oracle数据库的一些基本概念。Oracle是世界上最流行的商业关系型数据库管理系统之一,以其强大的功能和高可靠性而...

    Mybatis实现多表联合查询和批量插入

    Mybatis实现多表联合查询和批量插入 Mybatis是一款流行的持久层框架,它可以帮助开发者快速、高效地访问数据库。在实际开发中,经常需要对多个表进行联合查询,或者对大量数据进行批量插入。本文将详细介绍如何使用...

    mybatis-generator,一对一,一对多,批量更新插入2

    使用方法请看博客 https://blog.csdn.net/bandaotixiruiqiang/article/details/72478361#comments_12931827

    Mybatis批量删除多表

    MyBatis中的批量删除操作可以通过在Mapper文件中定义delete语句来实现。例如,在EmpMapper.xml文件中,我们可以定义一个名为batchDeleteEmps的delete语句,用于批量删除员工信息。 ```xml &lt;delete id=...

Global site tag (gtag.js) - Google Analytics