Java:使用PreparedStatement向MySQL插入多行数据

92

我想使用Java一次性向MySQL表中插入多行数据。行数是动态的。过去,我是这样做的...

for (String element : array) {
    myStatement.setString(1, element[0]);
    myStatement.setString(2, element[1]);

    myStatement.executeUpdate();
}

我想要优化这个查询,使用MySQL支持的语法:

INSERT INTO table (col1, col2) VALUES ('val1', 'val2'), ('val1', 'val2')[, ...]

使用 PreparedStatement 时,由于我事先不知道 array 包含多少个元素,所以我不知道如何做到这一点。如果使用 PreparedStatement 不可能实现,那么我该如何做(并仍然转义数组中的值)?

7个回答

193

你可以通过使用PreparedStatement#addBatch()创建批处理,然后使用PreparedStatement#executeBatch()执行它。

以下是一个示例:

public void save(List<Entity> entities) throws SQLException {
    try (
        Connection connection = database.getConnection();
        PreparedStatement statement = connection.prepareStatement(SQL_INSERT);
    ) {
        int i = 0;

        for (Entity entity : entities) {
            statement.setString(1, entity.getSomeProperty());
            // ...

            statement.addBatch();
            i++;

            if (i % 1000 == 0 || i == entities.size()) {
                statement.executeBatch(); // Execute every 1000 items.
            }
        }
    }
}

每1000个项目执行一次,因为一些JDBC驱动程序和/或数据库可能对批处理长度有限制。

另请参阅:


29
如果你在一个事务中处理插入操作,它们会更快地执行。具体来说,可以使用connection.setAutoCommit(false);connection.commit();实现。参考链接:http://download.oracle.com/javase/tutorial/jdbc/basics/transactions.html。 - Joshua Martell
1
看起来如果有999个项目,您可以执行一个空批处理。 - djechlin
2
@electricalbah,它将正常执行,因为 i == entities.size() - Yohanim
1
@AndréPaulo:只需要任何适用于预处理语句的SQL INSERT。请参考JDBC教程链接获取基本示例。这与具体问题无关。 - BalusC
对于其他人来说,如果需要添加对其他数据库类型的支持,检查 connection.getMetaData().supportsBatchUpdates(); 可能会很有用。 - Ben
显示剩余2条评论

34
当使用MySQL驱动程序时,您必须将连接参数rewriteBatchedStatements设置为true ( jdbc:mysql://localhost:3306/TestDB?**rewriteBatchedStatements=true**)
进行此参数设置后,仅在表被锁定一次且索引仅更新一次时,语句才会重写为批量插入。因此速度更快。
如果没有进行该参数设置,则唯一的优点是源代码更加清晰。

这是一条针对性能的注释,用于构建:statement.addBatch(); 如果(i + 1) % 1000 == 0,那么执行:statement.executeBatch(); // 每1000个项目执行一次。 - MichalSv
显然,MySQL驱动程序存在一个漏洞http://bugs.mysql.com/bug.php?id=71528。这也会对ORM框架(如Hibernate)造成问题https://hibernate.atlassian.net/browse/HHH-9134。 - Shailendra
是的,现在至少对于 5.1.45 mysql 连接器版本是正确的。 - v.ladynev
<artifactId>mysql-connector-java</artifactId> <version>8.0.14</version>刚刚检查了一下,版本号是正确的8.0.14。如果不添加rewriteBatchedStatements=true,就无法获得性能提升。 - vincent mathew

9

如果您可以动态地创建SQL语句,您可以执行以下解决方案:

String myArray[][] = { { "1-1", "1-2" }, { "2-1", "2-2" }, { "3-1", "3-2" } };

StringBuffer mySql = new StringBuffer("insert into MyTable (col1, col2) values (?, ?)");

for (int i = 0; i < myArray.length - 1; i++) {
    mySql.append(", (?, ?)");
}

myStatement = myConnection.prepareStatement(mySql.toString());

for (int i = 0; i < myArray.length; i++) {
    myStatement.setString(i, myArray[i][1]);
    myStatement.setString(i, myArray[i][2]);
}
myStatement.executeUpdate();

2
我相信被采纳的答案更好!!当我开始写这个答案时,我不知道批量更新,而那个答案还没有提交!! :) - Ali Shakiba
这种方法比已接受的方法快得多。我测试了它,但不知道为什么。 @JohnS 你知道为什么吗? - julian0zzx
1
@julian0zzx 不是,但可能是因为它被执行为单个 SQL 而不是多个。但我不确定。 - Ali Shakiba

3
如果您的表中有自增字段并且需要访问它,则可以使用以下方法...在使用之前进行测试,因为Statement中的getGeneratedKeys()取决于所使用的驱动程序。以下代码已在Maria DB 10.0.12和Maria JDBC驱动程序1.2上进行了测试。
请记住,增加批处理大小仅在一定程度上提高性能...对于我的设置,将批处理大小增加到500以上实际上会降低性能。
public Connection getConnection(boolean autoCommit) throws SQLException {
    Connection conn = dataSource.getConnection();
    conn.setAutoCommit(autoCommit);
    return conn;
}

