Hibernate exception: detached entity passed to persist, in one-direction ManyoOne relationship - spring

I am new to Hibernate and I encountered the classic "detached entity passed to persist" exception. I read a few similar questions here but non of them can apply to my situation.
I have 2 entities, Department and DeptEmpCode. Department has a foreign key that references DeptEmpCode. Many Departments may share one code, So it is many to one.
The entity code is as follows:
Department:
#Entity
#Table(name = "department")
public class Department {
private Integer id;
...
private DeptEmpCode status;
...
#ManyToOne(cascade=CascadeType.ALL)
#JoinColumn(name = "status")
public DeptEmpCode getStatus() {
return status;
}
public void setStatus(DeptEmpCode status) {
this.status = status;
}
}
DeptEmpCode:
#Entity
#Table(name = "code")
public class DeptEmpCode {
private Integer id;
....
}
I have omitted unnecessary code for easier read.
So when I hit the "add Department" button on the webpage, spring framework creates a Department object for me. Then I use "new" to create a DeptEmpCode object, and call Department's setStatus() to associate the DeptEmpCode object to the Department. Then I persist the Department. The code is as follows:
public void saveDept(Department dept) {
if(dept.getStatus() == null){
DeptEmpCode status = new DeptEmpCode();
status.setId(Constants.DEFAUL_DEPT_STATUS_ID);
dept.setStatus(status);
}
deptDAO.save(dept); //nothing but a persist() call
}
So what should be the problem? Should I make it bi-directional or its something else?(If I remove "cascadeType.ALL" then it would be a foreign key violation). Thanks!

since your Department object is already created. Use cascadeType.MERGE instead of cascadeType.ALL
When you use cascadeType.ALL, it will think the transaction is PERSISTED, it tries to PERSIST Department as well and that doesn't work since Department already is in the db. But with CascadeType.MERGE the Department is automatically merged(update) instead.
Update:
Service
#Override
#Transactional
public void saveDept(Department dept) {
if(dept.getStatus() == null){
DeptEmpCode status = new DeptEmpCode();
status.setId(Constants.DEFAUL_DEPT_STATUS_ID);
status.setType("DEFAULT");
status.setValue("DEFAULT");
dept.setStatus(status);
}
deptDAO.update(dept);
}
DAO
#Override
public void update(Department dept) {
em.merge(dept);
}
Explanation:
since status.id is a primary key and you set the value by yourself. When you call save it will create the status first, so it means the status has already (detached object) register in the database(not yet commit), if you still use em.persist(dept), it will also try to persist the detach object (status) and the department. so we should use merge which will merge the status and insert a new department.
you can see below is how hibernate insert your record.
however, if you don't set the status.id value and let it auto-generate, then it will persist department and status at the same time. so you won't have the problem. In your case, I know you want to assign the status.id to default which is one. So you should use merge as you will have the 2nd and 3rd department that use the default status id (which status id already in db).
Hibernate: insert into code (DES, INACTIVE_IND, CODE_TYPE, VALUE) values (?, ?, ?, ?)
Hibernate: insert into department (contact, des, dept_email, dept_name, status) values (?, ?, ?, ?, ?)

Related

Is there a way to have custom SQL query on top of JPA repository to have BULK UPSERTS?

