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)?

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 :


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.


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();

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

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();

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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.