private void testBatchInsert(int count, int maxBatchSize) {
    String querySql = "insert into batch_test(keyword) values(?)";
    try {
        Connection connection = getConnection(false);
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        boolean success = true;
        int[] executeResult = null;
        try {
            pstmt = connection.prepareStatement(querySql, Statement.RETURN_GENERATED_KEYS);
            for (int i = 0; i < count; i++) {
                pstmt.setString(1, UUID.randomUUID().toString());
                pstmt.addBatch();
                if ((i + 1) % maxBatchSize == 0 || (i + 1) == count) {
                    executeResult = pstmt.executeBatch();
                }
            }
            ResultSet ids = pstmt.getGeneratedKeys();
            for (int i = 0; i < executeResult.length; i++) {
                ids.next();
                if (executeResult[i] == 1) {
                    System.out.println("Execute Result: " + i + ", Update Count: " + executeResult[i] + ", id: "
                            + ids.getLong(1));
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            success = false;
        } finally {
            if (rs != null) {
                rs.close();
            }
            if (pstmt != null) {
                pstmt.close();
            }
            if (connection != null) {
                if (success) {
                    connection.commit();
                } else {
                    connection.rollback();
                }
                connection.close();
            }
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

3
@Ali Shakiba,您的代码需要进行一些修改。错误部分:
for (int i = 0; i < myArray.length; i++) {
     myStatement.setString(i, myArray[i][1]);
     myStatement.setString(i, myArray[i][2]);
}

更新后的代码:

String myArray[][] = {
    {"1-1", "1-2"},
    {"2-1", "2-2"},
    {"3-1", "3-2"}
};

StringBuffer mySql = new StringBuffer("insert into MyTable (col1, col2) values (?, ?)");

for (int i = 0; i < myArray.length - 1; i++) {
    mySql.append(", (?, ?)");
}

mysql.append(";"); //also add the terminator at the end of sql statement
myStatement = myConnection.prepareStatement(mySql.toString());

for (int i = 0; i < myArray.length; i++) {
    myStatement.setString((2 * i) + 1, myArray[i][1]);
    myStatement.setString((2 * i) + 2, myArray[i][2]);
}

myStatement.executeUpdate();

这是一种更快、更好的方法,整个答案都应该被接受。 - Shankar Guru
2
如已接受的答案中所述,某些JDBC驱动程序/数据库对可以包含在INSERT语句中的行数有限制。在上面的示例中,如果'myArray'的长度大于该限制,您将遇到异常。在我的情况下,我有一个1,000行的限制,这引发了批处理执行的需要,因为我可能会在任何给定的运行中更新超过1,000行。如果您知道要插入的行数少于允许的最大值,则此类型的语句理论上应该正常工作。请记住这一点。 - Danny Bullis
澄清一下,上面的答案提到了JDBC驱动程序/数据库在批处理长度方面的限制,但在插入语句中包含的行数也可能存在限制,这是我在我的情况中看到的。 - Danny Bullis

0

JDBC 中可以提交多个更新。

我们可以使用 StatementPreparedStatementCallableStatement 对象进行批量更新,同时禁用自动提交。

所有语句对象都提供了 addBatch()executeBatch() 函数,以实现批量更新。

其中,addBatch() 方法将一组语句或参数添加到当前批处理中。


0

如果您需要将数组传递给PreparedStatement,这可能会对您有所帮助。

将所需的值存储到数组中,并将其传递给一个函数以插入相同的值。

String sql= "INSERT INTO table (col1,col2)  VALUES (?,?)";
String array[][] = new String [10][2];
for(int i=0;i<array.size();i++){
     //Assigning the values in individual rows.
     array[i][0] = "sampleData1";   
     array[i][1] = "sampleData2";
}
try{
     DBConnectionPrepared dbcp = new DBConnectionPrepared();            
     if(dbcp.putBatchData(sqlSaveAlias,array)==1){
        System.out.println("Success"); 
     }else{
        System.out.println("Failed");
     }
}catch(Exception e){
     e.printStackTrace();
}

putBatchData(sql,2D_Array)

public int[] putBatchData(String sql,String args[][]){
        int status[];
        try {
            PreparedStatement stmt=con.prepareStatement(sql);
            for(int i=0;i<args.length;i++){
                for(int j=0;j<args[i].length;j++){
                    stmt.setString(j+1, args[i][j]);
                }            
                stmt.addBatch();
                stmt.executeBatch();
                stmt.clearParameters();
            }
            status= stmt.executeBatch();             
        } catch (Exception e) {
            e.printStackTrace();
        } 
        return status;
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接