MyBatis 和 Spring JDBC 比较

从 Java 运行 SQL 脚本,有两个库:MyBatis 和 Spring JDBC。MyBatis 提供了ScriptRunner类,Spring JDBC 提供了ScriptUtils来直接从磁盘读取 SQL 脚本文件并在目标数据库上运行。

使用MyBatis ScriptRunner执行SQL脚本
首先,让我们通过在pom.xml中包含以下内容来添加mybatis的Maven 依赖项:

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.7</version>
</dependency>

现在,让我们看一下MyBatisScriptUtility类:

public class MyBatisScriptUtility {
    public static void runScript(
      String path,
      Connection connection
    ) throws Exception {
      ScriptRunner scriptRunner = new ScriptRunner(connection);
      scriptRunner.setSendFullScript(false);
      scriptRunner.setStopOnError(true);
      scriptRunner.runScript(new java.io.FileReader(path));
    }
}

从上面的代码可以明显看出,ScriptRunner提供了逐行执行脚本以及一次性执行完整脚本的选项。

在执行SQL文件之前,我们先看一下:

-- Create the employees table if it doesn't exist
CREATE TABLE  employees (
    id INT PRIMARY KEY,
    first_name VARCHAR(50),
    last_name VARCHAR(50),
    department VARCHAR(50),
    salary DECIMAL(10, 2)
);

-- Insert employee records
INSERT INTO employees (id, first_name, last_name, department, salary)
VALUES (1, 'John', 'Doe', 'HR', 50000.00);

INSERT INTO employees (id, first_name, last_name, department, salary)
VALUES (2, 'Jane', 'Smith', 'IT', 60000.00);
--More SQL statements ....

我们可以看到,上面的文件由块注释、单行注释、空行、建表语句和插入语句混合组成。这使我们能够测试本文中讨论的库的解析能力。

执行完整脚本文件的实现非常简单。为此,从磁盘读取整个文件并作为字符串参数传递给方法java.sql.Statement.execute()。
逐行运行它:

@Test
public void givenConnectionObject_whenSQLFile_thenExecute() throws Exception {

    String path = new File(ClassLoader.getSystemClassLoader().getResource("employee.sql").getFile()).toPath().toString();
    MyBatisScriptUtility.runScript(path, connection);

    Statement statement = connection.createStatement();
    ResultSet resultSet = statement.executeQuery(
"SELECT COUNT(1) FROM employees");
    if (resultSet.next()) {
        int count = resultSet.getInt(1);
        Assert.assertEquals(
"Incorrect number of records inserted", 20, count);
    }
}

在上面的示例中,我们使用了一个 SQL 文件来创建一个员工表,然后向其中插入 20 条记录。

使用Spring JDBC ScriptUtils执行SQL脚本
让我们首先处理Maven 依赖关系:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.3.29</version>
</dependency>

之后,让我们看一下SpringScriptUtility类:

public class SpringScriptUtility {
    public static void runScript(String path, Connection connection) {
        boolean continueOrError = false;
        boolean ignoreFailedDrops = false;
        String commentPrefix = "--";
        String separator =
";";
        String blockCommentStartDelimiter =
"/*";
        String blockCommentEndDelimiter =
"*/";

        ScriptUtils.executeSqlScript(
          connection,
          new EncodedResource(new PathResource(path)),
          continueOrError,
          ignoreFailedDrops,
          commentPrefix,
          separator,
          blockCommentStartDelimiter,
          blockCommentEndDelimiter
        );
    }
}

正如我们在上面看到的,ScriptUtils 提供了许多读取 SQL 文件的选项。因此,它支持多个数据库引擎,这些引擎使用不同的分隔符来识别注释,而不仅仅是典型的“-”、“/*”和“*/”。此外,还有两个参数 continueOnError 和ignoreFailedDrops 其用途不言而喻。
与 MyBatis 库不同,ScriptUtils不提供运行完整脚本的选项,而是更喜欢一条一条地运行 SQL 语句。这可以通过查看其源代码来确认。

我们看一下执行过程:

