0
点赞
收藏
分享

微信扫一扫

vector与list的区别与应用?

文章目录

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工具类示例

  1. 编写脚本文件 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');
  1. 编写测试代码:
@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);
    }
}
  1. 执行测试代码,控制台打印了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源码深度解析

举报

相关推荐

0 条评论