相关文章推荐
想发财的炒粉  ·  conversion from ...·  3 月前    · 
千杯不醉的回锅肉  ·  Java ...·  1 年前    · 

​ 本文将研究如何使用Hibernate/JPA进行批量插入或更新实体。批量处理使我们可以在单个网络调用中向数据库发送一组SQL语句。这样,可以优化应用程序的网络和内存使用率。

1、创建实体

​ 首先,创建一个 School 实体:

@Entity
@Data
public class School {
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;
    private String name;
    @OneToMany(mappedBy = "school")
    private List<Student> students;

每所school至少有零个student:

@Entity
@Data
public class Student {
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;
    private String name;
    @ManyToOne
    private School school;

2、跟踪SQL查询

​ 在运行示例时,我们需要验证插入/更新语句确实是批量发送的。无奈的是,我们无法从Hibernate日志语句中了解SQL语句是否已批处理。因此,我们将使用数据源代理来跟踪Hibernate / JPA SQL语句:

private static class ProxyDataSourceInterceptor implements MethodInterceptor {
    private final DataSource dataSource;
    public ProxyDataSourceInterceptor(final DataSource dataSource) {
        this.dataSource = ProxyDataSourceBuilder.create(dataSource)
            .name("Batch-Insert-Logger")
            .asJson().countQuery().logQueryToSysOut().build();
    // Other methods...

3、默认行为

​ Hibernate默认情况下不启用批处理。这意味着它将为每个插入/更新操作发送单独的SQL语句:

@Transactional
@Test
public void whenNotConfigured_ThenSendsInsertsSeparately() {
    for (int i = 0; i < 10; i++) {
        School school = createSchool(i);
        entityManager.persist(school);
    entityManager.flush();

​ 在这里,persist了10个School实体。如果查看查询日志,可以看到Hibernate分别发送每个插入语句:

"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School1","1"]]
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School2","2"]]
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School3","3"]]
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School4","4"]]
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School5","5"]]
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School6","6"]]
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School7","7"]]
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School8","8"]]
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School9","9"]]
"querySize":1, "batchSize":0, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School10","10"]]

​ 因此,我们应该配置Hibernate以启用批处理。为此,我们应该将hibernate.jdbc.batch_size属性设置为大于0的数字。如果我们手动创建EntityManager,则应将hibernate.jdbc.batch_size添加到Hibernate属性中:

public Properties hibernateProperties() {
    Properties properties = new Properties();
    properties.put("hibernate.jdbc.batch_size", "5");
    // Other properties...
    return properties;

​ 如果使用的是Spring Boot,则可以将其定义为应用程序属性:

spring.jpa.properties.hibernate.jdbc.batch_size=5

4、批量插入单个表

4.1、批量插入,无显式刷新

​ 首先,看一下在仅处理一种实体类型时如何使用批处理插入。使用先前的代码示例,但是这次启用了批处理:

@Transactional
@Test
public void whenInsertingSingleTypeOfEntity_thenCreatesSingleBatch() {
    for (int i = 0; i < 10; i++) {
        School school = createSchool(i);
        entityManager.persist(school);

​ 在这里,persist了10个School实体。当查看日志时,我们可以验证Hibernate是否批量发送insert语句:

"batch":true, "querySize":1, "batchSize":5, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School1","1"],["School2","2"],["School3","3"],["School4","4"],["School5","5"]]
"batch":true, "querySize":1, "batchSize":5, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School6","6"],["School7","7"],["School8","8"],["School9","9"],["School10","10"]]

​ 这里要提到的重要一件事是内存消耗。当我们持久化一个实体时,Hibernate将其存储在持久化上下文中。例如,如果我们在一个事务中保留100,000个实体,则最终将在内存中拥有100,000个实体实例,可能会导致OutOfMemoryException

4.2、批量插入与显式刷新

​ 现在,我们将研究如何在批处理操作期间优化内存使用。让我们深入研究持久性上下文的作用。

​ 首先,持久性上下文将新创建的实体以及修改后的实体存储在内存中。同步事务后,Hibernate将这些更改发送到数据库。这通常发生在交易结束时。但是,调用EntityManager.flush()也会触发事务同步

​ 其次,持久性上下文用作实体缓存,因此也称为第一级缓存。要在持久性上下文中清除实体,我们可以调用EntityManager.clear()。

​ 因此,为了减少批处理期间的内存负载,只要达到批处理大小,我们就可以在应用程序代码上调用EntityManager.flush()EntityManager.clear()

@Transactional
@Test
public void whenFlushingAfterBatch_ThenClearsMemory() {
    for (int i = 0; i < 10; i++) {
        if (i > 0 && i % BATCH_SIZE == 0) {
            entityManager.flush();
            entityManager.clear();
        School school = createSchool(i);
        entityManager.persist(school);

​ 在这里,我们在持久性上下文中刷新实体,从而使Hibernate将查询发送到数据库。此外,通过清除持久性上下文,我们从内存中删除了School实体。批处理行为将保持不变

5、批量插入多个表

​ 现在让我们看看在一个事务中处理多种实体类型时如何配置批处理插入。

​ 当我们要保留几种类型的实体时,Hibernate为每种实体类型创建一个不同的批处理。这是因为在同一批中只能有一种类型的实体

​ 此外,由于Hibernate收集插入语句,因此每当遇到与当前批处理中不同的实体类型时,它将创建一个新批处理。即使已经有该实体类型的批次,也是如此:

@Transactional
@Test
public void whenThereAreMultipleEntities_ThenCreatesNewBatch() {
    for (int i = 0; i < 10; i++) {
        if (i > 0 && i % BATCH_SIZE == 0) {
            entityManager.flush();
            entityManager.clear();
        School school = createSchool(i);
        entityManager.persist(school);
        Student firstStudent = createStudent(school);
        Student secondStudent = createStudent(school);
        entityManager.persist(firstStudent);
        entityManager.persist(secondStudent);

​ 在这里,我们要插入School并将其分配给两个*Student,*然后重复此过程10次。

​ 在日志中,我们看到Hibernate 以几批大小为1的方式发送School插入语句,而我们原本只希望收到2批大小为5的数据。此外,Student插入语句也以几批大小为2的方式发送,而不是4批大小为5的方式发送。 :

"batch":true, "querySize":1, "batchSize":1, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School1","1"]]
"batch":true, "querySize":1, "batchSize":2, "query":["insert into student (name, school_id, id) 
  values (?, ?, ?)"], "params":[["Student-School1","1","2"],["Student-School1","1","3"]]
"batch":true, "querySize":1, "batchSize":1, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School2","4"]]
"batch":true, "querySize":1, "batchSize":2, "query":["insert into student (name, school_id, id) 
  values (?, ?, ?)"], "params":[["Student-School2","4","5"],["Student-School2","4","6"]]
"batch":true, "querySize":1, "batchSize":1, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School3","7"]]
"batch":true, "querySize":1, "batchSize":2, "query":["insert into student (name, school_id, id) 
  values (?, ?, ?)"], "params":[["Student-School3","7","8"],["Student-School3","7","9"]]
Other log lines...

​ 要批处理具有相同实体类型的所有插入语句,我们应该配置hibernate.order_inserts属性

我们可以使用EntityManagerFactory手动配置Hibernate属性:

public Properties hibernateProperties() {
    Properties properties = new Properties();
    properties.put("hibernate.order_inserts", "true");
    // Other properties...
    return properties;

​ 如果使用的是Spring Boot,则可以在application.properties中配置属性:

spring.jpa.properties.hibernate.order_inserts=true

​ 添加此属性后,我们将有1批用于School插入和2批针对Student插入:

"batch":true, "querySize":1, "batchSize":5, "query":["insert into school (name, id) values (?, ?)"], 
  "params":[["School6","16"],["School7","19"],["School8","22"],["School9","25"],["School10","28"]]
"batch":true, "querySize":1, "batchSize":5, "query":["insert into student (name, school_id, id) 
  values (?, ?, ?)"], "params":[["Student-School6","16","17"],["Student-School6","16","18"],
  ["Student-School7","19","20"],["Student-School7","19","21"],["Student-School8","22","23"]]
"batch":true, "querySize":1, "batchSize":5, "query":["insert into student (name, school_id, id) 
  values (?, ?, ?)"], "params":[["Student-School8","22","24"],["Student-School9","25","26"],
  ["Student-School9","25","27"],["Student-School10","28","29"],["Student-School10","28","30"]]

6、批量更新

​ 现在,让我们继续进行批处理更新。与批处理插入类似,我们可以对多个更新语句进行分组,然后一次性将它们发送到数据库。

​ 为此,我们将配置hibernate.order_updates和hibernate.jdbc.batch_versioned_data属性。如果我们手动创建EntityManagerFactory,则可以通过编程方式设置属性:

public Properties hibernateProperties() {
    Properties properties = new Properties();
    properties.put("hibernate.order_updates", "true");
    properties.put("hibernate.batch_versioned_data", "true");
    // Other properties...
    return properties;

​ 如果使用Spring Boot,则将它们添加到application.properties中:

spring.jpa.properties.hibernate.order_updates=true
spring.jpa.properties.hibernate.batch_versioned_data=true

​ 配置完这些属性后,Hibernate应该将更新语句分批分组:

@Transactional
@Test
public void whenUpdatingEntities_thenCreatesBatch() {
    TypedQuery<School> schoolQuery = 
      entityManager.createQuery("SELECT s from School s", School.class);
    List<School> allSchools = schoolQuery.getResultList();
    for (School school : allSchools) {
        school.setName("Updated_" + school.getName());

​ 在这里,我们更新了学校实体,并且Hibernate分2批发送了大小为5的SQL语句:

"batch"``:``true``, ``"querySize"``:1, ``"batchSize"``:5, ``"query"``:[``"update school set name=? where id=?"``], 
 ``"params"``:[[``"Updated_School1"``,``"1"``],[``"Updated_School2"``,``"2"``],[``"Updated_School3"``,``"3"``],
 ``[``"Updated_School4"``,``"4"``],[``"Updated_School5"``,``"5"``]]
"batch"``:``true``, ``"querySize"``:1, ``"batchSize"``:5, ``"query"``:[``"update school set name=? where id=?"``], 
 ``"params"``:[[``"Updated_School6"``,``"6"``],[``"Updated_School7"``,``"7"``],[``"Updated_School8"``,``"8"``],
 ``[``"Updated_School9"``,``"9"``],[``"Updated_School10"``,``"10"``]]

7、@Id生成策略

当我们想使用批处理进行插入/更新时,我们应该了解主键生成策略。如果我们的实体使用GenerationType.IDENTITY标识符生成器,则Hibernate将静默禁用批处理插入/更新

由于示例中的实体使用GenerationType.SEQUENCE标识符生成器,因此Hibernate启用了批处理操作:

@GeneratedValue (strategy = GenerationType.SEQUENCE) private long id; ​ 本文将研究如何使用Hibernate/JPA进行批量插入或更新实体。批量处理使我们可以在单个网络调用中向数据库发送一组SQL语句。这样,可以优化应用程序的网络和内存使用率。1、创建实体​ 首先,创建一个School实体:@Entity@Datapublic class School { @Id @GeneratedValue(strategy = Generati...
关于SQL和ORM的争论,永远都不会终止,我也一直在思考这个问题。昨天又跟群里的小伙伴进行了一番讨论,感触还是有一些,于是就有了今天这篇文。 声明:本文不会下关于Mybatis和JPA两个持久层框架哪个更好这样的结论。只是摆事实,讲道理,所以,请各位看官勿喷。 一、事件起因 关于Mybatis和JPA孰优孰劣的问题,争论已经很多年了。一直也没有结论,毕竟每个人的喜好和习惯是大不相同的。我也看过知乎上一些问答,各有各的理由,感觉都挺有道理。如果让我不带感情色彩地去分辨,其实我也是懵的,因为真的是公说公有理婆说婆有理。 而在国内,不得不承认,到今年(2019年),用Mybatis的公司确实是要比用
有时候我们需要向数据库插入大量数据,如果一条一条插入会非常慢所以我们可以考虑批量插入其实很简单 只需要使用默认的save()方法就可以了假设现在有一个student实体类  我们需要一次插入整个学区5000名学生的信息package com.chunying.boke.bean; * @author chunying public class Student { pr...
Jpa提供的“批量插入”方法是 repository.saveAll(Iterable<S> iter) repository.saveAll(Iterable<S> iter) @Transactional @Override public <S extends T> List<S> saveAll(Iterable<S> entities) { Assert.notNull(entities, "Entities must not.
hibernate.order_updates: Hibernate文档中提到,该配置用于在刷新一级缓存,提交UPDATE的时候,按照每类对象的主键顺序排序后再提交,可以在高并发情况下减少事务死锁的可能。这个配置默认为false,但是非常建议在可能存在高并发情况下开启,因为其实按照类型ID排序(在内存中),并不会消耗过多性能。那么这个配置到底什么含义呢?做个简单的测试。 首先准备一个对象U
List<UserInfo> userInfoList = userInfoRepo.findAllByDelFlag(DelFlag.NORMAL); //TODO 站内信附属表批量添加信息 if (userInfoList.size() > 0) { StringBuilder insert = new StringBuilder("INSERT INTO `system_message_info` (`mess...
import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.Transaction; import org.hibernate.b 1、entityManager.find(Class<T> entityClass, Object primaryKey); 根据主键查找数据;如果主键格式不正确,会抛出illegalArgumentException异常;如果主键在数据库未找到数据返回null; 2、entityManager.persist(Object entity); 新增数据;如果... 老大看时间的太慢了,提醒了我一下,让我去深入看看jpa的底层 突然想到之前有点印象,在不追求性能的情况下可以使用jpa批量插入,但是需要提升性能的时候就需要自己手动操作一波 然而因为之前都是用MySQL数据库,习惯的用了MySQL的批量插入的语法 再经过一阵子的折腾后,对自己的SQL语句
@Query("select name from Reservation where beautyType = :beautyType AND time BETWEEN :startDate AND :endDate") public List findReservations(@Param("startDate")Date startDate,@Param("endDate")Date end
在Spring Boot应用程序中,您可以使用JPA(Java Persistence API)来与Oracle数据库进行交互,并使用JPA Repository批量插入数据。 以下是使用Spring Boot,Oracle和JPA Repository批量插入数据的一些步骤: 1. 首先,创建一个实体类,它表示您要插入的数据,并使用JPA注解将其映射到数据库表。 2. 然后,在您的Spring Boot应用程序中创建一个JPA Repository,该仓库将帮助您与Oracle数据库进行交互。 3. 接下来,您可以使用Spring Boot的@Transactional注解来确保您的批量插入操作是原子的,这意味着它们要么全部成功,要么全部失败。 4. 然后,使用JPA EntityManager批量插入数据。您可以使用EntityManager的persist()方法将多个实体对象添加到持久化上下文中,然后使用flush()方法将它们一次性保存到数据库中。 以下是一个示例代码段,演示如何使用JPA Repository批量插入数据: @Repository public class MyRepository { @PersistenceContext private EntityManager entityManager; @Transactional public void batchInsert(List<MyEntity> entities) { for (int i = 0; i < entities.size(); i++) { entityManager.persist(entities.get(i)); if (i % 50 == 0) { entityManager.flush(); entityManager.clear(); entityManager.flush(); entityManager.clear(); 在这个示例中,我们在JPA Repository中定义了一个名为batchInsert()的方法,该方法将一个实体对象列表作为参数,并使用EntityManager批量插入它们。我们使用了flush()方法将批量插入的实体对象保存到数据库中,并使用clear()方法清除持久化上下文。为了优化性能,我们在每插入50个实体对象时执行一次flush()和clear()操作。 这是一个基本的示例,您可以根据自己的需求进行更改和扩展。
// 使用ThreadLocal保证线程安全 private static final ThreadLocal<DataBaseType> TYPE = new ThreadLocal<DataBaseType>(); // 往当前线程里设置数据源类型 public static void setDataBaseType(DataBaseType dataBaseType) { if (dataBaseType == null) { throw new NullPointerException(); TYPE.set(dataBaseType); // 获取数据源类型 public static DataBaseType getDataBaseType() { DataBaseType dataBaseType = TYPE.get() == null ? DataBaseType.Primary : TYPE.get(); return dataBaseType; // 清空数据类型 public static void clearDataBaseType() { TYPE.remove(); [/code]
Java实现双数据源的配置 xiaobin183182: 大佬,那个DataSourceType是什么呀,有没有代码啊 Spring boot项目部署到腾讯云服务器 新马冲老龙坝: 我都是直接找端口对应进程,然后kill这个进程