Issue
I have the following entities:
Warehouse.java
@Entity
@Table(name = "warehouses")
public class Warehouse {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(name = "name")
private String name;
@Column(name = "is_default")
private boolean isDefault;
@OneToOne(mappedBy = "warehouse", cascade = CascadeType.ALL, optional = false)
private WarehouseAddress address;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "store_id", referencedColumnName = "id", table = "warehouses")
private Store store;
//other getters and setters
public WarehouseAddress getAddress() {
return address;
}
public void setAddress(WarehouseAddress address) {
this.address = address;
this.address.setWarehouse(this);
}
//equals hashCode
}
WarehouseAddress.java
@Entity
@Table(name = "warehouse_addresses")
public class WarehouseAddress {
@Id
@Column(name = "warehouse_id")
private int id;
@OneToOne
@MapsId
private Warehouse warehouse;
@Column(name = "street_name")
private String streetName;
@Column(name = "street_number")
private String streetNumber;
@Column(name = "floor")
private String floor;
@Column(name = "door_number")
private String doorNumber;
@Column(name = "observation")
private String observation;
@Column(name = "zip_code")
private String zipCode;
@Column(name = "city")
private String city;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "province_id", referencedColumnName = "id", table = "warehouse_addresses")
private Province province;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "country_code", referencedColumnName = "code", table = "warehouse_addresses")
private Country country;
//other getters and setters
public void setWarehouse(Warehouse warehouse) {
this.warehouse = warehouse;
}
//equals and hashCode
}
The problem I'm getting is that whenever I have a Warehouse
entity that doesn't have a WarehouseAddress
, and I attempt to create one manually as shown in the self contained example below, I get the following exception:
2021-06-08 14:50:51,456 DEBUG [http-nio-8080-exec-1] org.hibernate.SQL: insert into warehouse_addresses (city, country_code, door_number, floor, observation, province_id, street_name, street_number, zip_code, warehouse_id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2021-06-08 14:50:51,461 WARN [http-nio-8080-exec-1] org.hibernate.engine.jdbc.spi.SqlExceptionHelper: SQL Error: 1048, SQLState: 23000
2021-06-08 14:50:51,461 ERROR [http-nio-8080-exec-1] org.hibernate.engine.jdbc.spi.SqlExceptionHelper: Column 'city' cannot be null
2021-06-08 14:50:51,467 ERROR [http-nio-8080-exec-1] org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/].[dispatcherServlet]: Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement] with root cause
java.sql.SQLIntegrityConstraintViolationException: Column 'city' cannot be null
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:117)
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97)
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122)
at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:953)
at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1092)
at ...
This is a quick and dirty example that triggers the following exception:
@RestController
@RequestMapping("test")
public class TestController {
private ProvinceService provinceService;
private CountryService countryService;
private WarehouseService warehouseService;
private StoreService storeService;
private WarehouseRepository warehouseRepository;
@Autowired
public TestController(ProvinceService provinceService, CountryService countryService, WarehouseService warehouseService, StoreService storeService, WarehouseRepository warehouseRepository) {
this.provinceService = provinceService;
this.countryService = countryService;
this.warehouseService = warehouseService;
this.storeService = storeService;
this.warehouseRepository = warehouseRepository;
}
@GetMapping
@Transactional
public void test() {
Store store = storeService.findById(55);
Warehouse warehouse = warehouseService.findByNameAndStore("WarehouseTestName", store);
WarehouseAddress newAddress = new WarehouseAddress();
warehouse.setAddress(newAddress);
newAddress.setProvince(provinceService.findByCode("AR-C"));
newAddress.setCountry(countryService.findByCode("AR"));
newAddress.setStreetName("a");
newAddress.setStreetNumber("a");
newAddress.setFloor(null);
newAddress.setDoorNumber(null);
newAddress.setObservation(null);
newAddress.setZipCode("155");
newAddress.setCity("CABA");
warehouseRepository.save(warehouse);
}
}
Interesting things I found
- If I move
warehouse.setAddress(newAddress);
to be called afternewAddress.setCity("CABA");
I don't get the exception. - Hibernate seems to "magically" populate the
id
field inWarehouseAddress
after callingnewAddress.setProvince(provinceService.findByCode("AR-C"));
, which is even weirder. This doesn't happen if I do what I mentioned earlier and callsetAddress
after setting the address fields.
Any help would be appreciated as to what could be causing this issue. I sadly can't use the first workaround because I'm using MapStruct to update DB entities and the generated code first assigns the WarehouseAddress
to the Warehouse
and then maps its properties (like in the example).
Solution
If you call warehouse.setAddress(newAddress);
first you modify the managed entity (managed entity because you have fetched the warehouse from the database => now managed by hibernate). After that you are performing a query operation => Hibernate flushes your previous changes to the database to prevent dirty reads before performing the database query operation. In this case the properties of newAddress
are not set which leads to the constraint violation exception. Same goes for the
Hibernate seems to "magically" populate the id field in WarehouseAddress after calling newAddress.setProvince(provinceService.findByCode("AR-C"));,
The WarehouseAddress
gets persisted before performing the query operation and has therefore an id.
Answered By - Daniel Wosch