Java: insira várias linhas no MySQL com PreparedStatement

87

Quero inserir várias linhas em uma tabela MySQL ao mesmo tempo usando Java. O número de linhas é dinâmico. No passado eu fazia ...

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

    myStatement.executeUpdate();
}

Eu gostaria de otimizar isso para usar a sintaxe compatível com MySQL:

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

mas com um PreparedStatementnão sei como fazer isso, pois não sei de antemão quantos elementos arrayconterá. Se não for possível com um PreparedStatement, de que outra forma posso fazer (e ainda escapar dos valores na matriz)?

Tom marthenal
fonte

Respostas:

175

Você pode criar um lote PreparedStatement#addBatch()e executá-lo por PreparedStatement#executeBatch().

Aqui está um exemplo inicial:

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

Ele é executado a cada 1000 itens porque alguns drivers JDBC e / ou DBs podem ter uma limitação no comprimento do lote.

Veja também :

BalusC
fonte
26
Suas inserções serão mais rápidas se você colocá-las em transações ... isto é, embrulhe com connection.setAutoCommit(false);e faça connection.commit(); download.oracle.com/javase/tutorial/jdbc/basics/…
Joshua Martell
1
Parece que você pode executar um lote vazio se houver 999 itens.
Djechlin
2
@electricalbah será executado normalmente porquei == entities.size()
Yohanes AI
Aqui está outro bom recurso sobre como reunir jobs em lote usando instruções preparadas. viralpatel.net/blogs/batch-insert-in-java-jdbc
Danny Bullis
1
@ AndréPaulo: Qualquer SQL INSERT adequado para uma instrução preparada. Consulte os links do tutorial JDBC para exemplos básicos. Isso não está relacionado à questão concreta.
BalusC
30

Quando o driver MySQL é usado, você deve definir o parâmetro de conexão rewriteBatchedStatementscomo verdadeiro ( jdbc:mysql://localhost:3306/TestDB?**rewriteBatchedStatements=true**).

Com este parâmetro, a instrução é reescrita para inserção em massa quando a tabela é bloqueada apenas uma vez e os índices são atualizados apenas uma vez. Portanto, é muito mais rápido.

Sem esse parâmetro, a única vantagem é o código-fonte mais limpo.

MichalSv
fonte
este é um comentário de desempenho para construção: statement.addBatch (); if ((i + 1)% 1000 == 0) {statement.executeBatch (); // Execute a cada 1000 itens. }
MichalSv
Aparentemente, o driver MySQL tem um bug bugs.mysql.com/bug.php?id=71528 Isso também causa problemas para estruturas ORM como Hibernate hibernate.atlassian.net/browse/HHH-9134
Shailendra
Sim. Isso também está correto por enquanto. Pelo menos para a 5.1.45versão do conector mysql.
v.ladynev
<artifactId> mysql-connector-java </artifactId> <version> 8.0.14 </version> Apenas verifiquei se está correto de 8.0.14. Sem adicionar rewriteBatchedStatements=truenão há ganho de desempenho.
Vincent Mathew
7

Se você pode criar sua instrução sql dinamicamente, você pode fazer a seguinte solução alternativa:

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
fonte
Acredito que a resposta aceita é muito melhor !! Eu não sabia sobre atualizações em lote e quando comecei a escrever esta resposta, essa resposta ainda não havia sido enviada !!! :)
Ali Shakiba
Essa abordagem é muito mais rápida do que a aceita. Eu testo, mas não encontro o porquê. @JohnS você sabe por quê?
julian0zzx
@ julian0zzx não, mas talvez porque é executado como sql único em vez de múltiplo. mas eu não tenho certeza.
Ali Shakiba
3

Caso você tenha incremento automático na tabela e precise acessá-la ... você pode usar a seguinte abordagem ... Faça o teste antes de usar porque getGeneratedKeys () em Statement porque depende do driver usado. O código abaixo foi testado no Maria DB 10.0.12 e no driver Maria JDBC 1.2

Lembre-se de que aumentar o tamanho do lote melhora o desempenho apenas até certo ponto ... para minha configuração, aumentar o tamanho do lote acima de 500 estava realmente degradando o desempenho.

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
fonte
3

@Ali Shakiba seu código precisa de algumas modificações. Parte do erro:

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

Código atualizado:

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
fonte
Esta é uma abordagem muito mais rápida e melhor na resposta completa. Esta deve ser a resposta aceita
Arun Shankar
1
Conforme mencionado na resposta aceita, alguns drivers / bancos de dados JDBC têm limites no número de linhas que você pode incluir em uma instrução INSERT. No caso do exemplo acima, se myArraytiver um comprimento maior do que esse limite, você encontrará uma exceção. No meu caso, tenho um limite de 1.000 linhas, o que provoca a necessidade de uma execução em lote, porque poderia estar atualizando potencialmente mais de 1.000 linhas em qualquer execução. Teoricamente, esse tipo de instrução deve funcionar bem se você souber que está inserindo menos do que o máximo permitido. Algo para se manter em mente.
Danny Bullis
Para esclarecer, a resposta acima menciona as limitações do driver / banco de dados JDBC no comprimento do lote, mas também pode haver limites no número de linhas incluídas em uma instrução de inserção, como vi no meu caso.
Danny Bullis
0

podemos enviar várias atualizações juntos em JDBC para enviar atualizações em lote.

podemos usar objetos Statement, PreparedStatement e CallableStatement para atualização de bacth com desabilitar autocommit

addBatch () e executeBatch () funções estão disponíveis com todos os objetos de instrução para ter BatchUpdate

aqui, o método addBatch () adiciona um conjunto de instruções ou parâmetros ao lote atual.

Kapil das
fonte