Більшість драйверів JDBC забезпечують підвищення продуктивності при пакетному множинному зверненні до одного й того ж скомпільованого стейтменту. Групуючи оновлення пакетів, ти обмежуєш кількість циклічних звернень до бази даних.

Основні пакетні операції за допомогою JdbcTemplate

Ти виконуєш пакетну обробку JdbcTemplate, реалізуючи два методи спеціального інтерфейсу BatchPreparedStatementSetter і передаючи цю реалізацію як другий параметр у виклику методу batchUpdate. Метод getBatchSize можна використовувати для встановлення значень параметрів скомпільованого стейтменту. Метод setValues можна використовувати для встановлення значень параметрів підготовленої інструкції. Цей метод викликається ту кількість разів, яка буде вказана у виклику getBatchSize. У наступному прикладі таблиця t_actor оновлюється на основі записів у списку, причому як пакет використовується весь список:

Java

public class JdbcActorDao implements ActorDao {
    private JdbcTemplate jdbcTemplate;
    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }
    public int[] batchUpdate(final List<Actor> actors) {
        return this.jdbcTemplate.batchUpdate(
                "update t_actor set first_name = ?, last_name = ? where id = ?",
                new BatchPreparedStatementSetter() {
                    public void setValues(PreparedStatement ps, int i) throws SQLException {
                        Actor actor = actors.get(i);
                        ps.setString(1, actor.getFirstName());
                        ps.setString(2, actor.getLastName());
                        ps.setLong(3, actor.getId().longValue());
                    }
                    public int getBatchSize() {
                        return actors.size();
                    }
                });
    }
    // ... додаткові методи
}
Kotlin

class JdbcActorDao(dataSource: DataSource) : ActorDao {
    private val jdbcTemplate = JdbcTemplate(dataSource)
    fun batchUpdate(actors: List<Actor>): IntArray {
        return jdbcTemplate.batchUpdate(
                "update t_actor set first_name = ?, last_name = ? where id = ?",
                object: BatchPreparedStatementSetter {
                    override fun setValues(ps: PreparedStatement, i: Int) {
                        ps.setString(1, actors[i].firstName)
                        ps.setString(2, actors[i].lastName)
                        ps.setLong(3, actors[i].id)
                    }
                    override fun getBatchSize() = actors.size
                })
    }
    // ... додаткові методи
}

Якщо ти обробляєш потік оновлень або читання з файлу, у тебе вже може бути потрібний розмір пакета, але в останньому пакеті може не виявитися такої кількості записів. У цьому випадку можна використовувати інтерфейс InterruptibleBatchPreparedStatementSetter, який дозволяє перервати пакет після вичерпання джерела вхідних даних. Метод isBatchExhausted дозволяє сигналізувати про завершення пакета.

Пакетні операції з використанням списку об'єктів

Шаблон JdbcTemplate та NamedParameterJdbcTemplate надають альтернативний спосіб забезпечення пакетного оновлення. Замість реалізації спеціалізованого пакетного інтерфейсу, ти надаєш усі значення параметрів у виклику у вигляді списку. Фреймворк перебирає ці значення та використовує внутрішній сетер скомпільованих стейтментів. API-інтерфейс залежить від того, чи ти використовуєш іменовані параметри. Для іменованих параметрів ти вказуєш масив SqlParameterSource по одному запису для кожного члена групи. Можна використовувати допоміжні методи SqlParameterSourceUtils.createBatch для створення цього масиву, передаючи масив об'єктів на основі біна (з використанням гетерів, що відповідають параметрам), екземплярів Map зі String-ключем (що містить відповідні параметри як значення), або комбінації і того, й іншого.

У наступному прикладі показано пакетне оновлення з використанням іменованих параметрів:

Java

public class JdbcActorDao implements ActorDao {
    private NamedParameterTemplate namedParameterJdbcTemplate;
    public void setDataSource(DataSource dataSource) {
        this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
    }
    public int[] batchUpdate(List<Actor>actors) {
        return this.namedParameterJdbcTemplate.batchUpdate(
                "update t_actor set first_name = :firstName, last_name = :lastName where id = :id",
                SqlParameterSourceUtils.createBatch(actors));
    }
    // ... додаткові методи
}
Kotlin

class JdbcActorDao(dataSource: DataSource) : ActorDao {
    private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource)
    fun batchUpdate(actors: List<Actor>): IntArray {
        return this.namedParameterJdbcTemplate.batchUpdate(
                "update t_actor set first_name = :firstName, last_name = :lastName where id = :id",
                SqlParameterSourceUtils.createBatch(actors));
    }
        // ... додаткові методи
}

Для SQL-стейтмента, що використовує класичні плейсхолдери ?, ти передаєш список, що містить масив об'єктів зі значеннями оновлення. Цей масив об'єктів повинен містити по одному елементу для кожного плейсхолдера в SQL-стейтменті, і вони повинні розташовуватися в тому ж порядку, в якому вони визначені в SQL-стейтменті. Наступний приклад аналогічний попередньому, за винятком того, що в ньому використовуються класичні для JDBC плейсхолдери ?:

Java

