JPA , many-to-many relation, deleting all previous relations and entering the new relations
Asked Answered
Z

5

8

here i am trying out many-to-many relationship in JPA, I'v created tables "tblcourse" and "tblStudent", a student can register to many courses,

create table tblcourse(
    id integer primary key,
    name varchar(100),
    duration integer
);

create table tblcourseStudent(
    studentid integer references tblstudent(studentId),
    courseId integer references tblcourse(id),
    constraint pk_composit_cs primary key(studentid,courseId)
)

Create table tblStudent(
    studentId integer primary key,
    ……..
    ….
);

The JPA representation of the above relation is as follows, this is the code for StudentEntity.java,

@Entity
@Table(name="TBLSTUDENT")
public class StudentEntity implements Serializable{

private static final long serialVersionUID = 100034222342L;

@Id
@Column(name="STUDENTID")
private Integer studentId;

@Column(name="STUDENTNAME")
private String studentName;

@Column(name="CONTACTNO")
private String contactNumber;

@Embedded
private StudentAddress address;

@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name="DEPTID")
private DeptEntity deptEntity;

@ManyToMany(fetch=FetchType.LAZY)
@JoinTable(name="tblcourseStudent",
            joinColumns=@JoinColumn(name="studentid"),
            inverseJoinColumns=@JoinColumn(name="courseId"))
    private List<CourseEntity> courseList;  
....
.....
.....
}

this the code for CourseEntity.java,

@Entity
@Table(name="TBLCOURSE")
public class CourseEntity implements Serializable{

        public CourseEntity(){

        }

    public CourseEntity(Integer courseId,String courseName,Integer courseDuration){
        this.courseId = courseId;
        this.courseName = courseName;
        this.courseDuration = courseDuration;
    }

    /**
     * 
     */
    private static final long serialVersionUID = -2192479237310864341L;

    @Id
    @Column(name="ID")
    private Integer courseId;

    @Column(name="NAME")
    private String courseName;

    @Column(name="DURATION")
    private Integer courseDuration;

    @ManyToMany(fetch=FetchType.LAZY)
    @JoinTable(name="tblcourseStudent",
                joinColumns=@JoinColumn(name="courseId"),
                inverseJoinColumns=@JoinColumn(name="studentid"))
    private List<StudentEntity> studentList;
    .........
}

Now, when i try to insert courses throught StudentEntity.java, the SQL queries fired at backend are

delete 
    from
        tblcourseStudent 
    where
        studentid=?

insert 
    into
        tblcourseStudent
        (studentid, courseId) 
    values
        (?, ?)

insert 
    into
        tblcourseStudent
        (studentid, courseId) 
    values
        (?, ?)

And, when i try to insert students throught CourseEntity.java, the SQL queries fired are as follows,

delete 
    from
        tblcourseStudent 
    where
        courseId=?

insert 
    into
        tblcourseStudent
        (courseId, studentid) 
    values
        (?, ?)  

in both of my case, the records are deleted and than the new mapping is inserted. So if i am inserting Courses for a student, first all the previouse courses for the student will be deleted from the third table, and the new courses will be entered,

So, my question is, if i don't want to delete old courses and add the new courses for the student how can i achieve, i.e i want to retain the old relationship,

Weather i have to achieve this programatically, or i have change the annotation, Waiting for the reply

This the code written in StudentServiceBean.java and the method "mapStudentToCourses" gets called when we map a single student to multiple Courses

