Как сделать массовые (многорядные) вставки с JpaRepository? - PullRequest
0 голосов
/ 09 июня 2018

При вызове метода saveAll моего JpaRepository с длинным List<Entity> из сервисного уровня в журнале трассировки Hibernate отображаются отдельные операторы SQL, выданные для объекта.

Можно ли принудительно заставить еговыполнить массовую вставку (т. е. многострочно) без необходимости вручную манипулировать с EntityManger, транзакциями и т. д. или даже необработанными строками операторов SQL?

С многострочной вставкой я имею в виду не просто переход от:

start transaction
INSERT INTO table VALUES (1, 2)
end transaction
start transaction
INSERT INTO table VALUES (3, 4)
end transaction
start transaction
INSERT INTO table VALUES (5, 6)
end transaction

до:

start transaction
INSERT INTO table VALUES (1, 2)
INSERT INTO table VALUES (3, 4)
INSERT INTO table VALUES (5, 6)
end transaction

но вместо:

start transaction
INSERT INTO table VALUES (1, 2), (3, 4), (5, 6)
end transaction

В PROD я использую CockroachDB, и разница в производительности значительна.

Ниже приведен минимальный пример, который воспроизводит проблему (H2 для простоты).


./src/main/kotlin/ThingService.kt:

package things

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.data.jpa.repository.JpaRepository
import javax.persistence.Entity
import javax.persistence.Id
import javax.persistence.GeneratedValue

interface ThingRepository : JpaRepository<Thing, Long> {
}

@RestController
class ThingController(private val repository: ThingRepository) {
    @GetMapping("/test_trigger")
    fun trigger() {
        val things: MutableList<Thing> = mutableListOf()
        for (i in 3000..3013) {
            things.add(Thing(i))
        }
        repository.saveAll(things)
    }
}

@Entity
data class Thing (
    var value: Int,
    @Id
    @GeneratedValue
    var id: Long = -1
)

@SpringBootApplication
class Application {
}

fun main(args: Array<String>) {
    runApplication<Application>(*args)
}

./src/main/resources/application.properties:

jdbc.driverClassName = org.h2.Driver
jdbc.url = jdbc:h2:mem:db
jdbc.username = sa
jdbc.password = sa

hibernate.dialect=org.hibernate.dialect.H2Dialect
hibernate.hbm2ddl.auto=create

spring.jpa.generate-ddl = true
spring.jpa.show-sql = true

spring.jpa.properties.hibernate.jdbc.batch_size = 10
spring.jpa.properties.hibernate.order_inserts = true
spring.jpa.properties.hibernate.order_updates = true
spring.jpa.properties.hibernate.jdbc.batch_versioned_data = true

./build.gradle.kts:

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    val kotlinVersion = "1.2.30"
    id("org.springframework.boot") version "2.0.2.RELEASE"
    id("org.jetbrains.kotlin.jvm") version kotlinVersion
    id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion
    id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion
    id("io.spring.dependency-management") version "1.0.5.RELEASE"
}

version = "1.0.0-SNAPSHOT"

tasks.withType<KotlinCompile> {
    kotlinOptions {
        jvmTarget = "1.8"
        freeCompilerArgs = listOf("-Xjsr305=strict")
    }
}

repositories {
    mavenCentral()
}

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("org.springframework.boot:spring-boot-starter-data-jpa")
    compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    compile("org.jetbrains.kotlin:kotlin-reflect")
    compile("org.hibernate:hibernate-core")
    compile("com.h2database:h2")
}

Запуск:

./gradlew bootRun

Триггеры INSERT для БД:

curl http://localhost:8080/test_trigger

Журналвывод:

Hibernate: select thing0_.id as id1_0_0_, thing0_.value as value2_0_0_ from thing thing0_ where thing0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: select thing0_.id as id1_0_0_, thing0_.value as value2_0_0_ from thing thing0_ where thing0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: select thing0_.id as id1_0_0_, thing0_.value as value2_0_0_ from thing thing0_ where thing0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: select thing0_.id as id1_0_0_, thing0_.value as value2_0_0_ from thing thing0_ where thing0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: select thing0_.id as id1_0_0_, thing0_.value as value2_0_0_ from thing thing0_ where thing0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: select thing0_.id as id1_0_0_, thing0_.value as value2_0_0_ from thing thing0_ where thing0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: select thing0_.id as id1_0_0_, thing0_.value as value2_0_0_ from thing thing0_ where thing0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: select thing0_.id as id1_0_0_, thing0_.value as value2_0_0_ from thing thing0_ where thing0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: select thing0_.id as id1_0_0_, thing0_.value as value2_0_0_ from thing thing0_ where thing0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: select thing0_.id as id1_0_0_, thing0_.value as value2_0_0_ from thing thing0_ where thing0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: select thing0_.id as id1_0_0_, thing0_.value as value2_0_0_ from thing thing0_ where thing0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: select thing0_.id as id1_0_0_, thing0_.value as value2_0_0_ from thing thing0_ where thing0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: select thing0_.id as id1_0_0_, thing0_.value as value2_0_0_ from thing thing0_ where thing0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: select thing0_.id as id1_0_0_, thing0_.value as value2_0_0_ from thing thing0_ where thing0_.id=?
Hibernate: call next value for hibernate_sequence
Hibernate: insert into thing (value, id) values (?, ?)
Hibernate: insert into thing (value, id) values (?, ?)
Hibernate: insert into thing (value, id) values (?, ?)
Hibernate: insert into thing (value, id) values (?, ?)
Hibernate: insert into thing (value, id) values (?, ?)
Hibernate: insert into thing (value, id) values (?, ?)
Hibernate: insert into thing (value, id) values (?, ?)
Hibernate: insert into thing (value, id) values (?, ?)
Hibernate: insert into thing (value, id) values (?, ?)
Hibernate: insert into thing (value, id) values (?, ?)
Hibernate: insert into thing (value, id) values (?, ?)
Hibernate: insert into thing (value, id) values (?, ?)
Hibernate: insert into thing (value, id) values (?, ?)
Hibernate: insert into thing (value, id) values (?, ?)