public class JdbcActorDao implements ActorDao {
    private JdbcTemplate jdbcTemplate;
    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }
    public int[] batchUpdate(final List<<Actor> actors) {
        List<Object[]> batch = new ArrayList<Object[]>();
        for (Actor actor : actors) {
            Object[] values = new Object[] {
                    actor.getFirstName(), actor.getLastName(), actor.getId()};
            batch.add(values);
        }
        return this.jdbcTemplate.batchUpdate(
                "update t_actor set first_name = ?, last_name = ? where id = ?",
                batch);
    }
    // ... додаткові методи
}
Kotlin

class JdbcActorDao(dataSource: DataSource) : ActorDao {
    private val jdbcTemplate = JdbcTemplate(dataSource)
    fun batchUpdate(actors: List<Actor>): IntArray {
        val batch = mutableListOf<Array<Any>>()
        for (actor in actors) {
            batch.add(arrayOf(actor.firstName, actor.lastName, actor.id))
        }
        return jdbcTemplate.batchUpdate(
                "update t_actor set first_name = ?, last_name = ? where id = ?", batch)
    }
    // ... додаткові методи
}

Усі методи пакетного оновлення, які ми описали раніше, повертають масив int, що містить кількість порушених рядків для кожного пакетного запису. Цей лічильник повідомляє драйвер JDBC. Якщо лічильник недоступний, драйвер JDBC повертає значення -2.

У такому сценарії, при автоматичному встановленні значень до базового PreparedStatement, відповідний тип JDBC для кожного значення повинен бути похідним від цього типу Java. Хоча зазвичай це добре працює, існує ймовірність виникнення проблем (наприклад, з null значеннями, що містяться в Map). У такому випадку Spring за замовчуванням викликає ParameterMetaData.getParameterType, що може бути витратним при використанні драйвера JDBC. Слід використовувати останню версію драйвера та розглянути можливість встановлення властивості spring.jdbc.getParameterType.ignore у true (як системна властивість JVM або через механізм SpringProperties), якщо ти стикаєшся з проблемою з продуктивністю (як повідомляється в Oracle 12c, JBoss і PostgreSQL).

До того ж, можна розглянути варіант явного зазначення відповідних типів JDBC або через BatchPreparedStatementSetter (як показано раніше), або через явний масив типів, що передається виклику на основі List<Object[]>, або через виклик registerSqlType для екземпляра користувача MapSqlParameterSource, або через BeanPropertySqlParameterSource, який виводить тип SQL з оголошеного в Java типу властивості навіть для порожнього значення.

Пакетні операції з використанням декількох пакетів

У попередньому прикладі пакетного оновлення йшлося про пакети, які настільки об'ємні, що потрібно розбити їх на кілька менших пакетів. Здійснити це можна за допомогою методів, згаданих раніше, виконавши кілька викликів методу batchUpdate, але тепер є і зручніший метод. Цей метод приймає, крім SQL-інструкції, Collection об'єктів, що містить параметри, кількість оновлень для кожного пакета та ParameterizedPreparedStatementSetter для встановлення значень параметрів скомпільованого стейтменту. Фреймворк перебирає надані значення та розбиває виклики оновлення на пакети зазначеного розміру.

У наступному прикладі показано пакетне оновлення, в якому використовується розмір пакета 100:

Java

public class JdbcActorDao implements ActorDao {
    private JdbcTemplate jdbcTemplate;
    public void setDataSource(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }
    public int[][] batchUpdate(final Collection<Actor> actors) {
        int[][] updateCounts = jdbcTemplate.batchUpdate(
                "update t_actor set first_name = ?, last_name = ? where id = ?",
                actors,
                100,
                (PreparedStatement ps, Actor actor) -> {
                    ps.setString(1, actor.getFirstName());
                    ps.setString(2, actor.getLastName());
                    ps.setLong(3, actor.getId().longValue());
                });
        return updateCounts;
    }
    // ... додаткові методи
}
Kotlin

class JdbcActorDao(dataSource: DataSource) : ActorDao {
    private val jdbcTemplate = JdbcTemplate(dataSource)
    fun batchUpdate(actors: List<Actor>): Array<IntArray> {
        return jdbcTemplate.batchUpdate(
                    "update t_actor set first_name = ?, last_name = ? where id = ?",
                    actors, 100) { ps, argument ->
            ps.setString(1, argument.firstName)
            ps.setString(2, argument.lastName)
            ps.setLong(3, argument.id)
        }
    }
    // ... додаткові методи
}

Метод пакетного оновлення для цього виклику повертає масив масивів int, який містить запис масиву для кожного пакета з масивом кількості зачеплених рядків для кожного оновлення. Довжина масиву верхнього рівня вказує на кількість запущених пакетів, а довжина масиву другого рівня – кількість оновлень у цьому пакеті. Кількість оновлень у кожному пакеті повинна відповідати розміру пакета, вказаному для всіх пакетів (за винятком останнього, який може бути меншим), залежно від загальної кількості зазначених об'єктів, що оновлюються. Лічильник оновлень для кожної інструкції оновлення — це лічильник, який повідомляється драйвером JDBC. Якщо лічильник недоступний, драйвер JDBC повертає значення -2.