Java: inserte varias filas en MySQL con PreparedStatement

88

Quiero insertar varias filas en una tabla MySQL a la vez usando Java. El número de filas es dinámico. En el pasado estaba haciendo ...

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

    myStatement.executeUpdate();
}

Me gustaría optimizar esto para usar la sintaxis compatible con MySQL:

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

pero con un PreparedStatementno conozco ninguna forma de hacer esto ya que no sé de antemano cuántos elementos arraycontendrán. Si no es posible con a PreparedStatement, ¿de qué otra manera puedo hacerlo (y aún así escapar de los valores en la matriz)?

Tom Marthenal
fuente

Respuestas:

177

Puede crear un lote PreparedStatement#addBatch()y ejecutarlo PreparedStatement#executeBatch().

Aquí hay un ejemplo de inicio:

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.
            }
        }
    }
}

Se ejecuta cada 1000 elementos porque algunos controladores JDBC y / o DB pueden tener una limitación en la longitud del lote.

Ver también :

BalusC
fuente
26
Sus inserciones irán más rápido si las coloca en transacciones ... es decir, envuelva connection.setAutoCommit(false);y connection.commit(); descargue.oracle.com/javase/tutorial/jdbc/basics/…
Joshua Martell
1
Parece que puede ejecutar un lote vacío si hay 999 artículos.
djechlin
2
@electricalbah se ejecutará normalmente porquei == entities.size()
Yohanes AI
Aquí hay otro buen recurso sobre cómo armar trabajos por lotes utilizando declaraciones preparadas. viralpatel.net/blogs/batch-insert-in-java-jdbc
Danny Bullis
1
@ AndréPaulo: Cualquier INSERT de SQL adecuado para una declaración preparada. Consulte los enlaces de tutoriales de JDBC para ver ejemplos básicos. Esto no está relacionado con la cuestión concreta.
BalusC
30

Cuando se usa el controlador MySQL, debe establecer el parámetro de conexión rewriteBatchedStatementsen verdadero ( jdbc:mysql://localhost:3306/TestDB?**rewriteBatchedStatements=true**).

Con este parámetro, la declaración se reescribe para inserción masiva cuando la tabla está bloqueada solo una vez y los índices se actualizan solo una vez. Entonces es mucho más rápido.

Sin este parámetro, la única ventaja es un código fuente más limpio.

MichalSv
fuente
este es un comentario de rendimiento para la construcción: statement.addBatch (); if ((i + 1)% 1000 == 0) {statement.executeBatch (); // Ejecuta cada 1000 elementos. }
MichalSv
Aparentemente, el controlador MySQL tiene un error bugs.mysql.com/bug.php?id=71528 Esto también causa problemas para marcos ORM como Hibernate hibernate.atlassian.net/browse/HHH-9134
Shailendra
Si. Esto también es correcto por ahora. Al menos para la 5.1.45versión del conector mysql.
v.ladynev
<artifactId> mysql-connector-java </artifactId> <version> 8.0.14 </version> Recién comprobado que es correcto de 8.0.14. Sin agregar, rewriteBatchedStatements=trueno hay ganancia de rendimiento.
Vincent Mathew
7

Si puede crear su declaración SQL dinámicamente, puede hacer la siguiente solución:

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();
Ali Shakiba
fuente
¡Creo que la respuesta aceptada es mucho mejor! No sabía acerca de las actualizaciones por lotes y cuando comencé a escribir esta respuesta, ¡esa respuesta aún no se envió! :)
Ali Shakiba
Este enfoque es mucho más rápido que el aceptado. Lo pruebo, pero no encuentro por qué. @JohnS, ¿sabes por qué?
julian0zzx
@ julian0zzx no, pero tal vez porque se ejecuta como un solo sql en lugar de múltiples. pero no estoy seguro.
Ali Shakiba
3

En caso de que tenga un incremento automático en la tabla y necesite acceder a él ... puede usar el siguiente enfoque ... Haga la prueba antes de usar porque getGeneratedKeys () en Statement porque depende del controlador utilizado. El siguiente código se prueba en Maria DB 10.0.12 y Maria JDBC driver 1.2

Recuerde que aumentar el tamaño del lote mejora el rendimiento solo hasta cierto punto ... para mi configuración, aumentar el tamaño del lote por encima de 500 en realidad degradaba el rendimiento.

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();
    }
}
gladiador
fuente
3

@Ali Shakiba, su código necesita alguna modificación. Parte de error:

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

Código actualizado:

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();
vinay
fuente
Este es un enfoque mucho más rápido y mejor en toda la respuesta. Esta debería ser la respuesta aceptada
Arun Shankar
1
Como se menciona en la respuesta aceptada, algunos controladores / bases de datos JDBC tienen límites en la cantidad de filas que puede incluir en una declaración INSERT. En el caso del ejemplo anterior, si myArraytiene una longitud mayor que ese límite, obtendrá una excepción. En mi caso, tengo un límite de 1,000 filas que provoca la necesidad de una ejecución por lotes, porque podría estar actualizando potencialmente más de 1,000 filas en cualquier ejecución dada. En teoría, este tipo de declaración debería funcionar bien si sabe que está insertando menos del máximo permitido. Algo para tener en cuenta.
Danny Bullis
Para aclarar, la respuesta anterior menciona las limitaciones del controlador / base de datos JDBC en la longitud del lote, pero también puede haber límites en el número de filas incluidas en una declaración de inserción, como he visto en mi caso.
Danny Bullis
0

podemos enviar varias actualizaciones juntas en JDBC para enviar actualizaciones por lotes.

podemos usar los objetos Statement, PreparedStatement y CallableStatement para la actualización básica con deshabilitar el compromiso automático

Las funciones addBatch () y executeBatch () están disponibles con todos los objetos de declaración para tener BatchUpdate

aquí el método addBatch () agrega un conjunto de declaraciones o parámetros al lote actual.

kapil das
fuente