I have a snowflake database and it doesn't support unique constraint enforcement (https://docs.snowflake.com/en/sql-reference/constraints-overview.html).
I'm planning to have a method on JPA repository with a custom SQL query to check for duplicates before inserting to the table.
Entity
#Entity
#Table(name = "STUDENTS")
public class Students {
#Id
#Column(name = "ID", columnDefinition = "serial")
#GenericGenerator(name = "id_generator", strategy = "increment")
#GeneratedValue(generator = "id_generator")
private Long id;
#Column(name = "NAME")
private String studentName;
}
Snowflake create table query
CREATE table STUDENTS(
id int identity(1,1) primary key,
name VARCHAR NOT NULL,
UNIQUE(name)
);
Repository
public interface StudentRepository extends JpaRepository<Students, Long> {
//
#Query(value = "???", nativeQuery = true)
List<Student> bulkUpsertStudents(List<Student> students);
}
You can use a SELECT query to check for duplicate values in the name column before inserting a new record into the table. For example:
#Query(value = "SELECT * FROM STUDENTS WHERE name = :name", nativeQuery = true)
List<Student> findByName(#Param("name") String name);
This method will return a list of Student records with the specified name value. If the list is empty, it means that there are no records with that name value, and you can safely insert a new record with that name value.
List<Student> studentList = new ArrayList<>();
for (Student student : students) {
List<Student> existingStudents = studentRepository.findByName(student.getName());
if (existingStudents.isEmpty()) {
studentsToInsert.add(student);
}
}
studentRepository.bulkUpsertStudents(studentList)
EDIT
If the above solution doesn't work. You can use the MERGE statement to update existing records in the table if the data has changed. For example, if you want to update the name of a Student if it has changed, you can use the following MERGE statement:
#Query(value = "MERGE INTO students t USING (SELECT :name AS name, :newName AS newName) s
ON t.name = s.name
WHEN MATCHED AND t.name <> s.newName THEN UPDATE SET t.name = s.newName
WHEN NOT MATCHED THEN INSERT (name) VALUES (s.name)", nativeQuery = true)
List<Student> bulkUpsertStudents(List<Student> students);
This query will update the name of each Student in the students list if it has changed, and if a conflict occurs, it will not insert a new record. This will ensure that only unique name values are inserted into the table, without having to perform a separate query for each record.
I was able to overcome this using the below approach but need to verify the performance of the queries.
Repository saveAll() method to save all the entities.
Using the custom nativeQuery as below
INSERT OVERWRITE INTO STUDENTS
WITH CTE AS(SELECT ROW_NUMBER() OVER (PARTITION BY NAME ORDER BY ID) AS RNO, ID, NAME FROM STUDENTS)
SELECT ID, NAME FROM CTE WHERE RNO = 1;
Example code :
import static io.vavr.collection.List.ofAll;
import static io.vavr.control.Option.of;
import static java.util.function.Predicate.not;
public Validation<ValidationError, List<Students>> saveAll(List<String> students) {
return of(students)
.filter(not(List::isEmpty))
.map(this::mapToEntities) // maps the list to list of database entities
.map(repository::saveAll) // save all
.toValidation(ERROR_SAVING_STUDENTS) // vavr validation in case of error
.peek(x -> repository.purgeStudents()) // purging to remove duplicates
.toValidation(ERROR_PURGING_STUDENTS);
}
This issue is only due to snowflake's incapability to check uniqueness.

Spring Boot - relationship deleted on save() method

My problem is this: There is a many to many relationship between two tables - Project and Employee. There is an option to update a given employee, but there is a little problem. After updating the employee, hibernate automatically deletes the employee's record from the connected project_employee table.
Hibernate: update employee set email=?, first_name=?, last_name=? where employee_id=?
And this happens right after that
Hibernate: delete from project_employee where employee_id=?
I'm following a course and I've just noticed this error. Source code of the lecturer is here:
https://github.com/imtiazahmad007/spring-framework-course
I've checked your github page:
#ManyToMany(cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.REFRESH, CascadeType.PERSIST},
fetch = FetchType.LAZY)
#JoinTable(name="project_employee",
joinColumns=#JoinColumn(name="employee_id"),
inverseJoinColumns= #JoinColumn(name="project_id")
)
#JsonIgnore
private List<Project> projects;
CascadeType.MERGE + CascadeType.PERSIST mean, that if Employee entity is saved, Project entity references must be saved.
In may-to-many cases it means:
DELETE by foreign key
Bulk insert
In case there's no bulk insert, there's an issue with persisntence context (your are saving an entity with empty collection of projects).
Possible solutions:
Remove CascadeType.MERGE + CascadeType.PERSIST if you do not want to change projects every time your save Employee. You can still save the ccollection via Repository
Make sure collection is attached on save action. That will cause Delete+Insert, but the resut will be ok.
Change Many-To-Many to One-To-Many with EmbeddedId
Please, refer to documentation:
When an entity is removed from the #ManyToMany collection, Hibernate simply deletes the joining record in the link table. Unfortunately, this operation requires removing all entries associated with a given parent and recreating the ones that are listed in the current running persistent context.
https://docs.jboss.org/hibernate/orm/5.6/userguide/html_single/Hibernate_User_Guide.html#associations-many-to-many
*** Update from dialog below to make cascade clear.
Say, you have two entities A & B (getters and setters omitted). + repos
#Entity
#Table(name = "a")
public class A {
#Id
private Integer id;
private String name;
#ManyToMany(cascade = {CascadeType.ALL})
#JoinTable(name="a_b",
joinColumns=#JoinColumn(name="a_id"),
inverseJoinColumns= #JoinColumn(name="b_id")
)
private List<B> bs;
}
#Entity
#Table(name = "b")
public class B {
#Id
private Integer id;
private String name;
}
You sample test looks like this:
#Test
public void testSave() {
B b = new B();
b.setId(1);
b.setName("b");
b = bRepository.save(b);
A a = new A();
a.setId(1);
a.setName("a");
a.setBs(Collections.singletonList(b));
aRepository.save(a);
a.setName("new");
service.save(a); //watch sevice implementations below
}
Version1:
#Transactional
public void save(A a) {
aRepository.save(a);
}
Hibernate logs are the following:
Hibernate:
update
a
set
name=?
where
id=?
Hibernate:
delete
from
a_b
where
a_id=?
Hibernate:
insert
into
a_b
(a_id, b_id)
values
(?, ?)
delete+bulk insert present (despite the fact, that B-s where not in fact changed)
Version2:
#Transactional
public void save(A a) {
Optional<A> existing = aRepository.findById(a.getId());
if (existing.isPresent()) {
a.setBs(existing.get().getBs());
}
aRepository.save(a);
}
Logs:
update
a
set
name=?
where
id=?
Here b-collection was forcibly re-attached, so hibernate understands, that it's not needed to be cascaded.