Ответы [ 3 ]

0 голосов
/ 16 июня 2018

Чтобы получить массовую вставку с помощью Sring Boot и Spring Data JPA, вам нужны только две вещи:

  1. установите для параметра spring.jpa.properties.hibernate.jdbc.batch_size подходящее значение, которое вам нужно (например: 20).

  2. используйте saveAll() метод вашего репо со списком сущностей, подготовленных для вставки.

Рабочий пример: здесь .

Что касается преобразования оператора вставки во что-то вроде этого:

INSERT INTO table VALUES (1, 2), (3, 4), (5, 6)

, то такое доступно в PostgreSQL: вы можете установить для параметра reWriteBatchedInserts значение true в соединении jdbcстрока:

jdbc:postgresql://localhost:5432/db?reWriteBatchedInserts=true

, тогда драйвер jdbc сделает это преобразование .

Дополнительную информацию о пакетировании вы можете найти здесь .

ОБНОВЛЕНО

Демонстрационный проект в Котлине: sb-kotlin-batch-insert-demo

ОБНОВЛЕНО

Hibernate отключает пакетную вставку на уровне JDBC прозрачно, если вы используете генератор идентификаторов IDENTITY.

0 голосов
/ 16 июня 2018

Основными проблемами является следующий код в SimpleJpaRepository:

@Transactional
public <S extends T> S save(S entity) {
    if (entityInformation.isNew(entity)) {
        em.persist(entity);
        return entity;
    } else {
        return em.merge(entity);
    }
}

В дополнение к настройкам свойства размера пакета необходимо убедиться, что вызовы класса SimpleJpaRepository сохраняются и не сливаются.Есть несколько подходов к решению этой проблемы: используйте генератор @Id, который не запрашивает последовательность, например

@Id
@GeneratedValue(generator = "uuid2")
@GenericGenerator(name = "uuid2", strategy = "uuid2")
var id: Long

, или заставьте постоянство обрабатывать записи как новые, заставив вашу сущность реализовать Persistable и переопределивisNew() call

@Entity
class Thing implements Pesistable<Long> {
    var value: Int,
    @Id
    @GeneratedValue
    var id: Long = -1
    @Transient
    private boolean isNew = true;
    @PostPersist
    @PostLoad
    void markNotNew() {
        this.isNew = false;
    }
    @Override
    boolean isNew() {
        return isNew;
    }
}

Или переопределить save(List) и использовать диспетчер сущностей для вызова persist()

@Repository
public class ThingRepository extends SimpleJpaRepository<Thing, Long> {
    private EntityManager entityManager;
    public ThingRepository(EntityManager entityManager) {
        super(Thing.class, entityManager);
        this.entityManager=entityManager;
    }

    @Transactional
    public List<Thing> save(List<Thing> things) {
        things.forEach(thing -> entityManager.persist(thing));
        return things;
    }
}

Приведенный выше код основан на следующих ссылках:

0 голосов
/ 09 июня 2018

Вы можете настроить Hibernate для выполнения массовых DML.Взгляните на Spring Data JPA - одновременные массовые вставки / обновления .Я думаю, что раздел 2 ответа может решить вашу проблему:

Включение пакетной обработки операторов DML. Включение поддержки пакетной обработки приведет к уменьшению количества обращений в базу данных для вставки / обновления того же числазаписи.

Цитирование из пакетных операторов INSERT и UPDATE:

hibernate.jdbc.batch_size = 50

hibernate.order_inserts = true

hibernate.order_updates =true

hibernate.jdbc.batch_versioned_data = true

ОБНОВЛЕНИЕ : Вы должны установить свойства гибернации по-разному в файле application.properties.Они находятся под пространством имен: spring.jpa.properties.*.Пример может выглядеть следующим образом:

spring.jpa.properties.hibernate.jdbc.batch_size = 50
spring.jpa.properties.hibernate.order_inserts = true
....
...