H2 - duplicate key not throwing exception in Spring boot test when Persistable isNewObject set to true

Clash Royale CLAN TAG#URR8PPP
H2 - duplicate key not throwing exception in Spring boot test when Persistable<ID> isNewObject set to true
I am using H2 as test DB in a Java Spring Boot application, but when I want to catch a "duplicate key" exception when trying to insert an duplicate ID/PK, H2 does not throw anything.
With Postman all is fine and nice, I just cannot pass the test.
The real DB is PostgreSQL, and it does throw exception when I integration test with Postman. But when unit testing I think it is not necessary to load real DB so I chose H2.
H2 config:
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 definition:
@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;
The controller insert method:
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));
The test:
@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.
Note that mockBinInfo has isNewObject always set to true, meaning that it should throw exception when finding duplicate PK in the second insertion, but it is not happening. This is required by Persistable<ID> interface, which will tell DB whether or not to persist when ID is duplicate or not.
mockBinInfo
isNewObject
true
Persistable<ID>
isNew()
true
See here for more information(searching "Persistable").
EDIT:
I also noticed that H2 seems not to support Spring Persistable<ID> and always return new=false in the saved entity.
Persistable<ID>
new=false
Log details:
MockHttpServletRequest:
HTTP Method = POST
Request URI = /insert
Parameters =
Headers = Content-Type=[application/json;charset=UTF-8], Accept=[application/json;charset=UTF-8]
Body = "bin":"123456", "json_full":"'brand':'visa', 'type':'credit', 'country':'USA', 'issuer':'BigBank'", "brand":"visa", "type":"credit", "country":"USA", "issuer":"BigBank", "newObject":"true", "new":"true", "createdAt":"18/08/2018 02:00:00 +0200", "updatedAt":"18/08/2018 02:00:00 +0200"
Session Attrs =
Handler:
Type = com.xxxxx.binlookup.controller.BinInfoController
Method = public org.springframework.http.ResponseEntity<java.lang.Object> com.xxxxx.binlookup.controller.BinInfoController.insertBIN(com.xxxxx.binlookup.model.BinInfo,javax.servlet.http.HttpServletResponse) throws com.fasterxml.jackson.core.JsonProcessingException
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 201
Error message = null
Headers = Content-Type=[application/json;charset=UTF-8]
Content type = application/json;charset=UTF-8
Body = "id":"123456","newObject":false,"new":false,"bin":"123456","json_full":"'brand':'visa', 'type':'credit', 'country':'USA', 'issuer':'BigBank'","brand":"visa","type":"credit","country":"USA","issuer":"BigBank","createdAt":"18/08/2018 02:00:00 +0200","updatedAt":"18/08/2018 02:00:00 +0200"
Forwarded URL = null
Redirected URL = null
Cookies =
1 Answer
1
If you call save() on an existing entity, Hibernate will not persist it again, but will just update the entity.
save()
Persistable
By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.
Yes but maybe I was not clear:
Persistableenables you to mark the object as new or old, thus adopting different strategies when saving. See edits of my question.– WesternGun
Aug 13 at 9:53