I'm making one to many relationship in hibernate, but when i try to insert a value
in the tables I get exception that my FK in photos entity can't be null.
My parent entity:
#Table(name = "library")
#Entity
public class Book {
#OneToMany(
fetch = FetchType.LAZY,
mappedBy = "book",
cascade = CascadeType.ALL,
orphanRemoval = true
)
// setters / getters and other columns
My child entity
#Entity
#Table(name = "photos")
public class photos{
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "idphotos")
private int id;
#ManyToOne(cascade = CascadeType.ALL,fetch = FetchType.LAZY)
#JoinColumn(name = "library_idlibrary")
private Book book;
// other columns
Exception:
java.sql.SQLIntegrityConstraintViolationException: Column 'library_idlibrary' cannot be null
com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:115)
com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:95)
com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122)
com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:960)
com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1116)
com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1066)
I have DAO object and controller which are inserting values :
Controller:
#PostMapping( value = "/newBook",consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
public String add(#ModelAttribute("book") #Valid Book book, HttpServletResponse response, HttpServletRequest request,#RequestParam(value = "photo", required = false) List<MultipartFile> photos, #RequestParam(value = "valute",required = false) String valute,BindingResult result) {
if (result.hasErrors()) {
return "addBook";}
List<entity.photos> list = new ArrayList<>();
for (MultipartFile photo : photos) {
File file =null;
if (!photo.isEmpty()) {
try {
validateImg(photo);
file = new File(context.getRealPath("/uploadFile/" + photo.getOriginalFilename()));
Logger.getAnonymousLogger().info("THE PATH IS " + file.getAbsolutePath());
FileUtils.copyInputStreamToFile(photo.getInputStream(), file);
file.createNewFile();
} catch (IOException ex) {
result.reject("uk_UA", "Поганий тип");
return "addBook";
}
}
if (file != null) {
photos ph = new photos(photo.getOriginalFilename());
StringBuilder builder = new StringBuilder();
builder.append(file.getAbsolutePath());
Logger.getAnonymousLogger().info("SOMETHING HAS BROKEN IN LOGGING FILE");
System.out.println("INSIDE ADD METHOD ");
list.add(ph);
} else list.add(new photos(""));
}
book.getPhotosList().addAll(list);
service.add(book);
return "redirect:/books";
}
DAO:
public void add(Book user) {
currentSession().save(user);
}
Most probably you have not set your backreference from the Photo to the Book and then you cascade the persist/merge operation from the Book through the photoes collection. When you add element to collection you need to maintain both sides of the relationship. For example:
Book book = new Book();
Photo photo = new Photo();
book.addPhoto(photo);
photo.setBook(book);
//notice on the line above we set the backreference of the photo to the book.
hibernateSession.persist(book);
// now it is cascading from book to photo and it will set the foreign keys correctly.
Related
I have a csv file that has a list of employees and projects information (each employee having multiple projects).
I upload the file, parse the csv and build a parent-child relationship -> Employee and Projects respectively and save it to Database.
Below is my Employee Entity class (I tried the commented part but no luck)
#Entity
#Table(name = "employees")
#Data
#NoArgsConstructor
#AllArgsConstructor
public class Employee {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#NotNull
private String name;
#NotNull
private String empId;
#NotNull
private String department;
//#OneToMany(fetch = FetchType.LAZY, mappedBy = "transcript", cascade = CascadeType.ALL) //tried didn't work
#OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "employee", orphanRemoval = true)
private List<Project> projects;
public Employee(String name, String empId, String department) {
this.name = name;
this.empId = empId;
this.department = department;
}
}
Below is my Project Entity class
#Entity
#Table(name = "projects")
#Data
#NoArgsConstructor
#AllArgsConstructor
public class Project {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String projectName;
private String projectCode;
private Instant projectStartDate;
#ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST)
#JoinColumn(name = "employee_id", nullable = false)
#JsonBackReference
private Employee employee;
public Project(String name, String code, Instant time) {
this.projectName = name;
this.projectCode = code;
this.projectStartDate = time;
}
}
This is where I am trying to save my Employee object:
#Transactional
public void uploadEmployees(MultipartFile file) {
try {
//calls csv helper that does all the parsing and brings into Employee model object format
Set<Employess> employees = CSVHelper.csvToEmployess(file.getInputStream(), "2");
employeeRepository.saveAll(employees);
System.out.println("I processed all the records of cdv into java");
} catch (IOException e) {
throw new RuntimeException(e);
}catch (Exception e ){
throw new RuntimeException(e.getMessage());
}
}
This above method throws :
2022-09-11T22:30:08.438-05:00 ERROR 63321 --- [nio-8086-exec-5] o.h.engine.jdbc.spi.SqlExceptionHelper : ERROR: null value in column "employee_id" of relation "projects" violates not-null constraint
Detail: Failing row contains (6, p1, 123, 0184-06-08T01:07:47Z, null).
If I change the id generation type to #GeneratedValue(strategy = GenerationType.AUTO) : I get the below exception:
2022-09-12T00:55:09.465-05:00 WARN 73014 --- [nio-8086-exec-1] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 0, SQLState: 42P01
2022-09-12T00:55:09.465-05:00 ERROR 73014 --- [nio-8086-exec-1] o.h.engine.jdbc.spi.SqlExceptionHelper : ERROR: relation "hibernate_sequence" does not exist
Position: 17
org.springframework.dao.InvalidDataAccessResourceUsageException: could not extract ResultSet; SQL [n/a]; nested exception is org.hibernate.exception.SQLGrammarException: could not extract ResultSet
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:259)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:233)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:551)
at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:152)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:174)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
at jdk.proxy2/jdk.proxy2.$Proxy133.saveAll(Unknown Source)
Just before executing saveAll method,I tried looking how my employees list looks like using debugger and it looks as below :
Employee(id=0, name="test", empId="4511504557911789", department="JJ967W"
projects=[
Project(id=0, projectName="p1", projectCode="123", projectStartDate="0184-06-08T01:07:47Z", employee=null),
Project(id=0, projectName="p2", projectCode="345", projectStartDate="0184-06-08T01:07:47Z", employee=null)
]
)
I am thinking employee=null makes sense because it was not saved to DB yet.
Please help me out in what I am missing here that is causing this to break.
Thank you very much for taking time in providng help.
**** UPDATED ****
Adding the part where I am adding parsing CSV and building Employee and Project objects
public static Set<Employee> csvToTranscripts(InputStream is) throws IOException {
try (BufferedReader fileReader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
CSVParser csvParser = new CSVParser(fileReader,
CSVFormat.DEFAULT.withFirstRecordAsHeader().withIgnoreHeaderCase().withTrim());) {
Set<Employee> employees = new HashSet<>();
List<CSVRecord> csvRecordsList = csvParser.getRecords();
Employee employee = null;
List<String> empIds = new ArrayList<>();
List<Project> projects = null;
//iterate through list and create project list for new emp_id
for(CSVRecord csvRecord : csvRecordsList){
if(employees.isEmpty() || !empIds.contains(csvRecord.get("EMP_ID"))){
employee = new Employee(
csvRecord.get("NAME"),
csvRecord.get("EMP_ID"),
csvRecord.get("DEPARTMENT")
);
empIds.add(csvRecord.get("EMP_ID")); //this is mainly to check for unique emp id
projects = new ArrayList<>();
}
Project project = new Project(csvRecord.get("NAME"),
csvRecord.get("CODE"),
formatToInstant(csvRecord.get("TIME")) );
project.setEmployee(employee);//setting employee to project here
projects.add(project);
employee.setProjects(projects); //setting projects list to employee here
employees.add(employee);
}
return employees;
} catch (IOException e) {
throw new RuntimeException("fail to parse CSV file: " + e.getMessage());
}
}
************ RESOLVED ******************
Thanks everyone for help. Setting employee to project resolved the issue. what a dumb I am !!!
project.setEmployee(employee);
Further to comment by #M.Deinum - declare a method like this in your Employee class and use it to add a project to an employee ensuring the bidirectional relationship is kept in sync
public void addProject(Project project) {
if (projects == null) {
projects = new ArrayList<>();
}
project.setEmployee(this);
projects.add(project);
}
I am thinking employee=null makes sense because it was not saved to DB yet.
This is wrong. If you save an item with bi-directional relationship then you cannot have a case when an orphan Project exists without an Employee. So before calling any CrudRepository.save*() you must make sure that all projects reference some Employee.
Often in such cases when you add projects to employee you do it like:
// method of Employee class
public void addAll(Projects projects) {
for (Project : projects) {
project.setEmployee(this);
}
this.projects.addAll(projects);
}
I would like to do the following:
Inserting the CityHistory into the database using JPA.
The first time there is no data, so a new city will be inserted. (IT WORKS FINE)
the (IDENTIFICATION) within the city table is a unique field.
What I want to achieve is when I am inserting the same city again is to reuse the existing field instead of trying to create a new one (identification will be like a city's unique name).
So how can I do that using JPA or Hibernate?
#Entity
public class CityHistory extends History implements Serializable {
#Id
#Column(name = "KEY_CITY_HISTORY", nullable = false, precision = 19)
private Long id;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "CITY_ID", nullable = false, foreignKey = #ForeignKey(name = "FK_CITY_ID"))
private City cityId;
#Column(name = "CITY_NAME", nullable = false)
private String cityName;
}
#Entity
public class City implements Serializable {
#Id
#Column(name = "KEY_CITY", nullable = false, precision = 19)
private Long id;
#Column(name = "IDENTIFICATION", nullable = false, unique = true)
private String identification;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "MUNICIPALITY_ID", foreignKey = #ForeignKey(name = "FK_MUNICIPALITY_ID"))
private Municipality municipalityId;
}
UPDATE
Here is how I am writing the data to the database,
It's a Spring Batch itemWriter
#Component
public class InfoItemWriter implements ItemWriter<Object> {
#Autowired
private CityHistoryRepository cityHistoryRepository;
#Override
public void write(List<? extends Object> items) throws Exception {
if (items.size() > 0 && items.get(0) instanceof CityHistory) {
cityHistoryRepository.saveAll((List<? extends CityHistory>) items);
}
}
}
First of all thanks to all who tried to help!
Reading the resources that #Benjamin Maurer provided:
I don't think you want the cascade on the ManyToOne side, see One-To-Many
The most common Parent – Child association consists of a one-to-many and a many-to-one relationship, where the cascade being useful for the one-to-many side only
As the relation I have is ManyToOne it was really not useful to use the cascade and doesn't serve my need.
I used a different approache to reach the goal. I have created a service where it validates the existence of a city, then adds a new city if it does not exist.
#Service
public class CityHistoryServiceImpl implements CityHistoryService {
#Autowired
CityRepository cityRepository;
#Autowired
CityHistoryRepository cityHistoryRepository;
#Override
public Optional<CityHistory> addCityHistory(City city, String cityName, ..) {
if (city != null && cityName != null) {
City city1 = addCityIfNotExist(city);
CityHistory cityHistory = new CityHistory();
cityHistory.setCityId(city1);
cityHistory.setCityName(cityName);
cityHistoryRepository.save(cityHistory);
return Optional.of(cityHistory);
}
return Optional.empty();
} ....
private City addCityIfNotExist(City city) {
City city1 = cityRepository.findFirstByBagId(city.getBagId());
if (city1 == null) {
city1 = cityRepository.save(city);
}
return city1;
}
}
Hibernate will use the #Id property of City to determine if it is new or not. When it is null, Hibernate couldn't possibly know that a similar entry already exists.
So you need to perform a query to find each city first:
for (var history : histories) {
var cities = em.createQuery("select city from City city where city.identification = ?1", City.class)
.setParameter(1, history.getCityId().getIdentification())
.getResultList();
if (!cities.isEmpty()) {
history.setCityId(cities.get(0));
}
em.persist(history);
}
If you use Hibernate and City.identification is unique and always non-null, you can use it as a NaturalID:
In City:
#NaturalId
private String identification;
Then:
for (var history : histories) {
var city = em.unwrap(Session.class)
.byNaturalId(City.class)
.using("identification", history.getCityId().getIdentification())
.getReference();
if (city != null) {
history.setCityId(city);
}
em.persist(history);
}
But if you do have City.id set, i.e., not null, you can use EntityManager.merge to get a managed entity:
for (var history : histories) {
City city = history.getCityId();
if (city.getId() != null) {
city = em.merge(city);
history.setCityId(city);
}
em.persist(history);
}
One more remark: We are not in the relational domain, but we are mapping object graphs. So calling your fields cityId and municipalityId is arguably wrong - even the type says so: City cityId.
They are not just plain identifiers, but full fledged objects: City city.
I have three entity classes of the following:
Shipments Entity:
#Entity
#Table(name = "SHIPMENT")
public class Shipment implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "SHIPMENT_ID", nullable = false)
private int shipmentId;
#Column(name = "DESTINATION", nullable = false)
private String destination;
#OneToMany(mappedBy = "shipment")
private List<ShipmentDetail> shipmentDetailList;
//bunch of other variables omitted
public Shipment(String destination) {
this.destination = destination;
shipmentDetailList = new ArrayList<>();
}
Shipment Details Entity:
#Entity
#Table(name = "SHIPMENT_DETAIL")
public class ShipmentDetail implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "SHIPMENT_DETAIL_ID", nullable = false)
private int shipmentDetailId;
#ManyToOne
#JoinColumn(name = "PRODUCT_ID", nullable = false)
private Product product;
#JsonIgnore
#ManyToOne
#JoinColumn(name = "SHIPMENT_ID", nullable = false)
private Shipment shipment;
//bunch of other variables omitted
public ShipmentDetail() {
}
public ShipmentDetail(Shipment shipment, Product product) {
this.product = product;
this.shipment = shipment;
}
Product Entity:
#Entity
#Table(name = "Product")
public class Product implements Serializable {
#Id
#Column(name = "PRODUCT_ID", nullable = false)
private String productId;
#Column(name = "PRODUCT_NAME", nullable = false)
private String productName;
//bunch of other variables omitted
public Product() {
}
public Product(String productId, String productName) {
this.productId = productId;
this.productName = productName;
}
I am receiving JSONs through a rest API. The problem is I do not know how to deserialize a new Shipment with shipmentDetails that have relationships to already existing objects just by ID. I know you can simply deserialize with the objectmapper, but that requires all the fields of product to be in each shipmentDetail. How do i instantiate with just the productID?
Sample JSON received
{
"destination": "sample Dest",
"shipmentDetails": [
{
"productId": "F111111111111111"
},
{
"productId": "F222222222222222"
}
]
}
Currently my rest endpoint would then receive the JSON, and do this:
public ResponseEntity<String> test(#RequestBody String jsonString) throws JsonProcessingException {
JsonNode node = objectMapper.readTree(jsonString);
String destination = node.get("destination").asText();
Shipment newShipment = new Shipment(destination);
shipmentRepository.save(newShipment);
JsonNode shipmentDetailsArray = node.get("shipmentDetails");
int shipmentDetailsArrayLength = shipmentDetailsArray.size();
for (int c = 0; c < shipmentDetailsArrayLength; c++) {
String productId = node.get("productId").asText();
Product product = productRepository.findById(productId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "No product with ID of: " + productId + " exists!"));
ShipmentDetail shipmentDetail = new ShipmentDetail(newShipment, product, quantity);
shipmentDetailRepository.save(shipmentDetail);
}
}
what i want to do is:
public ResponseEntity<String> test2(#RequestBody String jsonString) throws JsonProcessingException {
JsonNode wholeJson = objectMapper.readTree(jsonString);
Shipment newShipment = objectMapper.treeToValue(wholeJson, Shipment.class);
return new ResponseEntity<>("Transfer Shipment successfully created", HttpStatus.OK);
}
I tried this solution to no. avail:
Deserialize with Jackson with reference to an existing object
How do I make the product entity search for an existing product instead of trying to create a new product. The hacky extremely inefficient workaround I have been using is to traverse the json array, and for every productId find the product using the productRepository, and then set the shipmentDetail with the product one by one. Im not sure if this is best practice as im self learning spring.
So in pseudocode what im trying to do would be:
Receive JSON
Instantiate Shipment entity
Instantiate an array of shipmentDetail entities
For each shipmentDetail:
1. Find product with given productId
2. Instantiate shipmentDetail with product and shipment
Code has been significantly simplified to better showcase the problem,
You have a bottleneck in your code in this part:
Product product = productRepository.findById(productId)
Because you are making a query for each productId, and it will perform badly with large number of products. Ignoring that, I will recommend this aproach.
Build your own deserializer (see this):
public class ShipmentDeserializer extends JsonDeserializer {
#Override
public Shipment deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
String destination = node.get("destination").asText();
Shipment shipment = new Shipment(destination);
JsonNode shipmentDetailsNode = node.get("shipmentDetails");
List shipmentDetailList = new ArrayList();
for (int c = 0; c < shipmentDetailsNode.size(); c++) {
JsonNode productNode = shipmentDetailsNode.get(c);
String productId = productNode.get("productId").asText();
Product product = new Product(productId);
ShipmentDetail shipmentDetail = new ShipmentDetail(product);
shipmentDetailList.add(shipmentDetail);
}
shipment.setShipmentDetailList(shipmentDetailList);
return shipment;
}
}
Add the deserializer to your Shipment class:
#JsonDeserialize(using = ShipmentDeserializer .class)
public class Shipment {
// Class code
}
Deserialize the string:
public ResponseEntity test2(#RequestBody String jsonString) throws JsonProcessingException {
Shipment newShipment = objectMapper.readValue(jsonString, Shipment.class);
/* More code */
return new ResponseEntity("Transfer Shipment successfully created", HttpStatus.OK);
}
At this point, you are only converting the Json into classes, so we need to persist the data.
public ResponseEntity test2(#RequestBody String jsonString) throws JsonProcessingException {
Shipment newShipment = objectMapper.readValue(jsonString, Shipment.class);
shipmentRepository.save(newShipment);
List<ShipmentDetail> shipmentDetails = newShipment.getShipmentDetailList();
for (int i = 0; i < shipmentDetails.size(); c++) {
ShipmentDetail shipmentDetail = shipmentDetails.get(i);
shipmentDetail.setShipment(newShipment);
Product product = productRepository.findById(productId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "No product with ID of: " + productId + " exists!"));
shipmentDetail.setProduct(product);
shipmentDetailRepository.save(shipmentDetail);
}
return new ResponseEntity("Transfer Shipment successfully created", HttpStatus.OK);
}
I know you want to reduce the code in the test method, but I DO NOT RECOMMEND to combine the Json deserialize with the persistence layer. But if you want to follow that path, you could move the productRepository.findById(productId) into the ShipmentDeserializer class like this:
public class ShipmentDeserializer extends JsonDeserializer {
#Override
public Shipment deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
String destination = node.get("destination").asText();
Shipment shipment = new Shipment(destination);
JsonNode shipmentDetailsNode = node.get("shipmentDetails");
List shipmentDetailList = new ArrayList();
for (int c = 0; c < shipmentDetailsNode.size(); c++) {
JsonNode productNode = shipmentDetailsNode.get(c);
String productId = productNode.get("productId").asText();
Product product = productRepository.findById(productId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "No product with ID of: " + productId + " exists!"));
ShipmentDetail shipmentDetail = new ShipmentDetail(product);
shipmentDetailList.add(shipmentDetail);
}
shipment.setShipmentDetailList(shipmentDetailList);
return shipment;
}
}
But if you want to do that, you need to inject the repository into the deserializer (see this).
I think your current approach is not a bad solution, you are dealing with the problem correctly and in few steps.
Any way, one thing you can try is the following.
The idea will be to provide a new field, productId, defined on the same database column that supports the relationship with the Product entity, something like:
#Entity
#Table(name = "SHIPMENT_DETAIL")
public class ShipmentDetail implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "SHIPMENT_DETAIL_ID", nullable = false)
private int shipmentDetailId;
#Column(name = "PRODUCT_ID", nullable = false)
private String productId;
#ManyToOne
#JoinColumn(name = "PRODUCT_ID", insertable = false, updatable = false)
private Product product;
#JsonIgnore
#ManyToOne
#JoinColumn(name = "SHIPMENT_ID", nullable = false)
private Shipment shipment;
//bunch of other variables omitted
public ShipmentDetail() {
}
public ShipmentDetail(Shipment shipment, Product product) {
this.product = product;
this.shipment = shipment;
}
}
The product field must be annotated as not insertable and not updatable: on the contrary, Hibernate will complaint about which field should be used to maintain the relationship with the Product entity, in other words, to maintain the actual column value.
Modify the Shipment relationship with ShipmentDetail as well to propagate persistence operations (adjust the code as per your needs):
#OneToMany(mappedBy = "shipment", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ShipmentDetail> shipmentDetailList;
Then, you can safely rely on the Spring+Jackson deserialization and obtain a reference to the received Shipment object:
public ResponseEntity<String> processShipment(#RequestBody Shipment shipment) {
// At this point shipment should contain the different details,
// each with the corresponding productId information
// Perform the validations required, log information, if necessary
// Save the object: it should persist the whole object tree in the database
shipmentRepository.save(shipment);
}
This approach has an obvious drawback, the existence of the Product is not checked beforehand.
Although you can ensure data integrity at database level with the use of foreign keys, perhaps it would be convenient to validate that the information is right before perform the actual insertion:
public ResponseEntity<String> processShipment(#RequestBody Shipment shipment) {
// At this point shipment should contain the different details,
// each with the corresponding productId information
// Perform the validations required, log information, if necessary
List<ShipmentDetail> shipmentDetails = shipment.getShipmentDetails();
if (shipmentDetails == null || shipmentDetails.isEmpty()) {
// handle error as appropriate
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "No shipment details provided");
}
shipmentDetails.forEach(shipmentDetail -> {
String productId = shipmentDetail.getProductId();
Product product = productRepository.findById(productId).orElseThrow(
() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
"No product with ID of: " + productId + " exists!")
)
});
// Everything looks fine, save the object now
shipmentRepository.save(shipment);
}
So my second post. This time i worked on a passion project of mine, which turned out to be far more complicated than I expected and again I need some help.
I have two enitites: Gamestate and User.
Users are supposed to be able to join multiple Games(/gamestates). Games(/gamestates) are supposed to have muliple people join them. So therefore it is represented as a N:M Relation.
Depending on who joins and when they join they are supposed to have different roles, giving them different rights in the app. Which means I needed an N:M Relation with custom fields and therefore I had to model the relation table myself. That's as far as I have come.
Abstract Model:
#EqualsAndHashCode
#Getter
#Setter
#ToString
public abstract class AbstractModel {
#Id
#GeneratedValue
protected Long id;
#NotNull
protected String identifier;
}
User
#Getter
#Setter
#Entity
#Builder
#NoArgsConstructor
#AllArgsConstructor
#ToString(callSuper = true)
#EqualsAndHashCode(callSuper = true)
public class User extends AbstractModel{
private String nickName;
private UserRole role;
#ToString.Exclude
#EqualsAndHashCode.Exclude
#OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "user", orphanRemoval = true)
private LoginInformation loginInformation;
#ToString.Exclude
#EqualsAndHashCode.Exclude
#OneToMany(cascade = {CascadeType.PERSIST}, fetch = FetchType.LAZY, mappedBy = "gameState")
private List<UserGameState> userGameStates = new ArrayList<>();
//DTO Constructor
public User(UserDTO userDTO){
this.identifier = Optional.ofNullable(userDTO.getIdentifier())
.orElse(UUID.randomUUID().toString());
this.nickName = userDTO.getNickName() == null ? "": userDTO.getNickName();
this.role = UserRole.valueOf(userDTO.getRole());
this.loginInformation = null;
if(userDTO.getLoginInformation() != null) {
setLoginInformation(new LoginInformation(userDTO.getLoginInformation()));
} else {
setLoginInformation(new LoginInformation());
}
(userDTO.getUserGameStates() == null ? new ArrayList<GameStateDTO>() : userDTO.getUserGameStates())
.stream()
.map(x -> new UserGameState((UserGameStateDTO) x))
.forEach(this::addUserGameState);
}
GameState
#Getter
#Setter
#Entity
#Builder
#NoArgsConstructor
#AllArgsConstructor
#ToString(callSuper = true)
#EqualsAndHashCode(callSuper = true)
public class GameState extends AbstractModel{
private String name;
private String description;
private String image;
#ToString.Exclude
#EqualsAndHashCode.Exclude
#OneToMany(cascade = {CascadeType.PERSIST}, fetch = FetchType.LAZY, mappedBy = "user")
private List<UserGameState> userGameStates = new ArrayList<>();
//DTO Constructor
public GameState(GameStateDTO gameStateDTO){
this.identifier = Optional.ofNullable(gameStateDTO.getIdentifier())
.orElse(UUID.randomUUID().toString());
this.name = gameStateDTO.getName() == null ? "": gameStateDTO.getName();
this.description = gameStateDTO.getDescription() == null ? "": gameStateDTO.getDescription();
this.image = gameStateDTO.getImage() == null ? "": gameStateDTO.getImage();
(gameStateDTO.getUserGameStates() == null ? new ArrayList<UserDTO>() : gameStateDTO.getUserGameStates())
.stream()
.map(x -> new UserGameState((UserGameStateDTO) x))
.forEach(this::addUserGameState);
}
//----------------------1:1 Relationship Methods----------------------
//----------------------1:N Relationship Methods----------------------
public void addUserGameState(UserGameState userGameState) {
if (userGameStates.contains(userGameState)) {
return;
}
userGameStates.add(userGameState);
userGameState.setGameState(this);
}
public void removeUserGameState(UserGameState userGameState) {
if (!userGameStates.contains(userGameState)) {
return;
}
userGameState.setGameState(null);
userGameStates.remove(userGameState);
}
//----------------------N:1 Relationship Methods----------------------
//----------------------N:M Relationship Methods----------------------
}
UserGameSatet (Custom N:M Table)
#Getter
#Setter
#Entity
#Builder
#ToString
#NoArgsConstructor
#AllArgsConstructor
#EqualsAndHashCode
public class UserGameState{
#EmbeddedId
private User_GameState_PK id;
#ManyToOne(cascade = {CascadeType.PERSIST}, fetch = FetchType.LAZY)
#MapsId("user_id")
#JoinColumn(name = "USER_ID", insertable = false, updatable = false)
private User user;
#ManyToOne(cascade = {CascadeType.PERSIST}, fetch = FetchType.LAZY)
#MapsId("gameState_id")
#JoinColumn(name = "GAMESTATE_ID", insertable = false, updatable = false)
private GameState gameState;
//add Role later
public UserGameState(User u, GameState gs) {
// create primary key
this.id = new User_GameState_PK(u.getId(), gs.getId());
// initialize attributes
setUser(u);
setGameState(gs);
}
public UserGameState(UserGameStateDTO userGameStateDTO){
//this.id =
this.user = null;
this.gameState = null;
}
//----------------------1:1 Relationship Methods----------------------
//----------------------1:N Relationship Methods----------------------
//----------------------N:1 Relationship Methods----------------------
public void setUser(User user) {
if (Objects.equals(this.user, user)) {
return;
}
User oldUser = this.user;
this.user = user;
if (oldUser != null) {
oldUser.removeUserGameState(this);
}
if (user != null) {
user.addUserGameState(this);
}
}
public void setGameState(GameState gameState) {
if (Objects.equals(this.gameState, gameState)) {
return;
}
GameState oldGameState = this.gameState;
this.gameState = gameState;
if (oldGameState != null) {
oldGameState.removeUserGameState(this);
}
if (oldGameState != null) {
oldGameState.addUserGameState(this);
}
}
//----------------------N:M Relationship Methods----------------------
}
User_GameState_PK (Combined Key)
#Embeddable
#Builder
#ToString
#NoArgsConstructor
#AllArgsConstructor
#Getter
#Setter
public class User_GameState_PK implements Serializable {
#Column(name = "USER_ID")
private Long user_id;
#Column(name = "GAMESTATE_ID")
private Long gameState_id;
public User_GameState_PK(long user_id, long gameState_id){
this.user_id = user_id;
this.gameState_id = gameState_id;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass())
return false;
User_GameState_PK that = (User_GameState_PK) o;
return Objects.equals(user_id, that.user_id) &&
Objects.equals(gameState_id, that.gameState_id);
}
#Override
public int hashCode() {
return Objects.hash(user_id, gameState_id);
}
}
The method saving the Connection in my Service
(both GameState and User are already instantiated, and the method gets the identifier of both objects, retrieving them from the database and adding the relation between them.)
public Optional<GameStateDTO> addUserToGameState(String identifierGS, String identifierU) {
GameState gameState = gameStateRepo.findByIdentifier(identifierGS)
.orElseThrow(() -> new IllegalArgumentException("GameState ID has no according GameState."));
User user = userRepo.findByIdentifier(identifierU)
.orElseThrow(() -> new IllegalArgumentException("User ID has no according User."));
//Custom N:M Connection Part
UserGameState connection = new UserGameState(user, gameState);
userGameStateRepo.save(connection);
return Optional.of(gameState)
.map(m -> convertModelIntoDTO(m));
}
I managed to set the N:M table up, together with its combined key. I tested it with simple CRUD Routes, and they worked.
Next I tried to set up some routes so that people could actually join a game(/gamestate) at which point it throws the following exception upon saving.
javax.persistence.EntityExistsException: A different object with the same identifier value was already associated with the session : [com.Astralis.backend.model.UserGameState#User_GameState_PK(user_id=1, gameState_id=7)]
After reading through some posts on stackoverflow I tried out changing the Cascadetype to .MERGE, which resulted in this exception.
javax.persistence.EntityNotFoundException: ...
Really I am lost here, it feels like if I use .PERSIST, Hibernate complaines that it copies itself while saving the Relation. While if I change it to .MERGE, it complaines that the value isn't already present in the first place.
I am more than thankfull for any breadcrumb bringing me closer to a solution, as this turned out to be a gigantic roadblock for the project, and I have tried out everything that I can think of.
So after a few more days of searching I managed to solve it.
For this I first remade a guide's project in with the data structure from the guide and the service/controller structure of my project. Testing if it would work, and as it did I just started comparing the models with each other and tried all different possibilities out, to find out what is actually causing the issues.
The used guide is this one: https://vladmihalcea.com/the-best-way-to-map-a-many-to-many-association-with-extra-columns-when-using-jpa-and-hibernate/
I had six Copy&Paste (kinda) mistakes that caused Hibernate to falsely associate table columns with each other. These were:
in User:
...
#ToString.Exclude
#EqualsAndHashCode.Exclude
#OneToMany(
cascade = {CascadeType.PERSIST},
fetch = FetchType.LAZY,
mappedBy = "user",// changed from gameState to user
orphanRemoval = true
)
private List<UserGameState> userGameStates = new ArrayList<>();
...
in GameState the reverse:
...
#ToString.Exclude
#EqualsAndHashCode.Exclude
#OneToMany(cascade = {CascadeType.PERSIST},
fetch = FetchType.LAZY,
mappedBy = "gameState",// changed from user to gameState
orphanRemoval = true)
private List<UserGameState> userGameStates = new ArrayList<>();
...
3&4. The JoinColumn Annotations were unnecessary, seemingly I combiend multiple guides into one project. This caused then even more issues:
...
#ManyToOne(
cascade = {CascadeType.PERSIST},
fetch = FetchType.LAZY)
#MapsId("user_id")
//#JoinColumn(name = "USER_ID", insertable = false, updatable = false) //this one removed
private User user;
#ManyToOne(
cascade = {CascadeType.PERSIST},
fetch = FetchType.LAZY)
#MapsId("gameState_id")
//#JoinColumn(name = "GAMESTATE_ID", insertable = false, updatable = false) //this one removed
private GameState gameState;
...
5&6. Two minor copy&paste mistakes, in the "continuity keeper" methods in UserGameState:
...
public void setGameState(GameState gameState) {
if (Objects.equals(this.gameState, gameState)) {
return;
}
GameState oldGameState = this.gameState;
this.gameState = gameState;
if (oldGameState != null) {
oldGameState.removeUserGameState(this);
}
//I copied the previous if block, and replaced the remove... with add...
//But I didn't change the oldGameState to gameState.
//This didn't throw any errors, and actually it still created the relations properly, but I am pretty sure it would cause issues further down the line.
if (gameState != null) {
gameState.addUserGameState(this);
}
}
...
So how does this work now:
As before, when the route with the Identifiers for the connected GameState and User is called, the service "addUserToGameState" is called, getting the models with the given Identifiers.
...
public Optional<GameStateDTO> addUserToGameState(String identifierGS, String identifierU) {
GameState gameState = gameStateRepo.findByIdentifier(identifierGS)
.orElseThrow(() -> new IllegalArgumentException("GameState ID has no according GameState."));
User user = userRepo.findByIdentifier(identifierU)
.orElseThrow(() -> new IllegalArgumentException("User ID has no according User."));
//Custom N:M Connection Part
UserGameState connection = new UserGameState(user, gameState);
return Optional.of(gameState)
.map(m -> convertModelIntoDTO(m));
}
...
After that the UserGameState cosntructer is called, which sets and creates the combined key and calls the setter methods for the related User/GameState fields.
...
public UserGameState(User u, GameState gs) {
// create primary key
this.id = new User_GameState_PK(u.getId(), gs.getId());
// initialize attributes
setUser(u);
setGameState(gs);
}
...
I wrote the setters in a way, that they at the same time, check the added models for relationship consistency issues, and adjust their fields according to if they are newly edited or replaced.
...
public void setUser(User user) {
if (Objects.equals(this.user, user)) {
return;
}
User oldUser = this.user;
this.user = user;
if (oldUser != null) {
oldUser.removeUserGameState(this);
}
if (user != null) {
user.addUserGameState(this);
}
}
public void setGameState(GameState gameState) {
if (Objects.equals(this.gameState, gameState)) {
return;
}
GameState oldGameState = this.gameState;
this.gameState = gameState;
if (oldGameState != null) {
oldGameState.removeUserGameState(this);
}
if (gameState != null) {//copy paste error
gameState.addUserGameState(this);
}
}
...
I created two tables in Oracle SQL Developer editor whos realtion is Many-To-Many, and I also created their hibernate classes 'TestEmployee' and 'TestProject' as shown below in the code. As the relation between the two classes is
Many-To-Many, however a new table named 'Employee_Project2' was created in Oracle SQL Developer editor to hold te primary keys of the other two tables 'TestEmployee' and 'TestProject'.
Values to 'TestEmployee' and 'TestProject' were inserted through Hibernate as shown belwo in section 'records insertion'.
The problem i facing now is, when I run the follwoing command:
SELECT * from Employee_Project2;
from Oracle SQL Developer Editor, i get an empty table despite it is mentioned in the annotation of the Hibernate class 'TestProject' as follwos:
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(name = "Employee_Project2", joinColumns = #JoinColumn(name = "proj_id"), inverseJoinColumns = #JoinColumn(name = "emp_id"))
private Set<TestEmployee> employeesList;
Please let me know why despite there are records inserted into both 'TestEmployee' and 'TestProject' tables, the table 'Employee_Project2' is empty??
note:
I have not explicitly inserted any records into 'Employee_Project2' neither through Hibernate nor Oracle SQL Developer editor, because I expect the records "primary key" to be inserted automatically through Hibernate as the table 'Employee_Project2' is mentioned in the annotation
TestEmployee:
#Entity #Table(schema = "afk_owner", name = "Test_Employee2")
public class TestEmployee {
#Id
#Column(name = "emp_id")
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequencegen")
#SequenceGenerator(name = "sequencegen", sequenceName = "afk_owner.Test_Employee_seq", allocationSize = 1)
private Long mEmpId;
#Column(name = "emp_name")
private String mEmpName;
#Column(name = "emp_experience")
private int mEmpExperience;
#ManyToMany(cascade = CascadeType.ALL, mappedBy = "employeesList")
private Set<TestProject> mProjectsList;
public Long getmEmpId() {
return mEmpId;
}
public void setmEmpId(Long mEmpId) {
this.mEmpId = mEmpId;
}
public String getmEmpName() {
return mEmpName;
}
public void setmEmpName(String mEmpName) {
this.mEmpName = mEmpName;
}
public int getmEmpExperience() {
return mEmpExperience;
}
public void setmEmpExperience(int mEmpExperience) {
this.mEmpExperience = mEmpExperience;
}
public Set<TestProject> getmProjectsList() {
return mProjectsList;
}
public void setmProjectsList(Set<TestProject> mProjectsList) {
this.mProjectsList = mProjectsList;
}
public TestEmployee(String empName, int empExperience) {
this.mEmpName = empName;
this.mEmpExperience = empExperience;
}
public TestEmployee() {
// TODO Auto-generated constructor stub
}
}
TestProject:
#Entity #Table(schema = "afk_owner", name = "Test_Project2")
public class TestProject {
#Id
#Column(name = "proj_id")
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequencegen")
#SequenceGenerator(name = "sequencegen", sequenceName = "afk_owner.Test_Project_seq", allocationSize = 1)
private Long mProjId;
#Column(name = "proj_name")
private String mProjName;
#Column(name = "proj_desc")
private String mProjDesc;
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(name = "Employee_Project2", joinColumns = #JoinColumn(name = "proj_id"), inverseJoinColumns = #JoinColumn(name = "emp_id"))
private Set<TestEmployee> employeesList;
public Long getmProjId() {
return mProjId;
}
public void setmProjId(Long mProjId) {
this.mProjId = mProjId;
}
public String getmProjName() {
return mProjName;
}
public void setmProjName(String mProjName) {
this.mProjName = mProjName;
}
public String getmProjDesc() {
return mProjDesc;
}
public void setmProjDesc(String mProjDesc) {
this.mProjDesc = mProjDesc;
}
public Set<TestEmployee> getEmployeesList() {
return employeesList;
}
public void setEmployeesList(Set<TestEmployee> employeesList) {
this.employeesList = employeesList;
}
public TestProject(String projName, String projDesc) {
this.mProjName = projName;
this.mProjDesc = projDesc;
}
public TestProject() {
// TODO Auto-generated constructor stub
}
}
records insertion
Session session = HibernateUtil.getCurrentSession();
Transaction transaction = session.beginTransaction();
/*empAhmad*/
TestEmployee empAhmad = new TestEmployee();
TestProject projRoadSteepnessEstimation = new TestProject();
TestProject projObjectTrackingUsingLIDAR = new TestProject();
TestProject projSalientRegionDetector = new TestProject();
TestProject projAutonomousNavigationUsingGNSSSensors = new TestProject();
Set<TestProject> empAhmadProjLists = new HashSet<>();
empAhmad.setmEmpName("Ahmad");
empAhmad.setmEmpExperience(9);
projRoadSteepnessEstimation.setmProjName("Road Steepness Est");
projRoadSteepnessEstimation.setmProjDesc("Kalman Filter, Java");
empAhmadProjLists.add(projRoadSteepnessEstimation);
projObjectTrackingUsingLIDAR.setmProjName("Object Tracking LIDAR");
projObjectTrackingUsingLIDAR.setmProjDesc("C++, OpenCV");
empAhmadProjLists.add(projObjectTrackingUsingLIDAR);
projSalientRegionDetector.setmProjName("Salient Region Detector");
projSalientRegionDetector.setmProjDesc("Java, OpenCV");
empAhmadProjLists.add(projSalientRegionDetector);
projAutonomousNavigationUsingGNSSSensors.setmProjName("Autonomous Navigation GNSS");
projAutonomousNavigationUsingGNSSSensors.setmProjDesc("Android, Kalman Filter");
empAhmadProjLists.add(projAutonomousNavigationUsingGNSSSensors);
empAhmad.setmProjectsList(empAhmadProjLists);
/*empAmr*/
TestEmployee empAmr = new TestEmployee();
TestProject projKalmanForOnlineEstimation = new TestProject();
TestProject projNonLinearControlAndFiltering = new TestProject();
TestProject projAppForHydrolicProcess = new TestProject();
Set<TestProject> empAmrProjList = new HashSet<>();
empAmr.setmEmpName("Amr");
empAmr.setmEmpExperience(5);
projKalmanForOnlineEstimation.setmProjName("Kalman For Online Estimation");
projKalmanForOnlineEstimation.setmProjDesc("Kalman Filter, Java, C++");
empAmrProjList.add(projKalmanForOnlineEstimation);
projNonLinearControlAndFiltering.setmProjName("Non-Linear Control And Filtering");
projNonLinearControlAndFiltering.setmProjDesc("C++, wavelet analysis");
empAmrProjList.add(projNonLinearControlAndFiltering);
projAppForHydrolicProcess.setmProjName("App For Hydrolic Process");
projAppForHydrolicProcess.setmProjDesc("Android, OpenCV, C++");
empAmrProjList.add(projAppForHydrolicProcess);
empAmr.setmProjectsList(empAmrProjList);
/*empAli*/
TestEmployee empAli = new TestEmployee();
Set<TestProject> empAliProjList = new HashSet<>();
empAli.setmEmpName("Ali");
empAli.setmEmpExperience(7);
empAliProjList.add(projAutonomousNavigationUsingGNSSSensors);
empAliProjList.add(projObjectTrackingUsingLIDAR);
empAliProjList.add(projKalmanForOnlineEstimation);
empAliProjList.add(projAppForHydrolicProcess);
empAli.setmProjectsList(empAliProjList);
session.persist(empAhmad);
session.persist(empAmr);
session.persist(empAli);
transaction.commit();
Because you never inserted anything in the owning side tof the association: Project.employeesList. As simple as that.
You only populated the inverse side of the association: Employee.mProjectsList, but Hibernate only cares about the owning side.