Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Foreign key update anomaly: foreign keys set to null in Hibernate one-to-many relationship

I am finding that when the parent table in a one-to-many relationship is updated, the foreign keys of dependent data on the child table are being set to null leaving orphaned records on the child table.

I have two Java classes annotated with Hibernate tags. The parent table is:

@Entity
@Table(name = "PERSON")
public class Person implements Serializable {

// Attributes.    
@Id
@Column(name="PERSON_ID", unique=true, nullable=false)    
@GeneratedValue(strategy=GenerationType.AUTO)
private Integer personId;

@Column(name="NAME", nullable=false, length=50)      
private String name;

@Column(name="ADDRESS", nullable=false, length=100)
private String address;

@Column(name="TELEPHONE", nullable=false, length=10)
private String telephone;

@Column(name="EMAIL", nullable=false, length=50)
private String email;

@OneToMany(cascade=CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name="PERSON_ID")   
private List<Book> books;

And the child table is:

Entity
@Table(name = "BOOK")
public class Book implements Serializable {

// Attributes.
@Id
@Column(name="BOOK_ID", unique=true, nullable=false)
@GeneratedValue(strategy=GenerationType.AUTO)
private Integer bookId;

@Column(name="AUTHOR", nullable=false, length=50)
private String author;

@Column(name="TITLE", nullable=false, length=50)
private String title;

@Column(name="DESCRIPTION", nullable=false, length=500)
private String description;

@Column(name="ONLOAN", nullable=false, length=5)
private String onLoan;

// @ManyToOne      
// private Person person; 

But when an update is issued on a Person, any Books records related to the parent are set to null.

The Book table is:

CREATE TABLE BOOK (
BOOK_ID INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY (START WITH 1, INCREMENT BY 1),    
AUTHOR VARCHAR(50) NOT NULL,  
TITLE VARCHAR(100) NOT NULL, 
DESCRIPTION VARCHAR(500) NOT NULL,
ONLOAN VARCHAR(5) NOT NULL,
PERSON_ID INTEGER,   
CONSTRAINT PRIMARY_KEY_BOOK PRIMARY KEY(BOOK_ID),
CONSTRAINT FOREIGN_KEY_BOOK FOREIGN KEY(PERSON_ID) REFERENCES PERSON(PERSON_ID)) 

And the update method in the Person controller is:

@RequestMapping(value = "/profile", method = RequestMethod.POST)
public String postProfile(@ModelAttribute("person") Person person,             
                          BindingResult bindingResult,                              
                          Model model) {                  
    logger.info(PersonController.class.getName() + ".postProfile() method called.");

    personValidator.validate(person, bindingResult);
    if (bindingResult.hasErrors()) {
        return "view/profile";
    }
    else {              
        personService.update(person);                                        
        model.addAttribute("person", person);                                     
        return "view/options";            
    }
}   

And the actual DAO level method is:

@Override
public void update(Person person) {
    logger.info(PersonDAOImpl.class.getName() + ".update() method called.");

    Session session = sessionFactory.openSession();
    Transaction transaction = session.getTransaction();        
    try {
        transaction.begin();
        session.update(person);            
        transaction.commit();            
    }
    catch(RuntimeException e) {
        Utils.printStackTrace(e);
        transaction.rollback();
        throw e;
    }
    finally {
        session.close();
    }
}    

So I'm assuming that update is the cause of the issue but why?

I have tried merge, persist and saveOrUpdate methods as alternatives but to no avail.

Concerning the fact that my Book table has no annotation for @ManyToOne, disabling this tag was the only way in which I could get LAZY fetching to work.

This case also seems very similar to Hibernate one to many:"many" side record's foreign key is updated to null automatically and Hibernate Many to one updating foreign key to null, but if I adopt the changes specified to classes in these questions to my own tables, my application refuses to even compile seemingly because of problems with the use of mappedBy in the Person table.

Any advice is welcome.

Controller method changed to:

// Validates and updates changes made by a Person on profile.jap
@RequestMapping(value = "/profile", method = RequestMethod.POST)
public String postProfile(@ModelAttribute("person") Person person,             
                          BindingResult bindingResult,                              
                          Model model) {                  
    logger.info(PersonController.class.getName() + ".postProfile() method called.");

    // Validate Person.        
    personValidator.validate(person, bindingResult);
    if (bindingResult.hasErrors()) {
        return "view/profile";
    }
    else {              

        // Get current Person.
        Person currPerson = personService.get(person.getPersonId()); 

        // Set Books to updated Person.
        person.setBooks(currPerson.getBooks());
        personService.update(person); 

        model.addAttribute("person", person);                                     
        return "view/options";            
    }
}           

And it works.

like image 946
Mr Morgan Avatar asked Mar 19 '23 06:03

Mr Morgan


1 Answers

I assume that the postProfile() method receives a Person instance which only contains the ID, name, address etc. of the person, as posted by a web form, but that its list of books is null or empty.

And you're telling Hibernate to save that person. So you're effectively telling Hibernate that this person, identified by the given ID has a new name, a new address, a new email, ... and a new list of books which happens to be empty, and that this should be saved into the database.

So Hibernate does what you're telling it to do: it saves the new state of the person. And since the new person doesn't have any book, all the books it previously owned become owned by nobody.

You'll have to get the actual, persistent Person entity from the database, and copy the fields that should actually be modified from the new person to the persistent one.

Or you'll have to pre-load the persistent person from the database and make Spring populate this persistent person instead of creating a new instance per scratch. See http://docs.spring.io/spring/docs/4.0.x/spring-framework-reference/htmlsingle/#mvc-ann-modelattrib-methods.

like image 185
JB Nizet Avatar answered Mar 21 '23 19:03

JB Nizet