Select self referencing table with spring data into new object

I have a self referencing employee table. An Employee reports to his immediate lead which also an Employee.
fields->
id, name, employee_type, lead_id
I have mapped this table into this class,
public Class Employee {
private Integer id;
private String name;
private Integer employeeType; // 1-manager, 2-project lead, 3-developer, etc
private List<Employee> reporters;
}
How can I load all managers with his reporters using Spring Data JPA custom mapping? (those reporting employees will have their own reporters)
Mainly, I don't know how to map the corresponding list.
#Query("SELECT new Employee(id, name, employee_type) FROM employee")
List<Employee> findAllManagers();

JPA not returning the records which contain empty columns in the EmbeddedId

JpaRepository's findAll() method does not return the rows, if any of the field in the composite key is null.
This is the entity class with the EmbeddedId JobVaccinationPK
/**
* ApplicationParam entity. #author MyEclipse Persistence Tools
*/
#Entity
#Table(name="job_vaccination",schema="cdcis")
#SuppressWarnings("serial")
public class JobVaccination implements java.io.Serializable {
// Fields
#Column(name="default_yn", length=1)
private String defaultYn;
#EmbeddedId
private JobVaccinationPK jobVaccinationPK;
public JobVaccination(){
}
//setters getters
}
This is the Embedded class
#Embeddable
#SuppressWarnings("serial")
public class JobVaccinationPK implements Serializable{
#ManyToOne
#MapsId("job_category_id")
#JoinColumn(name = "job_category_id", nullable=true)
private JobCategoryTypeMast jobCategoryMast;
#ManyToOne
#MapsId("vaccination_id")
#JoinColumn(name = "vaccination_id", nullable=true)
private VaccinationMast vaccinationMast;
#ManyToOne
#MapsId("screening_type_id")
#JoinColumn(name = "screening_type_id", nullable=true)
private ScreeningTypeMast screeningTypeMast;
//getters and setters
}
Service implementation class
#Override
public SearchResult<JobVaccinationDto> getJobVaccination(JobVaccinationDto dto)
throws VaccinationException {
List<JobVaccination> vaccDetails = jobVaccinationRepo.findAll();
if(vaccDetails == null) return null;
List<JobVaccinationDto> jobVaccinationDtos = new ArrayList<JobVaccinationDto>();
jobVaccinationDtos = convertToDto(vaccDetails);
return new SearchResult<>(jobVaccinationDtos.size(), jobVaccinationDtos);
}
Here am able to insert a null value for either jobCategoryId or screeningTypeId, just like below row. But when I'm trying to fetch the rows which have empty values, it returns null. I've tried to debug but I was not able to find the cause.
This is the generated hibernate query:
Hibernate:
select
jobvaccina0_.job_category_id as job_cate4_13_,
jobvaccina0_.screening_type_id as screenin2_13_,
jobvaccina0_.vaccination_id as vaccinat3_13_,
jobvaccina0_.default_yn as default_1_13_
from
cdcis.job_vaccination jobvaccina0_
Hibernate:
select
jobcategor0_.job_category_id as job_cate1_11_0_,
jobcategor0_.job_category_name as job_cate2_11_0_,
jobcategor0_.job_category_name_ar as job_cate3_11_0_,
jobcategor0_.screening_type_id as screenin4_11_0_
from
cdcis.job_category_mast jobcategor0_
where
jobcategor0_.job_category_id=?
Hibernate:
select
screeningt0_.screening_type_id as screenin1_21_0_,
screeningt0_.active_yn as active_y2_21_0_,
screeningt0_.mmpid_required_yn as mmpid_re3_21_0_,
screeningt0_.screening_type as screenin4_21_0_
from
cdcis.screening_type_mast screeningt0_
where
screeningt0_.screening_type_id=?
Hibernate:
select
vaccinatio0_.vaccination_id as vaccinat1_27_0_,
vaccinatio0_.vaccination_name as vaccinat2_27_0_,
vaccinatio0_.vaccination_name_ar as vaccinat3_27_0_
from
cdcis.vaccination_mast vaccinatio0_
where
vaccinatio0_.vaccination_id=?
Going with #Adam Michalik answer. As a work-around I've introduced a new primary key field in the table, as we can't handle a null in the composite key.
Composite IDs cannot contain null values in any of the fields. Since the SQL semantics of NULL is that NULL <> NULL, it cannot be determined that a primary key (1, 2, NULL) is equal to (1, 2, NULL).
NULL means "no value" in SQL and its interpretation is up to you on a case-by-case basis. That's why SQL and JPA do not want to make assumptions that NULL = NULL and that a primary key containing a NULL identifies a single entity only.
You may choose to use a synthetic, generated primary key instead of the composite business primary key to overcome that. Then, you'd always have a non-null, single-column PK and nullable foreign keys.
change the data type of particular row in entity from int to integer