@Test
public void givenConnectionObject_whenSQLFile_thenExecute() throws Exception {
    String path = new File(ClassLoader.getSystemClassLoader()
      .getResource("employee.sql").getFile()).toPath().toString();
    SpringScriptUtility.runScript(path, connection);

    Statement statement = connection.createStatement();
    ResultSet resultSet = statement.executeQuery(
"SELECT COUNT(1) FROM employees");
    if (resultSet.next()) {
        int count = resultSet.getInt(1);
        Assert.assertEquals(
"Incorrect number of records inserted", 20, count);
    }
}

在上面的方法中,我们只是使用path 和connection 对象调用SpringScriptUtility.runScript () 。


使用JDBC批量执行SQL语句
我们已经看到这两个库几乎都支持执行 SQL 文件。但它们都没有提供批量运行 SQL 语句的选项。这是执行大型 SQL 文件的一个重要功能。
因此,让我们开发自己的SqlScriptBatchExecutor:

static void executeBatchedSQL(String scriptFilePath, Connection connection, int batchSize) throws Exception {
    List<String> sqlStatements = parseSQLScript(scriptFilePath);
    executeSQLBatches(connection, sqlStatements, batchSize);
}

上面的实现可以概括为两行:方法parseSQLScript() 从文件中获取SQL语句,executeSQLBatches() 批量执行它们。
让我们看一下parseSQLScript()方法:

static List<String> parseSQLScript(String scriptFilePath) throws IOException {
    List<String> sqlStatements = new ArrayList<>();

    try (BufferedReader reader = new BufferedReader(new FileReader(scriptFilePath))) {
        StringBuilder currentStatement = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            Matcher commentMatcher = COMMENT_PATTERN.matcher(line);
            line = commentMatcher.replaceAll("");

            line = line.trim();

            if (line.isEmpty()) {
                continue;
            }

            currentStatement.append(line).append(
" ");

            if (line.endsWith(
";")) {
                sqlStatements.add(currentStatement.toString());
                logger.info(currentStatement.toString());
                currentStatement.setLength(0);
            }
        }
    } catch (IOException e) {
       throw e;
    }
    return sqlStatements;
}

我们使用 COMMENT_PATTERN = Pattern.compile("-.*|/\\*(.|[\\\\n])*?\\*/") 来识别注释和空行,然后将它们从 SQL 文件中删除。与 MyBatis 一样,我们也只支持默认的注释分隔符。

我们可以看看 executeSQLBatches() 方法:

static void executeSQLBatches(Connection connection, List<String> sqlStatements, int batchSize) 
        throws SQLException {
    int count = 0;
    Statement statement = connection.createStatement();

    for (String sql : sqlStatements) {
        statement.addBatch(sql);
        count++;

        if (count % batchSize == 0) {
            logger.info("Executing batch");
            statement.executeBatch();
            statement.clearBatch();
        }
    }
    if (count % batchSize != 0) {
        statement.executeBatch();
    }
    connnection.commit();
}

上述方法获取 SQL 语句列表,对其进行遍历,然后在批量大小增长到参数 batchSize 的值时执行。

让我们看看自定义程序的运行情况:

@Test
public void givenConnectionObject_whenSQLFile_thenExecute() throws Exception {
    String path = new File(
      ClassLoader.getSystemClassLoader().getResource("employee.sql").getFile()).toPath().toString();
    SqlScriptBatchExecutor.executeBatchedSQL(path, connection, 10);
    Statement statement = connection.createStatement();
    ResultSet resultSet = statement.executeQuery(
"SELECT COUNT(1) FROM employees");

    if (resultSet.next()) {
        int count = resultSet.getInt(1);
        Assert.assertEquals(
"Incorrect number of records inserted", 20, count);
    }
}

它分两批执行 SQL 语句,每批 10 条。值得注意的是,这里的批次大小是参数化的,可以根据文件中 SQL 语句的数量进行调整。

总结
比较了 MyBatis 和 Spring JDBC,我们发现 Spring JDBC 在解析 SQL 文件方面更加灵活。此外,我们还开发了一个支持批量执行 SQL 语句的自定义实用程序。