@Stateless
@TransactionManagement(TransactionManagementType.CONTAINER)
public class StudentServiceBean implements StudentService{


@PersistenceContext(unitName="forPractise")
private EntityManager entityMgr;

@Resource
private SessionContext sessionContext;

@EJB
private DeptService deptService;
..........
......
...

@Override
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void mapStudentToCourses(Integer studentId,String courseIdList) throws Exception{
    List<CourseEntity> courseList = null;
    StudentEntity studentEntity  = null;
    TypedQuery<CourseEntity> courseQuery = null;        
    String query = "select c from CourseEntity c where c.courseId in ("+courseIdList+")";
    try{
        courseQuery = entityMgr.createQuery(query,CourseEntity.class);
        courseList =  courseQuery.getResultList();
        studentEntity = entityMgr.find(StudentEntity.class, studentId);
        studentEntity.setCourseList(courseList);
        entityMgr.merge(studentEntity);        
    }catch(Exception e){
        sessionContext.setRollbackOnly();
        throw e;
    }
}

This is the code when one Course is mapped to multiple students, its CourseServiceBean.java

@Stateless
@TransactionManagement(TransactionManagementType.CONTAINER)
public class CourseServiceBean implements CourseService{

@PersistenceContext(name="forPractise")
private EntityManager em;

@Resource
private SessionContext sessionCtx;

private Map<Integer, String> durationCode = null;

@EJB
private StudentService studentService;
........
......
...

@Override
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void mapCourseToStudents(Integer courseId,String studentIdList) throws Exception{
    List<StudentEntity> studentEntityList = null;
    TypedQuery<StudentEntity> studentQuery = null;
    String query = "select s from StudentEntity s where s.studentId IN ("+studentIdList+")";
    CourseEntity courseEntity = null;
    try{
        studentQuery = em.createQuery(query, StudentEntity.class);
        studentEntityList = studentQuery.getResultList();
        courseEntity = em.find(CourseEntity.class,courseId);
        courseEntity.setStudentList(studentEntityList);
        em.merge(courseEntity);
    }catch(Exception e){
        sessionCtx.setRollbackOnly();
        throw e;
    }
}
}    

this my persistence.xml file,

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
             version="2.0">
    <persistence-unit name="forPractise" transaction-type="JTA">
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <jta-data-source>jdbc/app</jta-data-source>
        <class>com.entity.StudentEntity</class>
        <class>com.entity.DeptEntity</class>
        <class>com.entity.CourseEntity</class>      
        <properties>
            <property name="hibernate.dialect"  value="org.hibernate.dialect.DerbyDialect"  />
            <property name="hibernate.show_sql" value="true" />
            <property name="hibernate.format_sql" value="true" />                           
        </properties>
    </persistence-unit>
</persistence>

waiting for the reply....

Zubkoff answered 25/4, 2012 at 14:51 Comment(4)
Can you post the config file persistence.xml?Checkerboard
Can you post the code you're running to make the insertions?Redraft
Added the code for save method, and also added persistence.xmlZubkoff
Possible duplicate of JPA update many-to-many deleting recordsGas
R
3

I might be wrong but I think this is normal that when you're making your insertions, Hibernate firsts deletes all the records from the associated table.

That's the reason : when working with x-to-many association (basically, associations that are mapped through a Collection), Hibernate's persistence context will perfom the dirty checkings based on the Collection's identifier.

Let's take the mapCourseToStudents() method from your CourseServiceBean class :

    ...
    studentQuery = em.createQuery(query, StudentEntity.class);
    studentEntityList = studentQuery.getResultList();
    courseEntity = em.find(CourseEntity.class,courseId);
    courseEntity.setStudentList(studentEntityList); // replacing the previous Collection by the one you retrieved by querying the DB !!! 
    em.merge(courseEntity);
    ...

If you really want to avoid Hibernate executing the delete statement first, you should add/remove items to the Collection instead of assigning a new Collection and configure in the mapping data the operations that you want to cascade.

Redraft answered 26/4, 2012 at 21:48 Comment(0)
H
0

Add new courses to the existing list:

Collection<Student> moreStudents = ...

course = em.find(CourseEntity.class,courseId);
course.getStudentList().addAll(moreStudents);
Horvitz answered 26/4, 2012 at 22:1 Comment(1)
(Regarding naming, consider removing unnecessary clutter like *List, *Entity, etc in your class and variable names. Eg: Student, Course, Participation, course.getStudents(). Less to write and read :-)Horvitz
P
0

I didn't overwrite the whole relationship list, and instead I add new entity to the original list. But, Hibernate still delete all my previous relationships.

According to this article: https://vladmihalcea.com/the-best-way-to-use-the-manytomany-annotation-with-jpa-and-hibernate/

This seems to be the current behaviour of Hibernate, and if we don't want that, we have to first correctly implement our entity's hashCode() and equals() methods, and use Set to model the ManyToMany relationship.

Parang answered 11/12, 2017 at 10:58 Comment(0)
B
0

I faced similar challange . I am providing the similar kind of scenario hope it may be helpful to some people.

i am taking Product and Order to explain the issue and using uni-directional manyToMany mapping

@Entity
@Table(name="Product")
public class Product {

    @Id
    @Column(name = "id")
    private Long id;

    
    @Column(name = "product")
    private String product;

