H2-当Persistable <id> isNewObject设置为true时,重复键在Spring Boot测试中不会引发异常

时间:2018-08-13 08:57:41

标签: java spring spring-boot h2

我在Java Spring Boot应用程序中将H2用作测试数据库,但是当我想在尝试插入重复的ID / PK时捕获“重复键”异常时,H2不会抛出任何异常。

有了Postman,一切都很好,我只是无法通过考试。

真正的数据库是PostgreSQL,当我与Postman进行集成测试时,它确实会引发异常。但是在进行单元测试时,我认为没有必要加载实际的数据库,因此我选择了H2。

H2配置:

spring.datasource.url=jdbc:h2:mem:tesdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;mode=MySQL
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

spring.datasource.testWhileIdle=true
spring.datasource.validationQuery=SELECT 1

spring.jpa.datasource.show-sql=true
spring.h2.console.enabled=true # if you need console

Bean定义:

@Entity
@Data
@JsonComponent
@Table(name="bin_info")
public class BinInfo implements Serializable, Persistable<String>{ //with Persistable we can check ID duplicate
    @Id
    @Size(min=6, max=8)
    @Column(name="bin")
    @JsonProperty("bin")
    private String bin;

    ...

    /**
     * Property for identifying whether the object is new or old,
     * will insert(new) or update(old)
     * If is new and id/bin is duplicate, org.hibernate.exception.ConstraintViolationException will be thrown.
     * If is old and id/bin is duplicate, just updates. Hibernate save() will upsert and no complain.
     */
    @Transient
    private boolean isNewObject;

    @Override
    public String getId() {
        return this.bin;
    }

    @Override
    public boolean isNew() {
        return isNewObject;
    }
    @Override
    public String getId() {
        return this.bin;
    }

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

控制器insert方法:

@RequestMapping(value="/insert", method=RequestMethod.POST)
public ResponseEntity<Object> insertBIN(@Valid @RequestBody BinInfo bin_info, HttpServletResponse response) throws JsonProcessingException {
    Map<String, Object> errors = new HashMap<String, Object>();
    try {
        OffsetDateTime now = OffsetDateTime.now();
        bin_info.setCreatedAt(now);
        bin_info.setUpdatedAt(now);
        bin_info.setNewObject(true); //if set to true, bin duplicate -> exception and return 200; then we avoid "select to check duplicate first"
        BinInfo saved = repository.save(bin_info);
        // if is new, created(201); if not, updated(status OK, 200)
        return ResponseEntity.status(HttpStatus.CREATED)
                .contentType(MediaType.APPLICATION_JSON_UTF8).body(saved);
    } catch (DataIntegrityViolationException e0) {
        log.warn("Update BIN due to duplicate", e0); // exception details in log
        //if duplicate, change newObject to false and save again. And return.
        bin_info.setNewObject(false);
        BinInfo saved = repository.save(bin_info);
        return ResponseEntity.ok()          // <<<<<< here I define "save duplicate=update=200, OK"
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .body(saved);
    } catch (Exception e) {
        log.error("Cannot save BinInfo. ", e); // exception details in log
        errors.put("error", "Cannot save BIN"); // don't expose to user
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .body(Utilities.jsonBuilder(errors));
    }

测试:

@Test
public void testBinInfoControllerInsertBIN() throws Exception {
    when(this.repository.save(any(BinInfo.class))).thenReturn(mockBinInfo);
    String content_double_quotes = "{\"bin\":\"123456\", "
                    + "\"json_full\":\"" + this.json_full + "\", "
                    + "\"brand\":\"" + this.brand + "\", "
                    + "\"type\":\"" + this.type + "\", "
                    + "\"country\":\"" + this.country + "\", "
                    + "\"issuer\":\"" + this.issuer + "\", "
                    + "\"newObject\":true, "
                    + "\"new\":true, "
                    + "\"createdAt\":\"18/08/2018 02:00:00 +0200\", "
                    + "\"updatedAt\":\"18/08/2018 02:00:00 +0200\"}";
    log.info("JSON input: " + content_double_quotes);

    //save the entity for the first time(newObject==true, new=true) and should return 201
    this.mockMvc.perform(post("/insert")
            .content(content_double_quotes) //json cannot have single quote; must be double
            .accept(MediaType.APPLICATION_JSON_UTF8_VALUE)
            .contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)
            )
        .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE))
        .andExpect(status().isCreated())
        .andDo(print())
        .andExpect(jsonPath("$.bin", is(this.bin)))
        .andExpect(jsonPath("$.json_full", is(this.json_full)))
        .andExpect(jsonPath("$.brand", is(this.brand)))
        .andExpect(jsonPath("$.type", is(this.type)))
        .andExpect(jsonPath("$.country", is(this.country)))
        .andExpect(jsonPath("$.issuer", is(this.issuer)))
        .andExpect(jsonPath("$.createdAt", is("18/08/2018 02:00:00 +0200")))
        .andExpect(jsonPath("$.updatedAt", is("18/08/2018 02:00:00 +0200")));

    //save the same entity, new == true, and should return 200
    this.mockMvc.perform(post("/insert")
            .content(content_double_quotes) //json cannot have single quote; must be double
            .accept(MediaType.APPLICATION_JSON_UTF8_VALUE)
            .contentType(MediaType.APPLICATION_JSON_UTF8_VALUE))
        .andDo(print())
        .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE))
        .andExpect(status().isOk());   //<<<<< here I always get 201, not 200. With Postman I get 200 instead.
}

请注意,mockBinInfo的{​​{1}}始终设置为isNewObject,这意味着在第二次插入中找到重复的PK时应抛出异常,但不会发生。 true接口要求这样做,当ID重复时,该接口将告诉DB是否保留。

  • 如果Persistable<ID>返回isNew(),则ID重复时将引发异常
  • 否则,将无提示更新

有关更多信息(请搜索“ Persistable”),请参见here

编辑:

我还注意到H2似乎不支持Spring true,并且总是在保存的实体中返回Persistable<ID>

日志详细信息:

new=false

2 个答案:

答案 0 :(得分:0)

如果您在现有实体上调用save(),则Hibernate将不会再次持久化它,而只会更新该实体。

答案 1 :(得分:0)

最后,我中止了该测试,并通过不检查异常来绕过它。它仍然是一个错误。