SpringBatch/Cloud Task의 SafeMode에서 버그 문제 해결

8047 단어
문제 설명
일반적으로 회사에는 DBA가 있는데 DBA는 Safe mode를 열었을 가능성이 높다. 즉, 색인 조건이 없는 업데이트 작업을 지원하지 않을 것이다.Spring Batch/Cloud Task에는 표 JOBSEQ 또는 TASKSEQ의 테이블은 데이터 하나만 있고 업데이트 작업을 완성할 수 없습니다.
Could not increment ID for BATCH_JOB_SEQ sequence table; nested exception is java.sql.SQLException:
You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column

솔루션
DBA가 제공하는 데이터베이스라면 데이터베이스를 아무리 바꿔도 해결할 수 없다.개발자가 직접 만든 개인 라이브러리 (DBA 관리에 없는 것) 를 제공하지 않으면더 좋은 해결 방안은 Spring JDBC의 MySQL Max Value Incrementer를 교체하는 것이다
이 클래스는 DefaultDataFieldMaxValueIncrementerFactory에 의해 생성됩니다.
//DefaultDataFieldMaxValueIncrementerFactory.java
@Override
    public DataFieldMaxValueIncrementer getIncrementer(String incrementerType, String incrementerName) {
        DatabaseType databaseType = DatabaseType.valueOf(incrementerType.toUpperCase());

        if (databaseType == DB2 || databaseType == DB2AS400) {
            return new DB2SequenceMaxValueIncrementer(dataSource, incrementerName);
        }
        else if (databaseType == DB2ZOS) {
            return new DB2MainframeSequenceMaxValueIncrementer(dataSource, incrementerName);
        }
        else if (databaseType == DERBY) {
            return new DerbyMaxValueIncrementer(dataSource, incrementerName, incrementerColumnName);
        }
        else if (databaseType == HSQL) {
            return new HsqlMaxValueIncrementer(dataSource, incrementerName, incrementerColumnName);
        }
        else if (databaseType == H2) {
            return new H2SequenceMaxValueIncrementer(dataSource, incrementerName);
        }
        else if (databaseType == MYSQL) {
            MySQLMaxValueIncrementer mySQLMaxValueIncrementer = new MySQLMaxValueIncrementer(dataSource, incrementerName, incrementerColumnName);
            mySQLMaxValueIncrementer.setUseNewConnection(true);
            return mySQLMaxValueIncrementer;
        }
....
}

그러면 Default DataField Max Value Incrementer Factory를 다음과 같이 바꿉니다.
package io.github.slankka.springbatch.safemode.patch;

import org.springframework.batch.item.database.support.DefaultDataFieldMaxValueIncrementerFactory;
import org.springframework.batch.support.DatabaseType;
import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer;

import javax.sql.DataSource;

/**
 * project: springbatch safemode patch
 * 
To prevent error:
* * Could not increment ID for BATCH_JOB_SEQ sequence table;
* nested exception is java.sql.SQLException:
* You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column
*
* * @author slankka on 2019/8/30. */ public class SafeModeMysqlIncreamentFactory extends DefaultDataFieldMaxValueIncrementerFactory { private DataSource dataSource; private String incrementerColumnName = "ID"; public SafeModeMysqlIncreamentFactory(DataSource dataSource) { super(dataSource); this.dataSource = dataSource; } @Override public void setIncrementerColumnName(String incrementerColumnName) { super.setIncrementerColumnName(incrementerColumnName); this.incrementerColumnName = incrementerColumnName; } @Override public DataFieldMaxValueIncrementer getIncrementer(String incrementerType, String incrementerName) { DatabaseType databaseType = DatabaseType.valueOf(incrementerType.toUpperCase()); if (databaseType == DatabaseType.MYSQL) { SafeModeMysqlMaxValueIncreamenter mySQLMaxValueIncrementer = new SafeModeMysqlMaxValueIncreamenter(dataSource, incrementerName, incrementerColumnName); mySQLMaxValueIncrementer.setUseNewConnection(true); return mySQLMaxValueIncrementer; } return super.getIncrementer(incrementerType, incrementerName); } }

SafeModeMysqlMaxValueIncreamenter 클래스를 제공합니다. 이 클래스는 문제 해결의 관건입니다.
MySQLMaxValueIncrementer의 코드를 직접 복사하고 stmt를 수정합니다.excuteUpdate의 SQL 문구이며 끝에where columnName > 0을 추가합니다.이렇게 하면 세이프모드 검사를 속일 수 있다.또한 이 필드가 메인 키가 아니라면 메인 키로 설정하면 된다.
 @Override
    protected synchronized long getNextKey() throws DataAccessException {
        if (this.maxId == this.nextId) {
            /*
             * If useNewConnection is true, then we obtain a non-managed connection so our modifications
             * are handled in a separate transaction. If it is false, then we use the current transaction's
             * connection relying on the use of a non-transactional storage engine like MYISAM for the
             * incrementer table. We also use straight JDBC code because we need to make sure that the insert
             * and select are performed on the same connection (otherwise we can't be sure that last_insert_id()
             * returned the correct value).
             */
            Connection con = null;
            Statement stmt = null;
            boolean mustRestoreAutoCommit = false;
            try {
                if (this.useNewConnection) {
                    con = getDataSource().getConnection();
                    if (con.getAutoCommit()) {
                        mustRestoreAutoCommit = true;
                        con.setAutoCommit(false);
                    }
                } else {
                    con = DataSourceUtils.getConnection(getDataSource());
                }
                stmt = con.createStatement();
                if (!this.useNewConnection) {
                    DataSourceUtils.applyTransactionTimeout(stmt, getDataSource());
                }
                // Increment the sequence column...
                String columnName = getColumnName();
                try {
                    stmt.executeUpdate("update " + getIncrementerName() + " set " + columnName +
                            " = last_insert_id(" + columnName + " + " + getCacheSize() + ") where " +  columnName + " > 0");
                } catch (SQLException ex) {
                    throw new DataAccessResourceFailureException("Could not increment " + columnName + " for " +
                            getIncrementerName() + " sequence table", ex);
                }
                // Retrieve the new max of the sequence column...
                ResultSet rs = stmt.executeQuery(VALUE_SQL);
                try {
                    if (!rs.next()) {
                        throw new DataAccessResourceFailureException("last_insert_id() failed after executing an update");
                    }
                    this.maxId = rs.getLong(1);
                } finally {
                    JdbcUtils.closeResultSet(rs);
                }
                this.nextId = this.maxId - getCacheSize() + 1;
            } catch (SQLException ex) {
                throw new DataAccessResourceFailureException("Could not obtain last_insert_id()", ex);
            } finally {
                JdbcUtils.closeStatement(stmt);
                if (con != null) {
                    if (this.useNewConnection) {
                        try {
                            con.commit();
                            if (mustRestoreAutoCommit) {
                                con.setAutoCommit(true);
                            }
                        } catch (SQLException ignore) {
                            throw new DataAccessResourceFailureException(
                                    "Unable to commit new sequence value changes for " + getIncrementerName());
                        }
                        JdbcUtils.closeConnection(con);
                    } else {
                        DataSourceUtils.releaseConnection(con, getDataSource());
                    }
                }
            }
        } else {
            this.nextId++;
        }
        return this.nextId;
    }

사용법
slanka/spring-batch-safemode-patch 참조

좋은 웹페이지 즐겨찾기