【问题标题】:why saveAll() always inserts data instead of update it?为什么 saveAll() 总是插入数据而不是更新数据?
【发布时间】:2021-03-17 14:42:08
【问题描述】:

Spring Boot 2.4.0,DB 是 MySql 8

每 15 秒使用 REST 从远程获取数据,并使用 saveAll() 将其存储到 MySql DB。

Which call the save() method for all the given entities.

所有数据都设置了 ID。
我希望如果 DB 没有这样的 id - 它将被插入
如果此类 ID 已在 DB 中提供 - 它将被更新

这里是从控制台截取的:

Hibernate: 
    insert 
    into
        iot_entity
        (controller_ref, description, device_id, device_ref, entity_type_ref, hw_address, hw_serial, image_ref, inventory_nr, ip6address1, ip6address2, ip_address1, ip_address2, latlng, location, mac_address, name, params, status, tenant, type, id) 
    values
        (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
...
2020-12-05 23:18:28.269 ERROR 15752 --- [  restartedMain] o.h.e.jdbc.batch.internal.BatchingBatch  : HHH000315: Exception executing batch [java.sql.BatchUpdateException: Duplicate entry '1' for key 'iot_entity.PRIMARY'], SQL: insert into iot_entity (controller_ref, description, device_id, device_ref, entity_type_ref, hw_address, hw_serial, image_ref, inventory_nr, ip6address1, ip6address2, ip_address1, ip_address2, latlng, location, mac_address, name, params, status, tenant, type, id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2020-12-05 23:18:28.269  WARN 15752 --- [  restartedMain] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1062, SQLState: 23000
2020-12-05 23:18:28.269 ERROR 15752 --- [  restartedMain] o.h.engine.jdbc.spi.SqlExceptionHelper   : Duplicate entry '1' for key 'iot_entity.PRIMARY'
2020-12-05 23:18:28.269 DEBUG 15752 --- [  restartedMain] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction rollback after commit exception

org.springframework.dao.DataIntegrityViolationException: could not execute batch; SQL [insert into iot_entity (controller_ref, description, device_id, device_ref, entity_type_ref, hw_address, hw_serial, image_ref, inventory_nr, ip6address1, ip6address2, ip_address1, ip_address2, latlng, location, mac_address, name, params, status, tenant, type, id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)]; constraint [iot_entity.PRIMARY]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute batch

这里是如何获取和保存的样子:

@Override
@SneakyThrows
@Scheduled(fixedDelay = 15_000)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void fetchAndStoreData() {
    IotEntity[] entities = restTemplate.getForObject(properties.getIotEntitiesUrl(), IotEntity[].class);

    log.debug("ENTITIES:\n{}", mapper.writerWithDefaultPrettyPrinter().writeValueAsString(entities));

    if (entities != null && entities.length > 0) {
        entityRepository.saveAll(List.of(entities));
    } else {
        log.warn("NO entities data FETCHED !!!");
    }
}

此方法每 15 秒运行一次

实体:

@Data
@Entity
@NoArgsConstructor
@EqualsAndHashCode(of = {"id"})
@ToString(of = {"id", "deviceId", "entityTypeRef", "ipAddress1"})
public class IotEntity implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    private Integer id;
    // other fields

和存储库:

public interface EntityRepository extends JpaRepository<IotEntity, Integer> {
}

这里是为 iot 实体截取的 JSON 格式:

2020-12-05 23:18:44.261 DEBUG 15752 --- [pool-3-thread-1] EntityService : ENTITIES:
[ {
  "id" : 1,
  "controllerRef" : null,
  "name" : "Local Controller Unterföhring",
  "description" : "",
  "deviceId" : "",
  ...

所以ID肯定设置好了。

此外,为项目启用了批处理。应该不会对储蓄产生任何影响。

我不明白为什么它会尝试插入新实体而不是更新现有实体?
为什么不能区分新旧实体?


更新:

为实体实现持久化:

@Data
@Entity
@NoArgsConstructor
@EqualsAndHashCode(of = {"id"})
@ToString(of = {"id", "deviceId", "entityTypeRef", "ipAddress1"})
public class IotEntity implements Serializable, Persistable<Integer> {
    private static final long serialVersionUID = 1L;

    @Id
    private Integer id;

    @Override
    public boolean isNew() {
        return false;
    }

    @Override
    public Integer getId() {
        return this.id;
    }

但是,它失败并出现同样的异常 - Duplicate entry '1' for key 'iot_entity.PRIMARY'

如果我要添加@GeneratedValue,如下所示:

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;

它不会失败。但是,它会自行更新 ID 值。

例如,它使用id = 15 获取:

[ {
  "id" : 15,
  "carParkRef" : 15,
  "name" : "UF Haus 1/2",

并且应该像下面这样保存:

实际上它有id = 2

而且是不正确的。


尝试添加到存储服务:

private final EntityManager entityManager;
...
List.of(carParks).forEach(entityManager::merge);

失败并出现相同的异常(无论是否实现 Persistable)。它尝试插入值 - insert into ... Duplicate entry '15' for key '... .PRIMARY'

来自application.yml的片段:

spring:
  # ===============================
  # = DATA SOURCE
  # ===============================
  datasource:
    url: jdbc:mysql://localhost:3306/demo_db
    username: root
    password: root
    initialization-mode: always

  # ===============================
  # = JPA / HIBERNATE
  # ===============================
  jpa:
    show-sql: true
    generate-ddl: true
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        format_sql: true
        generate_statistics: true

在这里你可以看到pom file content

如何解决这个问题?

【问题讨论】:

    标签: java spring-boot hibernate spring-data-jpa save


    【解决方案1】:

    你可以试试@GeneratedValue(strategy = GenerationType.AUTO) 这对我有用。

    【讨论】:

      【解决方案2】:

      Spring Data JPA 使用@version @Id 字段的组合来决定是合并还是插入。

      • null @id 和 null @version 将意味着新记录因此插入
      • 如果存在@id,则@version 字段用于决定是合并还是插入。
      • 仅在 (update .... where id = xxx and version = 0) 时调用更新

      因为你缺少@id 和@version,它试图插入,因为底层系统决定这是新记录,当运行 sql 时你得到错误。

      【讨论】:

      • 您错过了远程数据已设置 ID。代表这个远程数据的本地实体也有@Id注解
      【解决方案3】:

      看来我找到了这种行为的根源。

      主应用启动器如下所示:

      @AllArgsConstructor
      @SpringBootApplication
      public class Application implements CommandLineRunner {
      
          private final DataService dataService;
          private final QrReaderServer qrReaderServer;
          private final MonitoringService monitoringService;
      
          @Override
          public void run(String... args) {
              dataService.fetchAndStoreData();
              monitoringService.launchMonitoring();
              qrReaderServer.launchServer();
          }
      

      所有 3 个步骤都有严格的执行顺序。如果需要,第一个必须重复以在本地更新数据。另外两个仅处理存储数据的服务器。

      第一种方法的样子:

      @Scheduled(fixedDelay = 15_000)
      public void fetchAndStoreData() {
          log.debug("START_DATA_FETCH");
      
          carParkService.fetchAndStoreData();
          entityService.fetchAndStoreData();
          assignmentService.fetchAndStoreData();
          permissionService.fetchAndStoreData();
          capacityService.fetchAndStoreData();
      
          log.debug("END_DATA_FETCH");
      }
      

      此外,此执行也是计划好的。

      当应用启动时,它尝试执行两次获取:

      2020-12-14 14:00:46.208 DEBUG 16656 --- [pool-3-thread-1] c.s.s.s.data.impl.DataServiceImpl        : START_DATA_FETCH
      2020-12-14 14:00:46.208 DEBUG 16656 --- [  restartedMain] c.s.s.s.data.impl.DataServiceImpl        : START_DATA_FETCH
      

      2 个线程同时运行并并行存储 - 尝试insert 数据。 (每次开始时都会重新创建表)。

      以后所有的提取都很好,它们只由@Sceduled线程执行。

      如果评论 @Sceduled - 它会正常工作,没有任何异常。


      解决方案:

      为服务类添加了额外的布尔属性:

      @Getter
      private static final AtomicBoolean ifDataNotFetched = new AtomicBoolean(true);
      
      @Override
      @Scheduled(fixedDelay = 15_000)
      @Order(value = Ordered.HIGHEST_PRECEDENCE)
      public void fetchAndStoreData() {
          ifDataNotFetched.set(true);
          log.debug("START_DATA_FETCH");
      
          // fetch and store data with `saveAll()`
      
          log.debug("END_DATA_FETCH");
          ifDataNotFetched.set(false);
      }
      

      并控制应用启动后的值:

      @Value("${sharepark.remote-data-fetch-timeout}")
      private int dataFetchTimeout;
      private static int fetchCounter;
      
      @Override
      public void run(String... args) {
          waitRemoteDataStoring();
          monitoringService.launchMonitoring();
          qrReaderServer.launchServer();
      }
      
      private void waitRemoteDataStoring() {
          do {
              try {
                  if (fetchCounter == dataFetchTimeout) {
                      log.warn("Data fetch timeout reached: {}", dataFetchTimeout);
                  }
      
                  Thread.sleep(1_000);
      
                  ++fetchCounter;
                  log.debug("{} Wait for data fetch one more second...", fetchCounter);
              } catch (InterruptedException e) {
                  Thread.currentThread().interrupt();
              }
          } while (DataServiceImpl.getIfDataNotFetched().get() && fetchCounter <= dataFetchTimeout);
      }
      

      【讨论】:

        【解决方案4】:

        问题很可能是,由于@Id 没有用@GeneratedValue 标记,Spring Data 假定所有传递给save()/saveAll() 的分离(瞬态)实体都应该调用EntityManager.persist()

        尝试使IotEntity 实现Persistable 并从isNew() 返回false。这将告诉 Spring Data 始终使用 EntityManager.merge() 代替,这应该具有预期的效果(即插入不存在的实体并更新现有的实体)。

        【讨论】:

        • 尝试了这种方法并相应地更新了问题。有没有可能使用@GeneratedValue?我尝试了 AUTO 和 IDENTITY - 都使用自己的标识符(从 1 开始)保存到数据库,而不是使用已经设置的 ID。
        • 对于所有输入都设置了 id 的场景 - 不。如果您依赖自动生成,因此只有 entities 表中的现有实体设置了其预先存在的 id,但原始代码应该可以开箱即用。
        • 告诉 Spring Data 实体不是新的,这有点奇怪。您能否尝试注入一个普通的EntityManager 而不是存储库并在所有实体上调用EntityManager.merge()?让我知道这是否有效
        • List.of(entities).forEach(entityManager::merge)替换entityRepository.saveAll(List.of(entities))(当然你需要将EntityManager注入到服务中)
        • 你知道到底发生了什么吗?如何解决?
        猜你喜欢
        • 1970-01-01
        • 2014-05-21
        • 2022-01-07
        • 2016-07-14
        • 1970-01-01
        • 2019-05-28
        • 1970-01-01
        • 1970-01-01
        • 2014-07-26
        相关资源
        最近更新 更多