赞
踩
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; // ...... }
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);
}
}
如示例代码所示,ScriptRunner工具类的构造方法需要需要一个Connection对象作为参数。创建ScriptRunner对象后,调用该对象的runScript()
方法,传入一个SQL脚本文件的Reader对象,即可执行脚本文件中的SQL语句。
源码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还没结束,需要追加到上一行内容中。
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 {...}
@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()方法确实生效了。
以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源码深度解析
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。