唯一索引违规时H2错误的集成测试-测试@Sql数据加载后的陈旧ID序列?

问题描述 投票:0回答:0

报错信息是:

Unique index or primary key violation: "PRIMARY KEY ON PUBLIC.LAYOUT(ID) ( /* key:1 */ CAST(1 AS BIGINT), 'My simple layout', CAST(1 AS BIGINT))"; SQL statement:
insert into layout (name, profile_id, id) values (?, ?, ?) 

完整的控制台输出:

 :: Spring Boot ::                (v3.0.5)

2023-05-10T11:16:05.732+02:00  INFO 1302958 --- [           main] c.t.s.controller.LayoutControllerTest    : Starting LayoutControllerTest using Java 17.0.1 with PID 1302958 (started by stephane in /home/stephane/dev/java/projects/sql-fetch-all)
2023-05-10T11:16:05.734+02:00  INFO 1302958 --- [           main] c.t.s.controller.LayoutControllerTest    : No active profile set, falling back to 1 default profile: "default"
2023-05-10T11:16:06.576+02:00  INFO 1302958 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2023-05-10T11:16:06.678+02:00  INFO 1302958 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 84 ms. Found 3 JPA repository interfaces.
2023-05-10T11:16:07.286+02:00  INFO 1302958 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2023-05-10T11:16:07.521+02:00  INFO 1302958 --- [           main] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Added connection conn0: url=jdbc:h2:mem:9c41a542-2a01-4464-8528-da45f652e5fd user=SA
2023-05-10T11:16:07.523+02:00  INFO 1302958 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2023-05-10T11:16:07.575+02:00  INFO 1302958 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [name: default]
2023-05-10T11:16:07.622+02:00  INFO 1302958 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate ORM core version 6.1.7.Final
2023-05-10T11:16:07.970+02:00  INFO 1302958 --- [           main] SQL dialect                              : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2023-05-10T11:16:09.024+02:00  INFO 1302958 --- [           main] o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2023-05-10T11:16:09.034+02:00  INFO 1302958 --- [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2023-05-10T11:16:09.890+02:00  WARN 1302958 --- [           main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2023-05-10T11:16:10.320+02:00  INFO 1302958 --- [           main] o.s.b.t.m.w.SpringBootMockServletContext : Initializing Spring TestDispatcherServlet ''
2023-05-10T11:16:10.320+02:00  INFO 1302958 --- [           main] o.s.t.web.servlet.TestDispatcherServlet  : Initializing Servlet ''
2023-05-10T11:16:10.324+02:00  INFO 1302958 --- [           main] o.s.t.web.servlet.TestDispatcherServlet  : Completed initialization in 2 ms
2023-05-10T11:16:10.354+02:00  INFO 1302958 --- [           main] c.t.s.controller.LayoutControllerTest    : Started LayoutControllerTest in 4.985 seconds (process running for 6.219)
2023-05-10T11:16:10.773+02:00  WARN 1302958 --- [           main] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 23505, SQLState: 23505
2023-05-10T11:16:10.773+02:00 ERROR 1302958 --- [           main] o.h.engine.jdbc.spi.SqlExceptionHelper   : Unique index or primary key violation: "PRIMARY KEY ON PUBLIC.LAYOUT(ID) ( /* key:1 */ CAST(1 AS BIGINT), 'My simple layout', CAST(1 AS BIGINT))"; SQL statement:
insert into layout (name, profile_id, id) values (?, ?, ?) [23505-214]
2023-05-10T11:16:10.775+02:00  INFO 1302958 --- [           main] o.h.e.j.b.internal.AbstractBatchImpl     : HHH000010: On release of batch it still contained JDBC statements
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 5.997 s <<< FAILURE! - in com.thalasoft.sqlfetchall.controller.LayoutControllerTest
[ERROR] should_create_one  Time elapsed: 0.435 s  <<< ERROR!
jakarta.servlet.ServletException: 
Request processing failed: org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint ["PRIMARY KEY ON PUBLIC.LAYOUT(ID) ( /* key:1 */ CAST(1 AS BIGINT), 'My simple layout', CAST(1 AS BIGINT))"; SQL statement:
insert into layout (name, profile_id, id) values (?, ?, ?) [23505-214]]
    at com.thalasoft.sqlfetchall.controller.LayoutControllerTest.should_create_one(LayoutControllerTest.java:74)
Caused by: org.springframework.dao.DataIntegrityViolationException: 
could not execute statement; SQL [n/a]; constraint ["PRIMARY KEY ON PUBLIC.LAYOUT(ID) ( /* key:1 */ CAST(1 AS BIGINT), 'My simple layout', CAST(1 AS BIGINT))"; SQL statement:
insert into layout (name, profile_id, id) values (?, ?, ?) [23505-214]]
    at com.thalasoft.sqlfetchall.controller.LayoutControllerTest.should_create_one(LayoutControllerTest.java:74)
Caused by: org.hibernate.exception.ConstraintViolationException: could not execute statement
    at com.thalasoft.sqlfetchall.controller.LayoutControllerTest.should_create_one(LayoutControllerTest.java:74)
Caused by: org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: 
Unique index or primary key violation: "PRIMARY KEY ON PUBLIC.LAYOUT(ID) ( /* key:1 */ CAST(1 AS BIGINT), 'My simple layout', CAST(1 AS BIGINT))"; SQL statement:
insert into layout (name, profile_id, id) values (?, ?, ?) [23505-214]
    at com.thalasoft.sqlfetchall.controller.LayoutControllerTest.should_create_one(LayoutControllerTest.java:74)

2023-05-10T11:16:10.855+02:00  INFO 1302958 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2023-05-10T11:16:10.856+02:00  INFO 1302958 --- [ionShutdownHook] .SchemaDropperImpl$DelayedDropActionImpl : HHH000477: Starting delayed evictData of schema as part of SessionFactory shut-down'
2023-05-10T11:16:10.868+02:00  INFO 1302958 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2023-05-10T11:16:10.873+02:00  INFO 1302958 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.

测试用例为:

@SpringBootTest
@AutoConfigureMockMvc
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@SqlGroup({
  @Sql(value = "classpath:fixture/data.reset.sql", executionPhase = BEFORE_TEST_METHOD),
  @Sql(value = "classpath:fixture/data.init.sql", executionPhase = BEFORE_TEST_METHOD)
})
public class LayoutControllerTest {

  @Test
  void should_create_one() throws Exception {
    final File jsonFile = new ClassPathResource("fixture/data.layout.json").getFile();
    final String content = Files.readString(jsonFile.toPath());

    this.mockMvc.perform(post("/layout/create/{profileId}", 1)
        .contentType(APPLICATION_JSON)
        .content(content))
        .andDo(print())
        .andExpect(status().isCreated())
        .andExpect(jsonPath("$").isMap())
        .andExpect(jsonPath("$", aMapWithSize(4)))
        .andExpect(jsonPath("$.name").value("Another name"));

    assertThat(this.layoutRepository.findAll()).hasSize(1);
  }

SQL数据文件为:

SET REFERENTIAL_INTEGRITY   FALSE;
TRUNCATE TABLE layout RESTART IDENTITY;
TRUNCATE TABLE profile RESTART IDENTITY;
TRUNCATE TABLE profile_type RESTART IDENTITY;
TRUNCATE TABLE product RESTART IDENTITY;
TRUNCATE TABLE product_part RESTART IDENTITY;
TRUNCATE TABLE layout_product RESTART IDENTITY;
SET REFERENTIAL_INTEGRITY   TRUE;

INSERT INTO profile_type (id, profile_type_enum) VALUES (1, 'CAR');

INSERT INTO profile (id, profile_type_id) VALUES (1, 1);

INSERT INTO layout (id, name, profile_id) VALUES (1, 'My simple layout', 1);

INSERT INTO product (id, name, supplier) VALUES (1, 'Lamp', 'Sun');

INSERT INTO product_part (id, name, serial_number, product_id) VALUES (1, 'Shade', 'AA123FR', 1);
INSERT INTO product_part (id, name, serial_number, product_id) VALUES (2, 'Bulb', 'BF43944', 1);
INSERT INTO product_part (id, name, serial_number, product_id) VALUES (3, 'Cable', 'KF84324', 1);

INSERT INTO layout_product (id, layout_id, product_id) VALUES (1, 1, 1);

发布内容的json文件:

{ "name": "Another name" }

测试性能:

spring:
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:demo;DB_CLOSE_ON_EXIT=FALSE
    username: sa
    password:
  jpa:
    hibernate:
      ddl-auto: create-drop
    defer-datasource-initialization: true

spring:
  sql:
    init:
      mode: always

实体是:

@Entity
@Table(name = "layout")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Layout {

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE)
  private Long id;

  @NotNull
  private String name;

  @NotNull
  @ManyToOne(optional = false, fetch = FetchType.LAZY)
  private Profile profile;

  @OneToMany(mappedBy="layout", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
  private Set<LayoutProduct> layoutProducts;

  public void addProduct(LayoutProduct layoutProduct) {
    layoutProducts.add(layoutProduct);
    layoutProduct.setLayout(this);
  }

  public void removeProduct(LayoutProduct layoutProduct) {
    layoutProducts.remove(layoutProduct);
    layoutProduct.setLayout(null);
  }
}

存储库是:

@Repository
public interface LayoutRepository extends JpaRepository<Layout, Long> {

  @Query("SELECT new com.thalasoft.sqlfetchall.data.model.domain.projection.LayoutView(l, pf, pt, lp, pd) FROM LayoutProduct lp JOIN lp.layout l JOIN l.profile pf JOIN pf.profileType pt JOIN lp.product pd WHERE l.id = :id")
  List<LayoutView> findByIdFetching(@Param("id") Long id);

}

控制器是:

  @PostMapping("/create/{profileId}")
  @ResponseStatus(HttpStatus.CREATED)
  public LayoutGetDto create(@PathVariable("profileId") Long profileId, @RequestBody LayoutPostDto layoutDto) {
    layoutDto.setProfileDto(this.layoutMapper.entityToDto(profileService.findById(profileId)));
    LayoutGetDto ldto = this.layoutMapper.entityToDto(this.layoutService.create(this.layoutMapper.dtoToEntity(layoutDto)));
    return ldto;
  }

服务是:

  public Layout create(Layout layout) {
    final var newLayout = Layout.builder()
        .name(layout.getName())
        .profile(layout.getProfile())
        .build();
    return this.layoutRepository.save(newLayout);
  }

错误由

this.layoutRepository.save(newLayout);
调用触发。

调试器显示

newLayout
对象的预期 json 内容。

注意,插入失败的布局名称不是json内容文件中的名称,而是加载数据中的名称。

其他关于查找一个或全部以及删除的测试用例确实通过了。

如果我去掉下面的SQL语句:

INSERT INTO layout (id, name, profile_id) VALUES (1, 'My simple layout', 1);
INSERT INTO layout_product (id, layout_id, product_id) VALUES (1, 1, 1);

在仅由失败测试使用的数据文件的另一个副本中,然后测试正常通过。所以测试最初失败了,因为它试图在另一个已经存在具有相同 id 值的布局时添加一个布局。

奇怪的是这个输出:

org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint ["PRIMARY KEY ON PUBLIC.LAYOUT(ID) ( /* key:1 */ CAST(1 AS BIGINT), 'My simple layout', CAST(1 AS BIGINT))"; SQL statement:
insert into layout (name, profile_id, id) values (?, ?, ?) [23505-214]]

这是否意味着测试正在尝试插入名称为

My simple layout
的布局?或者这是否意味着这个布局已经存在于数据库中并且是冲突的根源?

项目在 Java 17 上,spring-boot-starter-parent 3.0.5

我已经看过这个question并试图根据它的答案采取行动,但它在错误中没有任何改变。

从这个question我怀疑问题来自序列id生成器,它在数据加载后没有更新。因此数据加载插入一个布局,稍后在测试中尝试插入一个 id 值为

1
的布局,因为它的序列在数据加载时没有更新。那只是一个猜测..

我还尝试用

@GeneratedValue(strategy = GenerationType.SEQUENCE)
替换 id 生成策略:
@GeneratedValue(strategy = GenerationType.IDENTITY)
但它在错误中没有任何改变。

更新:我编辑了数据加载文件,将值

1
的布局ID替换为值
2
,如下所示:

INSERT INTO layout (id, name, profile_id) VALUES (2, 'My simple layout', 1);
INSERT INTO layout_product (id, layout_id, product_id) VALUES (1, 2, 1);

所有测试都通过了。

这告诉我,在布局 id 值为

1
的手动插入后,id 序列没有刷新,并且序列仍然为测试的插入提供相同的
1
值,这导致违反约束。

jpa spring-boot-test unique-constraint
© www.soinside.com 2019 - 2024. All rights reserved.