    @ManyToMany(cascade = CascadeType.PERSIST,fetch= FetchType.EAGER)
    @JoinTable(name = "product_order_mapping", joinColumns = { @JoinColumn(name = "product_id") }, inverseJoinColumns = {
            @JoinColumn(name = "order_id") })
    @JsonProperty("orders")
    private Set<Order> orders =new HashSet<Order>();

    //setters and getters


}

@Table(name="Order")
public class Order {

    @Id
    @Column(name = "id")
    private Long id;
        
    @Column(name = "order")
    private String order;

    //setters and getters
    //equals and hashcode

}

Implementation Logic

productList and OrderFunctions are some list with list of products and orders details

for (Product pEntity : productList) {

OrderFunctions = this.getAllLineFunctions(product.getId(), cmEntity.getId())
                        .getBody();
Set<Order> oList = new HashSet<>();

for (OrderFunction orderFn : OrderFunctions) {
Order orderEntity = new Order();
orderEntity.setId(lfs.getOrderFunction_id());
orderEntity.setStageGroup(lfs.getOrderFunctionOrder());
oList.add(sgEntity);
}

orderRepository.saveAll(oList);
pEntity.setOrders(oList);
productRepository.save(pEntity);
}
}

To understand duplicacy mapping let us take an example

Suppose Product is under a Company Now a Company has multiple Product And Product has Multiple Order

Now Let us take a practical example for storing data in tables

set1--> company with id c1 has Product with id p1,p2,p3 product p1 has order o1,o2,o3 product p2 has order o1,o2 product p3 has order o2,o3,o4

set2--> company with id c2 has Product with id p1,p3 product p1 has order o1,o2 product p3 has order o2

Now when save set1 table product_order_mapping looks like

product_id  order_id
---------------------
   p1         o1
   p1         o2
   p1         o3
   p2         o1
   p2         o2
   p3         o2
   p3         o3
   p3         o4

But when save set2 after set1 table product_order_mapping will look like

product_id  order_id
---------------------
   p1         o1
   p1         o2
   p2         o1
   p2         o2
   p3         o2

We can see the difference before saving set2 and after saving set2 mappings p1->o3 , p3->o3 and p3->o4 are lost

Reason for losing mapping

when we tried to store set2 we tried to override the duplicate entry like p1->o1,p1->o2 which are allready available in the mapping table So what happened is before adding these duplicate entry all mappings related to p1 are removed and then p1->o1,p1->o2 got added

And so we lost some previous entry

Solution

Avoid adding duplicate entry from set2 like p1->o1,p1->o2 as they are already present in mapping table

for (Company cEntity : companyList) {
for (Product pEntity : productList) {

OrderFunctions = this.getAllOrderFunctions(cEntity.getId(), pEntity.getId());
                        
Set<Order> oList = new HashSet<>();

for (OrderFunction orderFn : OrderFunctions) {
Order orderEntity = new Order();
orderEntity.setId(lfs.getOrderFunction_id());
orderEntity.setStageGroup(lfs.getOrderFunctionOrder());
oList.add(sgEntity);
}

Set<Order> collectSG = pEntity.getOrders().stream().filter(o->oList.contains(o)).collect(Collectors.toSet());
oList.removeAll(collectSG);
if(oList.size()>0) {
orderRepository.saveAll(oList);
                
pEntity.setOrders(oList);
productRepository.save(pEntity);
}
}
}
}
Bruell answered 9/9, 2021 at 15:53 Comment(0)
P
0

TL;DR

  • Hibernate deletes all the records and then reinserts the remaining ones when using a List<> as a 'bag' in Hibernate terminology, i.e. the list does not have an index column specified with @OrderColumn() (basically it doesn't have ordering). Excerpt from Vlad Mihalcea' High-Performance Java Persistence: "A bag does not guarantee that elements are uniquely identifiable, hence Hibernate needs to delete and reinsert the elements associated with a given parent entity whenever a change occurs to the List."
  • One possible solution is to use instead a Set<> and Hibernate will only insert and delete the specific records (make sure to override the equals and hashCode methods for your entity).

Further reading: @OneToMany List<> vs Set<> difference

User John the Traveler confirms this behaviour in his answer, https://mcmap.net/q/1371713/-jpa-many-to-many-relation-deleting-all-previous-relations-and-entering-the-new-relations, even when simply adding or deleting from a bag (unorderd List) and not recreating it.

Plumate answered 24/4 at 20:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.