Большинство драйверов 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.