【问题标题】:How to do bulk (multi row) inserts with JpaRepository?如何使用 JpaRepository 进行批量(多行)插入?
【发布时间】:2018-11-19 04:46:21
【问题描述】:

当使用长 List<Entity> 从服务层调用我的 JpaRepositorysaveAll 方法时,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

触发数据库插入:

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

【问题讨论】:

  • 请查看我的回答,希望对您有所帮助:stackoverflow.com/a/50694902/5380322
  • @Cepr0 谢谢,但我已经这样做了(累积在一个列表中并调用saveAll。我刚刚添加了一个最小的代码示例来重现该问题。
  • 你设置hibernate.jdbc.batch_size属性了吗?
  • @Cepr0 是的。 (见上文)
  • 打错了,一定是这种形式:spring.jpa.properties.hibernate.jdbc.batch_size

标签: hibernate spring-boot kotlin spring-data-jpa cockroachdb


【解决方案1】:

要使用 Sring Boot 和 Spring Data JPA 进行批量插入,您只需要两件事:

  1. 将选项 spring.jpa.properties.hibernate.jdbc.batch_size 设置为您需要的适当值(例如:20)。

  2. 将您的 repo 的 saveAll() 方法与准备插入的实体列表一起使用。

工作示例是here

关于插入语句的转换成这样的:

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

PostgreSQL 中提供了这样的功能:您可以在 jdbc 连接字符串中将选项 reWriteBatchedInserts 设置为 true:

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

然后 jdbc 驱动会做this transformation

有关批处理的更多信息,您可以找到here

更新

Kotlin 中的演示项目:sb-kotlin-batch-insert-demo

更新

Hibernate disables insert batching at the JDBC level transparently if you use an IDENTITY identifier generator.

【讨论】:

  • 谢谢。我正在尝试让您的 Kotlin 演示运行,但尚未成功。我做了git clone https://github.com/Cepr0/sb-kotlin-batch-insert-democd sb-kotlin-batch-insert-demomvn package,但最终出现以下错误:gist.github.com/Dobiasd/7f1163110b52876f171d43e17af0853c
  • @Cepr0,我刚刚用 mySql db 尝试了你的程序,但它没有按预期工作。跟司机有什么关系。这是我正在使用的属性,``` spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect ` `
  • @ShaunakPatel 究竟什么不起作用,在哪个程序中,java 或 kotlin?
  • @Cepr0 在 Java 中。与您的程序相比,我只看到一个差异。 1)数据库(我正在使用MySQL)。意思是,我正在针对 MySQL 运行您的代码
  • 有没有一种方法可以拦截或监听 saveAll(List..) 方法的列表? ——
【解决方案2】:

根本问题是 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() 调用来强制持久性将记录视为新记录

@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;
    }
}

以上代码基于以下链接:

【讨论】:

  • 感谢 Jean 分享有用的链接。但是使用Persistable 方法保留@Generated @Id 值仍然存在问题。仅当我按照自己的逻辑手动设置 id 字段时,才会执行批处理。如果我的 Long id 属性依赖于 @Generated,则语句不会批量运行。您分享的所有链接都没有使用@Generated 类型策略和Persistable 方法。我什至检查了第二个链接中提供的 Github 代码链接,但它也手动分配了 id 属性。
  • 我认为这个回复并没有被真正理解(并且足够感激)。我自己发现了与 saveAll 相同的问题。所以重新表述这个问题:如果你有批处理,你的实体不使用生成的 ID,并且你使用 SimpleJpaRepository 和 saveAll,那么: 1.saveAll 将在循环中使用 save 2. save 将调用 entityInformation.isNew(entity) 获得响应每次通话都是假的。 3.将为每个实体调用合并。 4. IIUC 这些合并调用先选择,并且无法批处理,因此由于 saveAll 实现不正确,这将产生 N+1 问题。
  • 用spring和JPA进行批处理medium.com/@clydecroix/…
【解决方案3】:

您可以配置 Hibernate 以执行批量 DML。看看Spring Data JPA - concurrent Bulk inserts/updates。我认为答案的第 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
....

【讨论】:

  • 感谢您的建议。我试过了,但没有用。我在我的问题中添加了一个最小的代码示例来重现问题,即使使用您的设置也是如此。
  • 谢谢,我调整了我的配置(并相应地更新了我的问题),但仍然没有运气。
  • 您是否尝试过使用不同的数据库或者您的 H2 是必需的? @TobiasHermann 我建议接下来尝试使用 MySQL 数据库。并非所有数据库驱动都正确实现了 JDBC 批量插入/更新
  • 我尝试使用 CockroachDB 2.0.2。它支持多行插入,当我在我的应用程序中手动创建所需的java.sql.PreparedStatement 并使用javax.sql.DataSource 的原始java.sql.Connection 将其发送出去时,它的速度提高了大约10 倍。
  • 这是什么意思 spring.jpa.properties.hibernate.order_inserts?
【解决方案4】:

所有提到的方法都有效,但会很慢,特别是如果插入数据的源位于其他表中。首先,即使使用batch_size&gt;1,插入操作也会在多个 SQL 查询中执行。其次,如果源数据位于另一个表中,您需要使用其他查询来获取数据(并且在最坏的情况下将所有数据加载到内存中),并将其转换为静态批量插入。第三,为每个实体单独调用persist()(即使启用了批处理),您将使用所有这些实体实例使实体管理器一级缓存膨胀。

但是 Hibernate 还有另一个选择。如果您使用 Hibernate 作为 JPA 提供程序,您可以回退到 HQL,它本机 supports bulk inserts 从另一个表中进行子选择。例子:

Session session = entityManager.unwrap(Session::class.java)
session.createQuery("insert into Entity (field1, field2) select [...] from [...]")
  .executeUpdate();

这是否可行取决于您的 ID 生成策略。如果Entity.id是数据库生成的(例如MySQL自增),则执行成功。如果Entity.id 是由您的代码生成的(对于 UUID 生成器尤其如此),它将失败并出现“不支持的 id 生成方法”异常。

但是,在后一种情况下,这个问题可以通过自定义 SQL 函数来解决。例如,在 PostgreSQL 中,我使用了 uuid-ossp 扩展,它提供了 uuid_generate_v4() 函数,我最终在我的自定义对话框中注册了它:

import org.hibernate.dialect.PostgreSQL10Dialect;
import org.hibernate.dialect.function.StandardSQLFunction;
import org.hibernate.type.PostgresUUIDType;

public class MyPostgresDialect extends PostgreSQL10Dialect {

    public MyPostgresDialect() {
        registerFunction( "uuid_generate_v4", 
            new StandardSQLFunction("uuid_generate_v4", PostgresUUIDType.INSTANCE));
    }
}

然后我将这个类注册为一个休眠对话框:

hibernate.dialect=MyPostgresDialect

终于可以在批量插入查询中使用这个功能了:

SessionImpl session = entityManager.unwrap(Session::class.java);
session.createQuery("insert into Entity (id, field1, field2) "+
  "select uuid_generate_v4(), [...] from [...]")
  .executeUpdate();

最重要的是Hibernate生成的底层SQL来完成这个操作,它只是一个单一的查询:

insert into entity ( id, [...] ) select uuid_generate_v4(), [...] from [...]

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-06-10
    • 1970-01-01
    • 2017-07-17
    • 1970-01-01
    • 1970-01-01
    • 2022-01-01
    • 2015-03-01
    • 2014-10-29
    相关资源
    最近更新 更多