Mybatis为了统一读取SQL脚本文件中的SQL语句并执行,所以提供了ScriptRunner工具类(org.apache.ibatis.jdbc.ScriptRunner)。使用该类可以读取和执行SQL脚本,示例如下:
try{
Connection connection = DriverManager.getConnection("数据库连接");
ScriptRunner scriptRunner = new ScriptRunner(connection);
scriptRunner.runScript(Resources.getResourceAsReader("mysql.sql"));
} catch(Exception e){
// 异常
}
概述
从Mybatis源代码上来看ScriptRunner类没有继承任何父类,没有实现任何接口,是一个单纯的工具类。从上述示例代码可以看出,构造ScriptRunner需要先构造一个Connection对象,之所以需要Connection,是因为ScriptRunner类在读取解析完SQL脚本文件之后,会执行解析得到的SQL语句。为了进一步了解ScriptRunner类,首先看一下ScriptRunner类中定义的一些属性值。
public class ScriptRunner {
// SQL异常是否中断程序执行
private boolean stopOnError;
// 是否抛出SQLWarning
private boolean throwWarning;
// 设置是否自动提交
private boolean autoCommit;
// true:批量执行文件中的SQL语句
// false:逐条执行SQL语句,默认情况下,以分号分隔
private boolean sendFullScript;
// 是否去除SQL语句中的Windows系统里的\r换行符
private boolean removeCRs;
// 设置Statement中的escapeProcessing属性
private boolean escapeProcessing = true;
// 日志输出
private PrintWriter logWriter = new PrintWriter(System.out);
// 错误日志输出
private PrintWriter errorLogWriter = new PrintWriter(System.err);
// 脚本文件SQL语句的分隔符,默认为;(分号)
private String delimiter = DEFAULT_DELIMITER;
// 是否支持SQL语句分割符
private boolean fullLineDelimiter;
}
以上这些属性Mybatis都提供了对应的Setter方法来控制ScriptRunner工具类执行SQL脚本的行为,上述仅是对这些属性做了简单的介绍,后续会结合实际再进一步分析讲解各个属性。
runScript()方法
ScriptRunner类中仅提供了一个runScript()方法用于执行SQL脚本文件,runScript()方法也是其最为核心的方法。
public void runScript(Reader reader) {
setAutoCommit();
try {
if (sendFullScript) {
executeFullScript(reader);
} else {
executeLineByLine(reader);
}
} finally {
rollbackConnection();
}
}
从mybatis源代码来看runScript一共做了以下几件事:
- 调用setAutoCommit()方法设置autoCommit为true,开启自动提交;
- 判断sendFullScript属性值,如果值为true,调用executeFullScript()方法一次性读取SQL脚本中的所有内容,如果值为false,调用executeLineByLine()方法逐行读取SQL脚本中的内容,以分号作为一条SQL语句结束的标志;
- 最后调用rollbackConnection()方法来rollbackConnection。
executeFullScript()方法
private void executeFullScript(Reader reader) {
StringBuilder script = new StringBuilder();
try {
BufferedReader lineReader = new BufferedReader(reader);
String line;
while ((line = lineReader.readLine()) != null) {
script.append(line);
script.append(LINE_SEPARATOR);
}
String command = script.toString();
println(command);
executeStatement(command);
commitConnection();
} catch (Exception e) {
String message = "Error executing: " + script + ". Cause: " + e;
printlnError(message);
throw new RuntimeSqlException(message, e);
}
}
先来看看executeFullScript()方法内部的逻辑细节,从Mybatis源代码来看executeFullScript()方法一共做了如下几件事:
- 循环读取SQL脚本中的SQL语句,并将读取到的SQL语句组装放到一个StringBuilder类中;
- 调用println()方法打印SQL语句,println其内部使用到ScriptRunner类的logWriter属性值;
- 调用executeStatement(String command)方法执行SQL语句;
- 调用commitConnection()方法,如果ScriptRunner类的autoCommit属性值为false,就手动调用connection.commit()方法提交事务;
- 如果在以上整个执行过程的发生任务异常,就会调用printlnError()方法来打印异常信息。
executeLineByLine()方法
private void executeLineByLine(Reader reader) {
StringBuilder command = new StringBuilder();
try {
BufferedReader lineReader = new BufferedReader(reader);
String line;
while ((line = lineReader.readLine()) != null) {
handleLine(command, line);
}
commitConnection();
checkForMissingLineTerminator(command);
} catch (Exception e) {
String message = "Error executing: " + command + ". Cause: " + e;
printlnError(message);
throw new RuntimeSqlException(message, e);
}
}
executeLineByLine()方法内部的逻辑细节,从Mybatis源代码来看executeLineByLine()方法,该方法中对脚本中的内容逐行读取,然后调用handleLine()方法处理每行读取的内容。其核心逻辑详见handleLine方法,
private void handleLine(StringBuilder command, String line) throws SQLException {
String trimmedLine = line.trim();
if (lineIsComment(trimmedLine)) {
// 判断该行是否是注释
Matcher matcher = DELIMITER_PATTERN.matcher(trimmedLine);
if (matcher.find()) {
delimiter = matcher.group(5);
}
println(trimmedLine);
} else if (commandReadyToExecute(trimmedLine)) {
// 判断该行是否包含分号
// 获取分号之前的SQL语句
command.append(line, 0, line.lastIndexOf(delimiter));
command.append(LINE_SEPARATOR);
println(command);
executeStatement(command.toString());
// 重置StringBuilder
command.setLength(0);
} else if (trimmedLine.length() > 0) {
// 该行不包含分号,
// 这条SQL语句未结束,将当前行追加到StringBuilder
command.append(line);
command.append(LINE_SEPARATOR);
}
}
从Mybatis源代码来看,
- 首先判断该行是否是SQL注释语法,如果该行是注释就直接调用println()方法打印该行语句;
- 然后如果该行不是注释,则判断该行是否包含分号。如包含分号就说明是一条完整的SQL语句,接着做出如下处理:
- 获取分号之前的SQL语句
- 添加LINE_SEPARATOR属性对应的System.lineSeparator()的分隔符
- 打印SQL语句
- 调用executeStatement()方法执行SQL语句
- 重置当前使用的StringBuilder类
- 最后该行既不是注释,也不是完整的SQL语句。就说明该条SQL语句没有结束,将对应的SQL语句追加到StringBuilder类。