checkUpdateResult(updateDAO.execute( "order.updateStatus", ImmutableMap.of("orderId", order.getId(), "updateTs", TsUtils.now(), "preStatus", currStatus.getStatus(), "currentStatus",nextStatus.getStatus())) )
<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>
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
源碼:java
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) { } }