Using oneToMany relation, but saving data in individual tables at different point of time

I am working on a Spring-MVC application which has 2 tables in database and 2 domain classes. Class Person has oneTOMany relation with class Notes. I would like to add Person and notes both in database. So I googled, to find out many MVC based examples for the same problem. However they seem to assume a few things :
Data is being added in a static manner by the developer, mostly through Static void main() or another class.
Data regarding all the classes which are related is added altogether, eg : Table A has oneToMany relation, so the code will add data for both the tables in one class or one jsp file.
Other frameworks like Spring-Security at play(This point is understood).
So basically, similar examples with different names and developers is what I found. My problem is :
I don't have static void main, don't intend to use it.
I am adding data through HTML page wrapped inside JSP page.
I or the user will first register through the register form, just login later and then add notes, so I am not adding data for both tables at same time. (I have to believe this is possible by Hibernate)
Error :
org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.journaldev.spring.model.Person
org.hibernate.engine.internal.ForeignKeys.getEntityIdentifierIfNotUnsaved(ForeignKeys.java:294)
org.hibernate.type.EntityType.getIdentifier(EntityType.java:537)
org.hibernate.type.ManyToOneType.isDirty(ManyToOneType.java:311)
org.hibernate.type.ManyToOneType.isDirty(ManyToOneType.java:321)
org.hibernate.type.TypeHelper.findDirty(TypeHelper.java:294)
Person Model :
#Entity
#Table(name="person")
public class Person implements UserDetails{
private static final GrantedAuthority USER_AUTH = new SimpleGrantedAuthority("ROLE_USER");
#Id
#Column(name="id")
#GeneratedValue(strategy = GenerationType.SEQUENCE,generator = "person_seq_gen")
#SequenceGenerator(name = "person_seq_gen",sequenceName = "person_seq")
private int id;
#OneToMany(cascade = CascadeType.ALL,mappedBy = "person1")
private Set<Notes> notes1;
public Set<Notes> getNotes1() {
return notes1;
}
public void setNotes1(Set<Notes> notes1) {
this.notes1 = notes1;
}
Notes model :
#Entity
#Table(name="note")
public class Notes {
#Id
#Column(name="noteid")
#GeneratedValue(strategy = GenerationType.SEQUENCE,generator = "note_gen")
#SequenceGenerator(name = "note_gen",sequenceName = "note_seq")
private int noteId;
#ManyToOne
#JoinColumn(name = "id")
private Person person1;
public Person getPerson1() {
return person1;
}
public void setPerson1(Person person1) {
this.person1 = person1;
}
NotesDAOImpl :
#Transactional
#Repository
public class NotesDAOImpl implements NotesDAO{
private SessionFactory sessionFactory;
public void setSessionFactory(SessionFactory sf){
this.sessionFactory = sf;
}
#Override
public void addNote(Notes notes, int id) {
Session session = this.sessionFactory.getCurrentSession();
session.save(notes);
}
SQL schema :
CREATE TABLE public.person (
id INTEGER NOT NULL,
firstname VARCHAR,
username VARCHAR,
password VARCHAR,
CONSTRAINT personid PRIMARY KEY (id)
);
CREATE TABLE public.note (
noteid INTEGER NOT NULL,
sectionid INTEGER,
canvasid INTEGER,
text VARCHAR,
notecolor VARCHAR,
noteheadline VARCHAR,
id INTEGER NOT NULL,
CONSTRAINT noteid PRIMARY KEY (noteid)
);
ALTER TABLE public.note ADD CONSTRAINT user_note_fk
FOREIGN KEY (id)
REFERENCES public.person (id)
ON DELETE NO ACTION
ON UPDATE NO ACTION
NOT DEFERRABLE;
Btw, the id in addNote method is just me checking if SpringSecurity is actually sending userid, and has properly loggedin, debug purpose.
So, I am unable to add notes once user is logged in, what am I doing wrong? Or this is not possible with Hibernate. In that case, let me find a gun to shoot myself.. :P
Your code will try to save notes. But these notes will not be linked to any Person. You have to do below sequence of operation.
Find the logged in person or the person for which you want to save the notes.
Create notes object which will be in transient state.
Attach notes to the person.
If it is bidirectional relationaship, then person to notes.
Below is the code template.
#Transactional
#Repository
public class NotesDAOImpl implements NotesDAO{
private SessionFactory sessionFactory;
public void setSessionFactory(SessionFactory sf){
this.sessionFactory = sf;
}
#Override
public void addNote(Notes notes, int id) {
Session session = this.sessionFactory.getCurrentSession();
Person person = getPerson(); // this method should get logged in person or the person for whom you want to save the notes.
if (person.getNotes() == null) {
Set<Note> notes = new HashSet<Note>();
person.setNotes(notes);
}
person.getNotes().add(note);
note.setPerson(person); // If bidirectional relationship.
session.update(person); // if update does not work, try merge();
}
Also make sure you have cascade type set to MERGE in person entity on notes field.
Note: Above code is just example from your code and may have some compilation error. please correct according to your requirement.

Resources