为什么saveall()总是插入数据而不是更新数据?

qxsslcnc  于 2021-07-03  发布在  Java
关注(0)|答案(3)|浏览(967)

Spring Boot 2.4.0 ,数据库为mysql 8 .
每隔15秒用rest从远程获取数据,并用rest将其存储到mysql数据库 saveAll() .
为所有给定实体调用save()方法。
所有数据都已设置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> {
}

以下是json格式的iot实体截图:

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" : "",
  ...

所以身份证已经确定了。
此外,还为项目启用了批处理。它不应该对储蓄有任何影响。
我不明白为什么它试图插入一个新的实体而不是更新现有的实体?
为什么它不能区分新旧实体的区别?
更新:
为实体实现持久化:

@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文件内容。
如何解决这个问题?

vxqlmq5t

vxqlmq5t1#

spring数据jpa使用@version@id字段的组合来决定是合并还是插入。
null@id和null@version意味着插入新记录
如果存在@id,@version字段用于决定是合并还是插入。
更新仅在以下情况下调用(更新…)。。。。其中id=和version=0)
因为您缺少@id和@version,它正在尝试插入,因为underlysing系统决定这是新记录,并且在运行sql时得到错误。

bq8i3lrv

bq8i3lrv2#

问题很可能是,既然 @Id 未标记为 @GeneratedValue ,spring数据假定所有分离的(暂时的)实体都传递给 save()/saveAll() 应该有 EntityManager.persist() 在他们身上调用。
尝试制作 IotEntity 实施 Persistable 又回来了 falseisNew() . 这将告诉spring数据始终使用 EntityManager.merge() 相反,它应该具有预期的效果(即插入不存在的实体并更新现有的实体)。

qrjkbowd

qrjkbowd3#

看来我找到了这种行为的根源。
主应用程序启动程序看起来像:

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

这三个步骤都有严格的执行顺序。如果需要,第一种方法必须重复,以便在本地更新数据。另外两个服务器只处理存储的数据。
第一种方法如下所示:

@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

两个线程在同一个catch和store上并行运行-尝试 insert 数据(每次开始时都会重新创建表)。
所有稍后的回迁都很好,它们只由 @Sceduled 线程。
if注解 @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);
}

相关问题