|
|
|
|
@@ -0,0 +1,422 @@
|
|
|
|
|
package com.cool.store.utils;
|
|
|
|
|
|
|
|
|
|
import org.apache.commons.collections4.MapUtils;
|
|
|
|
|
import org.apache.commons.logging.Log;
|
|
|
|
|
import org.apache.commons.logging.LogFactory;
|
|
|
|
|
import org.apache.ibatis.session.ExecutorType;
|
|
|
|
|
import org.apache.ibatis.session.SqlSession;
|
|
|
|
|
import org.mybatis.spring.SqlSessionTemplate;
|
|
|
|
|
import org.mybatis.spring.SqlSessionUtils;
|
|
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
|
|
import org.springframework.core.io.support.EncodedResource;
|
|
|
|
|
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
|
|
|
|
import org.springframework.jdbc.datasource.init.*;
|
|
|
|
|
import org.springframework.lang.Nullable;
|
|
|
|
|
import org.springframework.stereotype.Component;
|
|
|
|
|
import org.springframework.util.Assert;
|
|
|
|
|
import org.springframework.util.StringUtils;
|
|
|
|
|
|
|
|
|
|
import java.io.IOException;
|
|
|
|
|
import java.io.LineNumberReader;
|
|
|
|
|
import java.sql.Connection;
|
|
|
|
|
import java.sql.SQLException;
|
|
|
|
|
import java.sql.Statement;
|
|
|
|
|
import java.util.ArrayList;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
import java.util.Objects;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @author zhangchenbiao
|
|
|
|
|
* @FileName: ScriptUtil
|
|
|
|
|
* @Description: 脚本工具
|
|
|
|
|
* @date 2022-01-21 16:48
|
|
|
|
|
*/
|
|
|
|
|
@Component
|
|
|
|
|
public class ScriptUtil {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Default statement separator within SQL scripts: {@code ";"}.
|
|
|
|
|
*/
|
|
|
|
|
public static final String DEFAULT_STATEMENT_SEPARATOR = ";";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fallback statement separator within SQL scripts: {@code "\n"}.
|
|
|
|
|
* <p>Used if neither a custom separator nor the
|
|
|
|
|
* {@link #DEFAULT_STATEMENT_SEPARATOR} is present in a given script.
|
|
|
|
|
*/
|
|
|
|
|
public static final String FALLBACK_STATEMENT_SEPARATOR = "\n";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public static final String EOF_STATEMENT_SEPARATOR = "^^^ END OF SCRIPT ^^^";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Default prefix for single-line comments within SQL scripts: {@code "--"}.
|
|
|
|
|
*/
|
|
|
|
|
public static final String DEFAULT_COMMENT_PREFIX = "--";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Default start delimiter for block comments within SQL scripts: {@code "/*"}.
|
|
|
|
|
*/
|
|
|
|
|
public static final String DEFAULT_BLOCK_COMMENT_START_DELIMITER = "/*";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Default end delimiter for block comments within SQL scripts: <code>"*/"</code>.
|
|
|
|
|
*/
|
|
|
|
|
public static final String DEFAULT_BLOCK_COMMENT_END_DELIMITER = "*/";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 变量开始字符$
|
|
|
|
|
*/
|
|
|
|
|
public static final String VARIABLE_START_DELIMITER = "\\$\\{";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 变量开始字符#
|
|
|
|
|
*/
|
|
|
|
|
public static final String VARIABLE_START_DELIMITER2 = "\\#\\{";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 变量结束字符
|
|
|
|
|
*/
|
|
|
|
|
public static final String VARIABLE_END_DELIMITER = "}";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 单引号
|
|
|
|
|
*/
|
|
|
|
|
public static final String SINGLE_QUOTE = "'";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private final SqlSessionTemplate sqlSessionTemplate;
|
|
|
|
|
private final DataSourceTransactionManager dataSourceTransactionManager;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@Autowired
|
|
|
|
|
public ScriptUtil(SqlSessionTemplate sqlSessionTemplate, DataSourceTransactionManager dataSourceTransactionManager) {
|
|
|
|
|
this.sqlSessionTemplate = sqlSessionTemplate;
|
|
|
|
|
this.dataSourceTransactionManager = dataSourceTransactionManager;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static final Log logger = LogFactory.getLog(ScriptUtil.class);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Split an SQL script into separate statements delimited by the provided
|
|
|
|
|
* separator string. Each individual statement will be added to the provided
|
|
|
|
|
* {@code List}.
|
|
|
|
|
* <p>Within the script, the provided {@code commentPrefix} will be honored:
|
|
|
|
|
* any text beginning with the comment prefix and extending to the end of the
|
|
|
|
|
* line will be omitted from the output. Similarly, the provided
|
|
|
|
|
* {@code blockCommentStartDelimiter} and {@code blockCommentEndDelimiter}
|
|
|
|
|
* delimiters will be honored: any text enclosed in a block comment will be
|
|
|
|
|
* omitted from the output. In addition, multiple adjacent whitespace characters
|
|
|
|
|
* will be collapsed into a single space.
|
|
|
|
|
* @param resource the resource from which the script was read
|
|
|
|
|
* @param script the SQL script; never {@code null} or empty
|
|
|
|
|
* @param separator text separating each statement — typically a ';' or
|
|
|
|
|
* newline character; never {@code null}
|
|
|
|
|
* @param commentPrefix the prefix that identifies SQL line comments —
|
|
|
|
|
* typically "--"; never {@code null} or empty
|
|
|
|
|
* @param blockCommentStartDelimiter the <em>start</em> block comment delimiter;
|
|
|
|
|
* never {@code null} or empty
|
|
|
|
|
* @param blockCommentEndDelimiter the <em>end</em> block comment delimiter;
|
|
|
|
|
* never {@code null} or empty
|
|
|
|
|
* @param statements the list that will contain the individual statements
|
|
|
|
|
* @throws ScriptException if an error occurred while splitting the SQL script
|
|
|
|
|
*/
|
|
|
|
|
public static void splitSqlScript(@Nullable EncodedResource resource, String script,
|
|
|
|
|
String separator, String commentPrefix, String blockCommentStartDelimiter,
|
|
|
|
|
String blockCommentEndDelimiter, List<String> statements) throws ScriptException {
|
|
|
|
|
|
|
|
|
|
Assert.hasText(script, "'script' must not be null or empty");
|
|
|
|
|
Assert.notNull(separator, "'separator' must not be null");
|
|
|
|
|
Assert.hasText(commentPrefix, "'commentPrefix' must not be null or empty");
|
|
|
|
|
Assert.hasText(blockCommentStartDelimiter, "'blockCommentStartDelimiter' must not be null or empty");
|
|
|
|
|
Assert.hasText(blockCommentEndDelimiter, "'blockCommentEndDelimiter' must not be null or empty");
|
|
|
|
|
|
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
|
boolean inSingleQuote = false;
|
|
|
|
|
boolean inDoubleQuote = false;
|
|
|
|
|
boolean inEscape = false;
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < script.length(); i++) {
|
|
|
|
|
char c = script.charAt(i);
|
|
|
|
|
if (inEscape) {
|
|
|
|
|
inEscape = false;
|
|
|
|
|
sb.append(c);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
// MySQL style escapes
|
|
|
|
|
if (c == '\\') {
|
|
|
|
|
inEscape = true;
|
|
|
|
|
sb.append(c);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (!inDoubleQuote && (c == '\'')) {
|
|
|
|
|
inSingleQuote = !inSingleQuote;
|
|
|
|
|
}
|
|
|
|
|
else if (!inSingleQuote && (c == '"')) {
|
|
|
|
|
inDoubleQuote = !inDoubleQuote;
|
|
|
|
|
}
|
|
|
|
|
if (!inSingleQuote && !inDoubleQuote) {
|
|
|
|
|
if (script.startsWith(separator, i)) {
|
|
|
|
|
// We've reached the end of the current statement
|
|
|
|
|
if (sb.length() > 0) {
|
|
|
|
|
statements.add(sb.toString());
|
|
|
|
|
sb = new StringBuilder();
|
|
|
|
|
}
|
|
|
|
|
i += separator.length() - 1;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
else if (script.startsWith(commentPrefix, i)) {
|
|
|
|
|
// Skip over any content from the start of the comment to the EOL
|
|
|
|
|
int indexOfNextNewline = script.indexOf('\n', i);
|
|
|
|
|
if (indexOfNextNewline > i) {
|
|
|
|
|
i = indexOfNextNewline;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
// If there's no EOL, we must be at the end of the script, so stop here.
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (script.startsWith(blockCommentStartDelimiter, i)) {
|
|
|
|
|
// Skip over any block comments
|
|
|
|
|
int indexOfCommentEnd = script.indexOf(blockCommentEndDelimiter, i);
|
|
|
|
|
if (indexOfCommentEnd > i) {
|
|
|
|
|
i = indexOfCommentEnd + blockCommentEndDelimiter.length() - 1;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
throw new ScriptParseException(
|
|
|
|
|
"Missing block comment end delimiter: " + blockCommentEndDelimiter, resource);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (c == ' ' || c == '\n' || c == '\t') {
|
|
|
|
|
// Avoid multiple adjacent whitespace characters
|
|
|
|
|
if (sb.length() > 0 && sb.charAt(sb.length() - 1) != ' ') {
|
|
|
|
|
c = ' ';
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
sb.append(c);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (StringUtils.hasText(sb)) {
|
|
|
|
|
statements.add(sb.toString());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Read a script from the provided resource, using the supplied comment prefix
|
|
|
|
|
* and statement separator, and build a {@code String} containing the lines.
|
|
|
|
|
* <p>Lines <em>beginning</em> with the comment prefix are excluded from the
|
|
|
|
|
* results; however, line comments anywhere else — for example, within
|
|
|
|
|
* a statement — will be included in the results.
|
|
|
|
|
* @param resource the {@code EncodedResource} containing the script
|
|
|
|
|
* to be processed
|
|
|
|
|
* @param commentPrefix the prefix that identifies comments in the SQL script —
|
|
|
|
|
* typically "--"
|
|
|
|
|
* @param separator the statement separator in the SQL script — typically ";"
|
|
|
|
|
* @return a {@code String} containing the script lines
|
|
|
|
|
* @throws IOException in case of I/O errors
|
|
|
|
|
*/
|
|
|
|
|
private static String readScript(EncodedResource resource, @Nullable String commentPrefix,
|
|
|
|
|
@Nullable String separator) throws IOException {
|
|
|
|
|
|
|
|
|
|
LineNumberReader lnr = new LineNumberReader(resource.getReader());
|
|
|
|
|
try {
|
|
|
|
|
return readScript(lnr, commentPrefix, separator);
|
|
|
|
|
}
|
|
|
|
|
finally {
|
|
|
|
|
lnr.close();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Read a script from the provided {@code LineNumberReader}, using the supplied
|
|
|
|
|
* comment prefix and statement separator, and build a {@code String} containing
|
|
|
|
|
* the lines.
|
|
|
|
|
* <p>Lines <em>beginning</em> with the comment prefix are excluded from the
|
|
|
|
|
* results; however, line comments anywhere else — for example, within
|
|
|
|
|
* a statement — will be included in the results.
|
|
|
|
|
* @param lineNumberReader the {@code LineNumberReader} containing the script
|
|
|
|
|
* to be processed
|
|
|
|
|
* @param commentPrefix the prefix that identifies comments in the SQL script —
|
|
|
|
|
* typically "--"
|
|
|
|
|
* @param separator the statement separator in the SQL script — typically ";"
|
|
|
|
|
* @return a {@code String} containing the script lines
|
|
|
|
|
* @throws IOException in case of I/O errors
|
|
|
|
|
*/
|
|
|
|
|
public static String readScript(LineNumberReader lineNumberReader, @Nullable String commentPrefix,
|
|
|
|
|
@Nullable String separator) throws IOException {
|
|
|
|
|
|
|
|
|
|
String currentStatement = lineNumberReader.readLine();
|
|
|
|
|
StringBuilder scriptBuilder = new StringBuilder();
|
|
|
|
|
while (currentStatement != null) {
|
|
|
|
|
if (commentPrefix != null && !currentStatement.startsWith(commentPrefix)) {
|
|
|
|
|
if (scriptBuilder.length() > 0) {
|
|
|
|
|
scriptBuilder.append('\n');
|
|
|
|
|
}
|
|
|
|
|
scriptBuilder.append(currentStatement);
|
|
|
|
|
}
|
|
|
|
|
currentStatement = lineNumberReader.readLine();
|
|
|
|
|
}
|
|
|
|
|
appendSeparatorToScriptIfNecessary(scriptBuilder, separator);
|
|
|
|
|
return scriptBuilder.toString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void appendSeparatorToScriptIfNecessary(StringBuilder scriptBuilder, @Nullable String separator) {
|
|
|
|
|
if (separator == null) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
String trimmed = separator.trim();
|
|
|
|
|
if (trimmed.length() == separator.length()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// separator ends in whitespace, so we might want to see if the script is trying
|
|
|
|
|
// to end the same way
|
|
|
|
|
if (scriptBuilder.lastIndexOf(trimmed) == scriptBuilder.length() - trimmed.length()) {
|
|
|
|
|
scriptBuilder.append(separator.substring(trimmed.length()));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Does the provided SQL script contain the specified delimiter?
|
|
|
|
|
* @param script the SQL script
|
|
|
|
|
* @param delim the string delimiting each statement - typically a ';' character
|
|
|
|
|
*/
|
|
|
|
|
public static boolean containsSqlScriptDelimiters(String script, String delim) {
|
|
|
|
|
boolean inLiteral = false;
|
|
|
|
|
boolean inEscape = false;
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < script.length(); i++) {
|
|
|
|
|
char c = script.charAt(i);
|
|
|
|
|
if (inEscape) {
|
|
|
|
|
inEscape = false;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
// MySQL style escapes
|
|
|
|
|
if (c == '\\') {
|
|
|
|
|
inEscape = true;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (c == '\'') {
|
|
|
|
|
inLiteral = !inLiteral;
|
|
|
|
|
}
|
|
|
|
|
if (!inLiteral && script.startsWith(delim, i)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public void executeSqlScript(EncodedResource resource, Map<String, Object> params) throws ScriptException {
|
|
|
|
|
String commentPrefix = DEFAULT_COMMENT_PREFIX;
|
|
|
|
|
String separator = DEFAULT_STATEMENT_SEPARATOR;
|
|
|
|
|
try {
|
|
|
|
|
if (logger.isDebugEnabled()) {
|
|
|
|
|
logger.debug("Executing SQL script from " + resource);
|
|
|
|
|
}
|
|
|
|
|
long startTime = System.currentTimeMillis();
|
|
|
|
|
String script;
|
|
|
|
|
try {
|
|
|
|
|
script = readScript(resource, commentPrefix, separator);
|
|
|
|
|
}catch (IOException ex) {
|
|
|
|
|
throw new CannotReadScriptException(resource, ex);
|
|
|
|
|
}
|
|
|
|
|
if (!EOF_STATEMENT_SEPARATOR.equals(separator) && !containsSqlScriptDelimiters(script, separator)) {
|
|
|
|
|
separator = FALLBACK_STATEMENT_SEPARATOR;
|
|
|
|
|
}
|
|
|
|
|
List<String> statements = new ArrayList<>();
|
|
|
|
|
splitSqlScript(resource, script, separator, commentPrefix, DEFAULT_BLOCK_COMMENT_START_DELIMITER,
|
|
|
|
|
DEFAULT_BLOCK_COMMENT_END_DELIMITER, statements);
|
|
|
|
|
int stmtNumber = 0;
|
|
|
|
|
SqlSession sqlSession = SqlSessionUtils.getSqlSession(
|
|
|
|
|
sqlSessionTemplate.getSqlSessionFactory(), ExecutorType.BATCH,
|
|
|
|
|
sqlSessionTemplate.getPersistenceExceptionTranslator());
|
|
|
|
|
Connection connection = null;
|
|
|
|
|
Statement stmt = null;
|
|
|
|
|
try {
|
|
|
|
|
connection = sqlSession.getConnection();
|
|
|
|
|
stmt = connection.createStatement();
|
|
|
|
|
connection.setAutoCommit(false);
|
|
|
|
|
for (String statement : statements) {
|
|
|
|
|
stmtNumber++;
|
|
|
|
|
try {
|
|
|
|
|
statement = sqlVariableReplace(statement, params);
|
|
|
|
|
stmt.execute(statement);
|
|
|
|
|
int rowsAffected = stmt.getUpdateCount();
|
|
|
|
|
logger.info(rowsAffected + " returned as update count for SQL: " + statement);
|
|
|
|
|
}catch (SQLException ex) {
|
|
|
|
|
logger.error(ex);
|
|
|
|
|
ex.printStackTrace();
|
|
|
|
|
connection.rollback();
|
|
|
|
|
sqlSession.clearCache();
|
|
|
|
|
throw new ScriptStatementFailedException(statement, stmtNumber, resource, ex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
connection.commit();
|
|
|
|
|
sqlSession.clearCache();
|
|
|
|
|
}catch (Exception e){
|
|
|
|
|
logger.error(e);
|
|
|
|
|
e.printStackTrace();
|
|
|
|
|
connection.rollback();
|
|
|
|
|
sqlSession.clearCache();
|
|
|
|
|
throw new Exception(e);
|
|
|
|
|
}finally {
|
|
|
|
|
try {
|
|
|
|
|
if(Objects.nonNull(stmt)){
|
|
|
|
|
stmt.close();
|
|
|
|
|
}
|
|
|
|
|
}catch (Throwable ex) {
|
|
|
|
|
logger.trace("Could not close JDBC Statement", ex);
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
if(Objects.nonNull(connection)){
|
|
|
|
|
connection.close();
|
|
|
|
|
}
|
|
|
|
|
} catch (SQLException throwables) {
|
|
|
|
|
logger.trace("Could not close JDBC Statement", throwables);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
long elapsedTime = System.currentTimeMillis() - startTime;
|
|
|
|
|
if (logger.isDebugEnabled()) {
|
|
|
|
|
logger.debug("Executed SQL script from " + resource + " in " + elapsedTime + " ms.");
|
|
|
|
|
}
|
|
|
|
|
}catch (Exception ex) {
|
|
|
|
|
if (ex instanceof ScriptException) {
|
|
|
|
|
throw (ScriptException) ex;
|
|
|
|
|
}
|
|
|
|
|
throw new UncategorizedScriptException(
|
|
|
|
|
"Failed to execute database script from resource [" + resource + "]", ex);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 处理sql变量
|
|
|
|
|
* @param statement
|
|
|
|
|
* @param params
|
|
|
|
|
* @return
|
|
|
|
|
*/
|
|
|
|
|
public String sqlVariableReplace(String statement, Map<String, Object> params){
|
|
|
|
|
if(MapUtils.isEmpty(params)){
|
|
|
|
|
return statement;
|
|
|
|
|
}
|
|
|
|
|
if(!statement.contains(VARIABLE_START_DELIMITER) && !statement.contains(VARIABLE_END_DELIMITER)){
|
|
|
|
|
return statement;
|
|
|
|
|
}
|
|
|
|
|
for (String key : params.keySet()) {
|
|
|
|
|
String realKey = VARIABLE_START_DELIMITER + key + VARIABLE_END_DELIMITER;
|
|
|
|
|
String realKey2 = VARIABLE_START_DELIMITER2 + key + VARIABLE_END_DELIMITER;
|
|
|
|
|
statement = statement.replaceAll(realKey, params.get(key).toString());
|
|
|
|
|
statement = statement.replaceAll(realKey2, SINGLE_QUOTE + params.get(key) + SINGLE_QUOTE);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
return statement;
|
|
|
|
|
}
|
|
|
|
|
}
|