文章目录
- 3.2 使用ScriptRunner执行脚本
- 3.2.1 ScriptRunner工具类简介
- 3.2.2 ScriptRunner工具类示例
- 3.2.3 ScriptRunner工具类源码
- 3.3 使用SqlRunner操作数据库
- 3.3.1 SqlRunner工具类简介
- 3.3.2 SqlRunner工具类示例
- 3.3.3 SqlRunner工具类源码解读
3.2 使用ScriptRunner执行脚本
3.2.1 ScriptRunner工具类简介
ScriptRunner是MyBatis提供的读取脚本文件中的SQL语句并执行的工具类。
它提供了一些属性,用于控制执行SQL脚本的一些行为:
源码1:org/apache/ibatis/jdbc/ScriptRunner.java
public class ScriptRunner {
// 执行脚本遇到异常时是否中断执行
private boolean stopOnError;
// 是否抛出SQLWarning警告
private boolean throwWarning;
// 是否自动提交
private boolean autoCommit;
// 属性为true时,批量执行文件中的SQL语句
// 属性为false时,逐条执行文件中的SQL语句
private boolean sendFullScript;
// 是否取出windows系统换行符中的 \r
private boolean removeCRs;
// 设置Statement属性是否支持转义处理
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;
// ......
}
3.2.2 ScriptRunner工具类示例
- 编写脚本文件 insert-data.sql ,并放在resources目录下:
insert into user (name, age, phone, birthday) values('U1', 15, '18705464523', '2024-03-09');
insert into user (name, age, phone, birthday) values('U2', 16, '18705464523', '2024-03-10');
insert into user (name, age, phone, birthday) values('U3', 17, '18705464523', '2024-03-11');
insert into user (name, age, phone, birthday) values('U4', 18, '18705464523', '2024-03-12');
insert into user (name, age, phone, birthday) values('U5', 19, '18705464523', '2024-03-13');
- 编写测试代码:
@Test
public void testScriptRunner() {
try {
Connection connection = DbUtils.getConnection();
ScriptRunner scriptRunner = new ScriptRunner(connection);
scriptRunner.runScript(Resources.getResourceAsReader("insert-data.sql"));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
- 执行测试代码,控制台打印了5条SQL语句,数据库插入了这5条数据:
如示例代码所示,ScriptRunner工具类的构造方法需要需要一个Connection对象作为参数。创建ScriptRunner对象后,调用该对象的runScript()
方法,传入一个SQL脚本文件的Reader对象,即可执行脚本文件中的SQL语句。
3.2.3 ScriptRunner工具类源码
源码2:org/apache/ibatis/jdbc/ScriptRunner.java
public void runScript(Reader reader) {
// 设置事务是否自动提交
setAutoCommit();
try {
if (sendFullScript) {
// 批量执行文件中的SQL语句
executeFullScript(reader);
} else {
// 逐条执行文件中的SQL语句
executeLineByLine(reader);
}
} finally {
rollbackConnection();
}
}
由 源码2 可知,runScript()
方法首先会调用setAutoCommit
方法使事务的自动提交配置生效,实际上就是将autoCommit属性的值设置到Connection对象中。
该方法接下来根据sendFullScript属性的值,判断出是批量执行文件中的SQL语句,还是逐条执行文件中的SQL语句。
源码3:org/apache/ibatis/jdbc/ScriptRunner.java
private void executeFullScript(Reader reader) {
StringBuilder script = new StringBuilder();
try {
// 逐行读取脚本文件的SQL语句,并以LINE_SEPARATOR拼接
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);
// 执行拼接起来的SQL语句
executeStatement(command);
// 提交事务
commitConnection();
} // catch ......
}
private void executeStatement(String command) throws SQLException {
try (Statement statement = connection.createStatement()) {
statement.setEscapeProcessing(escapeProcessing);
String sql = command;
if (removeCRs) {
// removeCRs属性是指是否取出windows系统换行符中的\r
sql = sql.replace("\r\n", "\n");
}
try {
// 执行SQL语句
boolean hasResults = statement.execute(sql);
while (!(!hasResults && statement.getUpdateCount() == -1)) {
checkWarnings(statement);
printResults(statement, hasResults);
hasResults = statement.getMoreResults();
}
} // catch ...
}
}
由 源码3 可知,批量执行SQL语句的方法是executeFullScript()
,该方法会逐行读取脚本文件的SQL语句,并以LINE_SEPARATOR(如果是Linux系统,则是"\n";如果是Windows系统,则是"\r\n")拼接起来,然后直接执行这个拼接起来的SQL语句,并提交事务。
源码4:org/apache/ibatis/jdbc/ScriptRunner.java
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 ......
}
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)) {
// 该行是待执行的命令
// 获取该行分号之前的内容
command.append(line, 0, line.lastIndexOf(delimiter));
// 添加一个分号
command.append(LINE_SEPARATOR);
println(command);
// 执行SQL语句
executeStatement(command.toString());
command.setLength(0);
} else if (trimmedLine.length() > 0) {
// 如果不是待执行的命令,则说明这条SQL还没结束
// 则追加到上一行内容
command.append(line);
command.append(LINE_SEPARATOR);
}
}
// 判断某行是否是注释:以"//"或"--"开头
private boolean lineIsComment(String trimmedLine) {
return trimmedLine.startsWith("//") || trimmedLine.startsWith("--");
}
// 判断某行是否是待执行的命令
// 如果分割符没有占一行,则必须包括分号
// 如果分割符占一行,则只能是分号
private boolean commandReadyToExecute(String trimmedLine) {
return !fullLineDelimiter && trimmedLine.contains(delimiter) || fullLineDelimiter && trimmedLine.equals(delimiter);
}
由 源码4 可知,逐行执行SQL语句的方法是executeLineByLine()
,该方法会逐行读取脚本文件的SQL语句,然后直接调用handleLine()
方法进行处理。
在handleLine()
方法中,首先会判断这一行内容是否是SQL注释(以"//“或”–"开头),如果是则打印注释内容;
其次判断这一行内容是否是待执行的命令(包含分号),如果是则立即调用Statement对象的execute()
方法执行SQL语句;
如果既不是注释,又不是命令,则说明这条SQL还没结束,需要追加到上一行内容中。
3.3 使用SqlRunner操作数据库
3.3.1 SqlRunner工具类简介
SqlRunner是MyBatis提供的用于操作数据库的工具类,它对JDBC做了很好的封装,结合SQL工具类,可以方便地通过Java代码执行SQL语句并检索SQL执行结果。
SqlRunner工具类提供了几个操作数据库的方法:
源码5:org/apache/ibatis/jdbc/SqlRunner.java
// 关闭Connection对象
public void closeConnection() {...}
// 执行SELECT语句,只返回1条记录,如果查询结果行数不等于1,则抛出异常
// SQL语句中可以使用占位符,可变参数args为占位符赋值
public Map<String, Object> selectOne(String sql, Object... args) throws SQLException {...}
// 执行SELECT语句,但返回多条记录,返回值中每个Map对象就是一行记录
// SQL语句中可以使用占位符,可变参数args为占位符赋值
public List<Map<String, Object>> selectAll(String sql, Object... args) throws SQLException {...}
// 分别执行INSERT、UPDATE、DELETE语句,插入、更新、删除一条数据
public int insert(String sql, Object... args) throws SQLException {...}
public int update(String sql, Object... args) throws SQLException {...}
public int delete(String sql, Object... args) throws SQLException {...}
// 执行任意一条SQL语句,最好为DDL语句
public void run(String sql) throws SQLException {...}
3.3.2 SqlRunner工具类示例
@Test
public void testSqlRunner() {
try {
Connection connection = DbUtils.getConnection();
SqlRunner sqlRunner = new SqlRunner(connection);
// 插入一条记录
String insertSql = new SQL() {{
INSERT_INTO("user");
INTO_COLUMNS("name", "age", "phone", "birthday");
INTO_VALUES("?,?,?,?");
}}.toString();
sqlRunner.insert(insertSql, "王母娘娘", 1000, "12530", "0000-05-21");
System.out.println("执行INSERT语句成功");
// 查询该条记录
String selectSql = new SQL() {{
SELECT("*");
FROM("user");
WHERE("name = ?");
}}.toString();
Map<String, Object> resultMap = sqlRunner.selectOne(selectSql, "王母娘娘");
System.out.println(resultMap);
// 修改该条记录
String updateSql = new SQL() {{
UPDATE("user");
SET("phone = ?");
WHERE("name = ?");
}}.toString();
sqlRunner.update(updateSql, "12345", "王母娘娘");
System.out.println("执行UPDATE语句成功");
// 再次查询该条记录
resultMap = sqlRunner.selectOne(selectSql, "王母娘娘");
System.out.println(resultMap);
// 删除这条记录
String deleteSql = new SQL() {{
DELETE_FROM("user");
WHERE("name = ?");
}}.toString();
sqlRunner.delete(deleteSql, "王母娘娘");
System.out.println("执行DELETE语句成功");
// 再次查询该条记录
resultMap = sqlRunner.selectOne(selectSql, "王母娘娘");
System.out.println(resultMap);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
控制台打印执行结果:
执行INSERT语句成功
{PHONE=12530, ID=20, BIRTHDAY=0001-05-21 00:00:00.0, NAME=王母娘娘, AGE=1000}
执行UPDATE语句成功
{PHONE=12345, ID=20, BIRTHDAY=0001-05-21 00:00:00.0, NAME=王母娘娘, AGE=1000}
执行DELETE语句成功
java.lang.RuntimeException: java.sql.SQLException: Statement returned 0 results where exactly one (1) was expected.
在以上案例中,先后执行SqlRunner工具类的insert()→selectOne()→update()→selectOne()→delete()→selectOne()
方法,测试了SqlRunner工具类的增删改查的功能。
可以发现,最后一次执行selectOne()
方法时抛出异常,提示返回0条记录但却使用了selectOne()
方法,这说明delete()方法确实生效了。
3.3.3 SqlRunner工具类源码解读
以selectAll()
方法为例,研究SqlRunner工具类的具体实现。
源码6:org/apache/ibatis/jdbc/SqlRunner.java
public List<Map<String, Object>> selectAll(String sql, Object... args) throws SQLException {
try (PreparedStatement ps = connection.prepareStatement(sql)) {
// 为参数占位符设置参数
setParameters(ps, args);
// 执行查询操作
try (ResultSet rs = ps.executeQuery()) {
// 处理结果
return getResults(rs);
}
}
}
由 源码6 可知,selectAll()
方法的逻辑有三步:
(1)调用Connection对象的prepareStatement()
方法获取PreparedStatement对象,并调用setParameters()
方法为SQL语句中的占位符设置参数;
(2)调用PreparedStatement的executeQuery()
方法执行查询操作;
(3)调用getResults()
方法将ResultSet对象转换为List集合,其中List集合中每一个Map对象对应数据库中的一条记录。
源码7:org/apache/ibatis/jdbc/SqlRunner.java
private void setParameters(PreparedStatement ps, Object... args) throws SQLException {
// 遍历参数
for (int i = 0, n = args.length; i < n; i++) {
// 参数为空,直接抛出异常
if (args[i] == null) {
// throw ...
}
if (args[i] instanceof Null) {
// 参数是Null类型的,则为占位符设置null
((Null) args[i]).getTypeHandler().setParameter(ps, i + 1, null, ((Null) args[i]).getJdbcType());
} else {
// 正常参数,根据参数类型获取对应的TypeHandler
TypeHandler typeHandler = typeHandlerRegistry.getTypeHandler(args[i].getClass());
if (typeHandler == null) {
// TypeHandler对象没有获取到,抛出异常
// throw ...
} else {
// 使用TypeHandler对象为占位符设置参数
typeHandler.setParameter(ps, i + 1, args[i], null);
}
}
}
}
由 源码7 可知,setParameters()
方法会对参数进行遍历,逐个处理:
(1)如果参数为空,直接抛出异常;
(2)如果参数是Null类型的,则为占位符设置null值;
(3)如果是正常参数,则先根据参数类型获取对应的TypeHandler;TypeHandler对象没有获取到,则抛出异常;获取到了则调用TypeHandler对象的setParameter()
方法为占位符设置参数。
源码:org/apache/ibatis/jdbc/SqlRunner.java
private List<Map<String, Object>> getResults(ResultSet rs) throws SQLException {
List<Map<String, Object>> list = new ArrayList<>();
List<String> columns = new ArrayList<>();
List<TypeHandler<?>> typeHandlers = new ArrayList<>();
// 1.获取ResultSetMetaData对象,通过该对象获取所有列名
ResultSetMetaData rsmd = rs.getMetaData();
for (int i = 0, n = rsmd.getColumnCount(); i < n; i++) {
columns.add(rsmd.getColumnLabel(i + 1));
try {
// 2.获取列的JDBC类型
Class<?> type = Resources.classForName(rsmd.getColumnClassName(i + 1));
// 根据列的JDBC类型获取对应的TypeHandler对象
TypeHandler<?> typeHandler = typeHandlerRegistry.getTypeHandler(type);
// 如果没有获取到则获取Object对象的TypeHandler对象
if (typeHandler == null) {
typeHandler = typeHandlerRegistry.getTypeHandler(Object.class);
}
typeHandlers.add(typeHandler);
} catch (Exception e) {
typeHandlers.add(typeHandlerRegistry.getTypeHandler(Object.class));
}
}
// 遍历ResultSet对象,将ResultSet对象中的记录行转换为Map对象
while (rs.next()) {
Map<String, Object> row = new HashMap<>();
for (int i = 0, n = columns.size(); i < n; i++) {
String name = columns.get(i);
TypeHandler<?> handler = typeHandlers.get(i);
// 往Map对象中添加一行数据
// key - 列名,转为大写
// value - 通过TypeHandler对象的getResult方法将JDBC类型转换为JAVA类型数据
row.put(name.toUpperCase(Locale.ENGLISH), handler.getResult(rs, name));
}
list.add(row);
}
return list;
}
由 源码 可知,getResults()
方法的处理逻辑是:
(1)获取ResultSetMetaData对象,该对象封装了结果集的元数据信息,包括所有的字段名称及列的数量等信息;
(2)遍历所有列,获取每一列的JDBC类型,根据JDBC类型获取对应的TypeHandler对象,如果没有获取到则获取Object对象的TypeHandler对象,最后将TypeHandler对象注册到变量名为typeHandlers的List集合中。
(3)遍历ResultSet对象,将ResultSet对象中的记录行转换为Map对象。这个Map对象的key是列名(转为大写),value是通过TypeHandler对象的getResult()
方法将JDBC类型数据转换为JAVA类型数据。
…
本节完,更多内容请查阅分类专栏:MyBatis3源码深度解析