I am having trouble inserting records with a #OneToMany relationship using Hibernate and Spring MVC. I can successfully insert records without adding anything to the #OneToMany collection. However, upon adding a collection record, it fails stating that there is a SQLIntegrityConstraintViolationException.
My current code for the mapping (annotation-style) is as follows:
Contact.java
package mil.navy.navsupbsc.entity;
import java.util.Collection;
import java.util.LinkedHashSet;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.persistence.Transient;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
#Entity
#Table(name = "CONTACT2")
public class Contact extends Auditable {
public Contact() {
}
// Create with mandatory fields
public Contact(long id, Salutation salutation, String firstName,
String middleInitial, String lastName,
MilitaryCivilianInformation milCivInfo) {
this.setContactId(id);
this.setSalutation(salutation);
this.setFirstName(firstName);
this.setMiddleInitial(middleInitial);
this.setLastName(lastName);
this.setMilitaryCivilianInformation(milCivInfo);
}
/**
*
*/
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue
#SequenceGenerator(name = "CONTACT_SEQ")
#Column(name = "CONTACT_ID")
private Long contactId;
#Fetch(FetchMode.SUBSELECT)
#OneToMany(cascade = { CascadeType.ALL, CascadeType.REMOVE }, mappedBy = "contact", orphanRemoval = true)
private Collection<Email> emails = new LinkedHashSet<Email>();
/**
* #return the emails
*/
public Collection<Email> getEmails() {
for (Email email : emails) {
email.getEmailType();
}
return emails;
}
/**
* #param emails
* the emails to set
*/
public void setEmails(Collection<Email> emails) {
this.emails.clear();
for (Email email : emails) {
this.addEmail(email);
}
}
public void addEmail(Email email) {
email.setContact(this);
this.getEmails().add(email);
}
[...more Getters / Setters and fields]
}
Email.java
package mil.navy.navsupbsc.entity;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import org.codehaus.jackson.annotate.JsonBackReference;
/**
* Implements Auditing Properties
*/
#Entity
#Table(name = "EMAIL")
public class Email extends Auditable {
private static final long serialVersionUID = -4833322552325183301L;
#Id
#GeneratedValue
#SequenceGenerator(name = "EMAIL_SEQ")
#Column(name = "EMAIL_ID")
private long emailId;
#JsonBackReference
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "CONTACT_FK")
private Contact contact;
[More fields]
public Contact getContact() {
return contact;
}
public void setContact(Contact contact) {
this.contact = contact;
}
public long getEmailId() {
return emailId;
}
public void setEmailId(long emailId) {
this.emailId = emailId;
}
[more getters / setters]
}
ContactDAOImpl (I've tried many variations of this with no success)
public void saveContact(Contact contact) {
Session session = sessionFactory.getCurrentSession();
Collection<Email> emailCollection = new LinkedHashSet<Email>();
emailCollection = contact.getEmails();
Contact contactToSave;
long contactId;
if (contact.getContactId() == 0 || contact.getContactId() == null) {
contactToSave = new Contact((long) 0, contact.getSalutation(),
contact.getFirstName(), contact.getMiddleInitial(),
contact.getLastName(),
contact.getMilitaryCivilianInformation());
session.save(contactToSave);
session.flush();
for (Email email : emailCollection) {
// email.setContact(contactToSave);
contactToSave.addEmail(email);
}
session.saveOrUpdate(contactToSave);
session.flush();
session.clear();
}
Any help with this is much appreciated. I had a previous version of this that updated records correctly, but can't seem to work out the Save new records. I also originally used the contact that I passed in from the web service, but I attempted to create a new record in the DAO to eliminate potential problems in the latest variation of my code.
Also, I know that there are many similar questions, but I have tried many of the answers with no success (hence the new question).
Thank you for your help!
UPDATE
I checked to see what ID the data layer returns after the initial save and verified (unsuccessfully) that the same ID was saved in the database. The returned ID is different than the saved ID. For example, the latest save showed the Contact ID as '1129' with the returned contact after the initial save. I did a retrieve from the database with contact ID '1129' - and it successfully returned the contact. After closing the transaction, I viewed the data directly in the database. The database showed '193' as the Contact ID instead of '1129'. Any ideas??
I figured out the issue I was having.
Prior to creating the Hibernate entity definition, I created a sequence and trigger in the database directly. I also specified the same sequence in the entity definition within the JAVA hibernate code. However, my syntax for generating the sequence was not complete.
Since the syntax for generating the sequence was incomplete, the JAVA code was using the default HIBERNATE_SEQUENCE generator, rather than the sequence I created. Hibernate returned the sequence value from HIBERNATE_SEQUENCE (creating the ID prior to the database insert). However, immediately prior to inserting the value into the database, Oracle ran my custom sequence and assigned the Contact ID to the newly generated value.
Consequently, when I tried to add a value to the collection, the Insert statement attempted to use the sequence value generated by Hibernate as the foreign key, rather than the sequence value generated by the database.
In order to ensure that the Hibernate code and database were in sync, I removed the ContactID trigger from the database. Based on another question in StackOverflow (Hibernate and Oracle Sequence), I updated the JAVA Hibernate code for the Contact.java ContactID declaration to:
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "CONTACT_SEQ")
#SequenceGenerator(name = "CONTACT_SEQ", sequenceName = "contact_seq", allocationSize = 1)
#Column(name = "CONTACT_ID")
private Long contactId;
Thankfully this fixed the problem. The JAVA Hibernate code now creates the ID through the sequence I specified. The database then successfully inserts those values into the database!
I was also able to simplify my save to:
public void saveContact(Contact contact) {
Session session = sessionFactory.getCurrentSession();
if (contact.getContactId() == 0 || contact.getContactId() == null) {
session.save(contact);
session.flush();
}
}
I'm sure that I'll need to change it again though to account for updating the contact - probably using saveOrUpdate() instead of save().
Related
This question already has answers here:
Spring Data JPA Pageable with #ManyToMany
(2 answers)
Closed 1 year ago.
In my current project, I am using JPA and Hibernate to handle my database layer.
One of my entities has a Many-To-Many relation to itself, which works great using the #ManyToMany and the #JoinTable annotations.
My problem is that I am asked to add a pagination support to the entity. I have searched online for a solution but the closest thing I have found to a solution works on a different use case, where the relation is between 2 entities (and not an entity to itself).
This is the important parts from the entity's class:
package iob.data;
import iob.data.primarykeys.InstancePrimaryKey;
import iob.logic.exceptions.instance.InvalidBindingOperationException;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import java.util.Date;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
#NoArgsConstructor
#AllArgsConstructor
#Getter
#Setter
#RequiredArgsConstructor
#Entity
#Table(name = "INSTANCES")
#IdClass(InstancePrimaryKey.class)
public class InstanceEntity {
//<editor-fold desc="Primary key">
#Id
// The index would be generated from a sequence named "INSTANCES_SEQUENCE"
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "INSTANCES_SEQUENCE")
// Here we create that sequence, and define it to be updated every insertion (allocationSize = 1).
#SequenceGenerator(name = "INSTANCES_SEQUENCE", sequenceName = "INSTANCES_SEQUENCE", allocationSize = 1)
private long id;
#Id
#NonNull
private String domain;
//</editor-fold>
//<editor-fold desc="Many to Many relation">
// Define a new table that would store the references
#JoinTable(name = "INSTANCES_TO_INSTANCES",
// The main columns for this attribute are the parent's primary key which is constructed from domain and id
inverseJoinColumns = {#JoinColumn(name = "PARENT_ID", referencedColumnName = "id"),
#JoinColumn(name = "PARENT_DOMAIN", referencedColumnName = "domain")},
// The referenced columns for this attribute are the child's primary key which is also constructed from domain and id
joinColumns = {#JoinColumn(name = "CHILD_ID", referencedColumnName = "id"),
#JoinColumn(name = "CHILD_DOMAIN", referencedColumnName = "domain")})
// Declare the parent's side of the relation
#ManyToMany(fetch = FetchType.LAZY)
private Set<InstanceEntity> parentInstances = new HashSet<>();
// Declare the children's size of the relation, and define that it is related to the parentInstances
#ManyToMany(mappedBy = "parentInstances", fetch = FetchType.LAZY)
private Set<InstanceEntity> childInstances = new HashSet<>();
//</editor-fold>
public void addParent(InstanceEntity parent) {
if (this.equals(parent))
throw new InvalidBindingOperationException("Cannot assign parent to himself");
parentInstances.add(parent);
}
public void addChild(InstanceEntity child) {
if (this.equals(child))
throw new InvalidBindingOperationException("Cannot assign child to himself");
childInstances.add(child);
}
#Override
public int hashCode() {
return Objects.hash(id, domain);
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
InstanceEntity entity = (InstanceEntity) o;
return id == entity.id && domain.equals(entity.domain);
}
}
So far I tried (probably naively) write the getter for parentInstance and pass it a Pagable object (because it works on derived-queries, so worth a shot 😅) but obviously it did nothing.
The only solution I can think of is removing the relation, and creating it manually (create the table using another entity and add a new PagingAndSortingRepository that would retrieve the relevant instances and then convert them to InstanceEntity in the service).
So, how can I add a pagination support for parentInstances and childInstances?
I have found the solution in some unexpected post. It is not about pagination, but it still solved my problem.
So the solution in my case was to add the following function to my PagingAndSortingRepository interface:
Page<InstanceEntity> findDistinctByParentInstancesIn(#Param("parent_instances") Set<InstanceEntity> parentInstances, Pageable pageable);
And then calling it is:
InstanceEntity ie = new InstanceEntity();
ie.setDomain(parentDomain);
ie.setId(Long.parseLong(parentId));
instancesDao.findDistinctByParentInstancesIn(Collections.singleton(ie), PageRequest.of(page, size));
To be honest, I'm not sure how/why it works. IntelliJ IDEA gives me an error in the repository interface Expected parameter types: Collection<Set<InstanceEntity>>.
So if someone has an explanation, I would be glad to hear it.
I'm working in a web app with Hibernate 5 and SpringMVC, when I worked just with simple tables (without relationships between any tables) everything work okay, update, save, delete..., but if I try to add some relationships between tables, and when I mapped, I got this error :
#OneToOne or #ManyToOne on 'univ_orientation.dz.Entity.cursusGlobale.Student_idStudent' references an unknown entity: int
I had searched to solve this issue, I found out that its a configuration problem, Hibernate doesn't recognize class cursusGlobale as an entity.
My question is why Hibernate doesn't recognize class cursusGlobale as an entity when I work with relationships, but it does if I work just with simple tables without relationships between tables?
package univ_orientation.dz.Entity;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import javax.persistence.Table;
#Entity
#Table(name="cursusGlobale")
public class cursusGlobale {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Column(name="idcursusGlobale")
private int idcursusGlobale;
#Column(name="nbrRedoubler")
private int nbrRedoubler;
#Column(name="nbrDette")
private int nbrDette;
#Column(name="idStudent")
private int idStudent;
#OneToOne( cascade= CascadeType.ALL)
#JoinColumn(name="Student_idStudent")
private int Student_idStudent;
public cursusGlobale() {
}
public cursusGlobale(int nbrRedoubler, int nbrDette, int
student_idStudent) {
super();
this.nbrRedoubler = nbrRedoubler;
this.nbrDette = nbrDette;
idStudent = student_idStudent;
}
public int getIdcursusGlobale() {
return idcursusGlobale;
}
public void setIdcursusGlobale(int idcursusGlobale) {
this.idcursusGlobale = idcursusGlobale;
}
public int getNbrRedoubler() {
return nbrRedoubler;
}
public void setNbrRedoubler(int nbrRedoubler) {
this.nbrRedoubler = nbrRedoubler;
}
public int getNbrDette() {
return nbrDette;
}
public void setNbrDette(int nbrDette) {
this.nbrDette = nbrDette;
}
public int getIdStudent() {
return idStudent;
}
public void setIdStudent(int idStudent) {
this.idStudent = idStudent;
}
}
You have not create a relationship. You are using a java native type int instead of another class Student.
#OneToOne( cascade= CascadeType.ALL)
#JoinColumn(name="Student_idStudent")
private int Student_idStudent;
As the error message clearly says: #OneToOne or #ManyToOne on 'univ_orientation.dz.Entity.cursusGlobale.Student_idStudent' references an unknown entity: int, Student_idStudent is defined as an int, not as an Entity. You will need something more along the lines of
#OneToOne( cascade= CascadeType.ALL)
#JoinColumn(name="Student_idStudent")
private Student student;
Even though it is a class it will be persisted as a Foreign Key to the Student table in the database. This is part of the abstraction provided by JPA.
Reference: JPA Hibernate One-to-One relationship
I created two tables -student and subject.
Then I created a third table student_subject, which is a joint table to connect students with subjects. It has 5 fileds: row id, student id, subject id and the created_at and updated_at fields (which comes from the AuditModel class):
package com.rest.web.postgresapi.data.models;
import com.fasterxml.jackson.annotation.JsonIgnore;
import javax.persistence.*;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
#Entity
#Table(uniqueConstraints = {
#UniqueConstraint(columnNames = {"student_id", "subject_id"})
})
public class StudentSubject extends AuditModel {
#Id
#GeneratedValue(generator = "enrollment_generator")
#SequenceGenerator(
name = "enrollment_generator",
sequenceName = "enrollment_sequence",
initialValue = 4420
)
private Long id;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "student_id", nullable = false)
#OnDelete(action = OnDeleteAction.CASCADE)
#JsonIgnore
private Student student_id; // If I put private Long student_id, it fails
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "subject_id", nullable = false)
#OnDelete(action = OnDeleteAction.CASCADE)
#JsonIgnore
private Subject subject_id; // If I put private Long subject_id, it fails
// Constructors
protected StudentSubject() {}
public StudentSubject(Student student_id, Subject subject_id) {
this.student_id = student_id;
this.subject_id = subject_id;
}
// Getters
public Long getId() {
return id;
}
public Student getStudent_id() {
return student_id;
}
public Subject getSubject_id() {
return subject_id;
}
// Setters
public void setId(Long id) {
this.id = id;
}
public void setStudent_id(Student student) {
this.student_id = student;
}
public void setSubject_id(Subject subject) {
this.subject_id = subject;
}
}
The application perfectly creates the tables in the database and I can get and post in the student and subject tables. No problem with that. The pain comes with the controller for the joint table.
This is the controller for the student_subject joint table table
package com.rest.web.postgresapi.controllers;
import com.rest.web.postgresapi.data.models.StudentSubject;
import com.rest.web.postgresapi.repository.StudentSubjectRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.List;
#RestController
public class StudentSubjectController {
#Autowired
private StudentSubjectRepository studentSubjectRepository;
#GetMapping("/enrollments")
public List<StudentSubject> getAllStudentsSubjects(){
return studentSubjectRepository.findAll();
}
#PostMapping("/enrollments/student/subject")
public StudentSubject createStudentSubject(#Valid #RequestBody StudentSubject studentSubject) {
return studentSubjectRepository.save(studentSubject);
}
}
There are two problems:
1 .- when I do the get from the student_subject table, It only retrieves the id of the row and the created_at and updated_at fields. No student_id nor subject_id.
response from get
2 .- when I do the post (from postman) to insert a row, I got the following error:
Detail: Failing row contains (4671, 2018-11-20 11:04:34.176, 2018-11-20 11:04:34.176, null, null).
I provide both student_id and subject_id, as you can see at this screenshot from postman, but the error clearly states both fields are null:
postman post
It seems that my definition of the table is somehow wrong. What am I missing in my code?
Spring MVC uses Jackson to serialize/deserialize Java objects to/from JSON.
If you annotate an attribute with #JSONIgnore then Jackson will ignore it.
This is the reason why you don't see the student_id field or the subject_id field in your JSON response of the GET method. Because Jackson is ignoring them when converts from object to json.
And this is the reason why your POST fails. Because Jackson is ignoring the received attributes. Jackson is creating an empty entity and JPA is saving the entity without student_id and subject_id.
Solved by replacing
#JsonIgnore
with
#JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
as indicated in this answer
I am specifying my entity as follows
package com.drishti.training.dbentity;
import java.util.List;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.Table;
import com.drishti.dacx.core.framework.ameyoentitytypes.AbstractDBEntity;
/**
*
*/
#Entity
#Table(name = "template")
public class TemplateDBEntity extends AbstractDBEntity {
String template_name, organisationId;
#Column(name = "organisation_id", nullable = false)
public String getOrganisationId() {
return organisationId;
}
public void setOrganisationId(String organisationId) {
this.organisationId = organisationId;
}
private String templateId;
// private List<Integer> listOfTrainingIds;
private List<String> listOfTrainingIds;
#Id
#Column(name = "template_id", nullable = false)
public String getTemplateId() {
return templateId;
}
public void setTemplateId(String templateId) {
this.templateId = templateId;
}
#ElementCollection(targetClass = String.class)
#CollectionTable(name = "template_id_vs_training_id", joinColumns = #JoinColumn(name = "template_id"))
#Column(name = "training_id", nullable = false)
public List<String> getListOfTrainingIds() {
return listOfTrainingIds;
}
public void setListOfTrainingIds(List<String> listOfTrainingIds) {
this.listOfTrainingIds = listOfTrainingIds;
}
#Column(name = "template_name")
public String getName() {
return template_name;
}
public void setName(String name) {
this.template_name = name;
}
}
and
another table is
create table template_id_vs_training_id
(
template_id varchar references template(template_id) on delete cascade,
training_id varchar references training(training_id) on delete cascade,
PRIMARY KEY (template_id,training_id)
);
but when i load the TemplateDBEntity it provides me the above reported error.
LazyInitializationException, as hibernate documentation says:
Indicates an attempt to access not-yet-fetched data outside of a
session context. For example, when an uninitialized proxy or
collection is accessed after the session was closed
The only cause of this exception is listOfTrainingIds property as it's an
#ElementCollection which is Lazy loaded by default, so either :
Make sure that you're accessing listOfTrainingIds property inside a transaction (Your entity can use available session to fetch your collection).
Or make it eager #ElementCollection(fetch = FetchType.EAGER), but be aware that every time your select the entity from the database, your collection will be loaded eagerly as well, even if you don't need it, which can impact performance.
Or use fetch keyword in your hibernate Query (if you're using query to load your entity):
List<TemplateDBEntity> TemplateDBEntitys = session.createQuery(
"select t from TemplateDBEntity t join fetch t.listOfTrainingIds",
TemplateDBEntity.class).getResultList();
Or use #FetchProfile.
// In class...
#FetchProfile(
name = "withListOfTrainingIds",
fetchOverrides = {#FetchProfile.FetchOverride(mode = FetchMode.JOIN, association = "listOfTrainingIds", entity = TemplateDBEntity.class)})
public class TemplateDBEntity extends AbstractDBEntity {
//...
}
// To get your entity
session.enableFetchProfile("withListOfTrainingIds");
System.out.println(session.get(TemplateDBEntity.class, templateId));
session.disableFetchProfile("withListOfTrainingIds");
I prefer the last two options, as hibernate will perform one query to database, even you keep the collection lazy loaded, which is better for performance.
I'm having troubles in creating a custom query within spring, because my Entity contains an "_" character in it's parameter's name: "game_date".
My table has a column named "game_date" as well.
I have created following method:
List<Games> findByGame_dateAndOpponent(#Param("game_date") Date game_date, #Param("opponent") String opponent);
but when I start my app, it's crashing with exception of kind: "org.springframework.data.mapping.PropertyReferenceException: No property gamedate found for type Games!". After changing a parameter name to the "gameDate" both in Entity and Query method, it stopped complaining, and is actually returning expected entries. But at the same time, it doesn't return values from the column "game_date", in the search queries, which is a simple regular column of a Date type. I have no idea what's going on with all this thing.
DB I'm using is MySql.
Here comes the code itself:
Entity:
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
#Entity
#Table(name = "games")
public class Games {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id_game")
private int id;
#Column(name = "game_date", columnDefinition = "DATE")
#Temporal(TemporalType.DATE)
private Date gameDate;
public Date getGame_date() {
return gameDate;
}
public void setGame_date(Date _game_date) {
this.gameDate = _game_date;
}
}
And a repository:
import java.sql.Date;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
#RepositoryRestResource
public interface GamesRepository extends CrudRepository< Games , Integer > {
List< Games > findById( #Param( "id" ) int id );
List< Games > findAll( );
List<Games> findByGameDateAndOpponent(#Param("game_date") Date game_date, #Param("opponent") String opponent);
}
The underscore is a reserved keyword in Spring Data JPA. It should be enough to remove it from your property and from its getters and setters and Hibernate will do the rest:
#Entity
#Table(name = "games")
public class Games {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id_game")
private int id;
//Getter and setters for id
#Column(name = "game_date")
private Date gameDate;
public Date getGameDate() {
return gameDate;
}
public void setGameDate(Date gameDate) {
this.gameDate = gameDate;
}
}
Also, in general, try to use java naming convention for variable and field names, which is mixed case with lowercase first.
See also:
Spring Data JPA repository methods don't recognize property names with underscores