简体   繁体   中英

Hibernate calling update for child entity whereas insert for parent during save() operation

TL;DR

For OneToMany maping between Note and Tag using third table Note_tag , the save() is not able to save Note entity.

Background:

So I was workng on this NoteApp ( Github repo location ) which saves a Note with title, description, status and tag(tag being a String value). As a feature update , I thought to add multiple Tags for a Note. For this, I created a Tag table and a third association table of tag and note using faily straigt forward @JoinTable annotation. This feature led me to above mentioned issue while saving the Note entity.

What I am using behind the screen:

Java 1.8, Hiberanate, SpringBoot

More details on tech stack at here

What I already have working:

Save() Note without Tag s.

My Note.java:

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
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.JoinTable;
import javax.persistence.OneToMany;
import javax.persistence.Table;

@Entity
@Table(name = "T_NOTE")
public class Note implements Serializable {

    private static final long serialVersionUID = -9196483832589749249L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name="ID")
    private Integer id;

    @Column(name="TITLE")
    private String title;

    @Column(name="DESCRIPTION")
    private String description;

    @Column(name = "LAST_UPDATED_DATE")
    private Date lastUpdatedDate;

    @Column(name="STATUS")
    private String status;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinTable(name = "T_NOTE_TAG", joinColumns = { @JoinColumn(name="NOTE_ID", referencedColumnName = "ID") }, inverseJoinColumns = {
            @JoinColumn(name = "TAG_ID", referencedColumnName = "TAG_ID") })
    private List<Tag> tags = new ArrayList<Tag>();
    /** getters setters omitted for brevity**/
}

My Tag.java:

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "T_TAG")
public class Tag implements Serializable {

    private static final long serialVersionUID = -2685158076345675196L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name="TAG_ID")
    private Integer tagId;

    @Column(name="TAG_NAME")
    private String tagName;
}

My NoteDto.java:

public class NoteDto {

    private int id;
    private String title;
    private String description;
    private List<TagDto> tags = new ArrayList<TagDto>();
    private Date lastUpdatedDate;
    private String status;
}

My TagDto.java:

public class TagDto {

    private int tagId;
    private String tagName;
}

My Table creation queries:

CREATE database if NOT EXISTS noteApp;

CREATE TABLE `T_NOTE` (
    `id` int(11) unsigned NOT NULL auto_increment,
    `description` varchar(20) NOT NULL DEFAULT '',
    `header` varchar(20) NOT NULL,
    `status` varchar(20) NOT NULL,
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


CREATE TABLE `T_TAG` (
    `tag_id` int unsigned not null auto_increment,
    `tag_name` varchar(30) not null,
    PRIMARY KEY(TAG_ID)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


CREATE TABLE `T_NOTE_TAG` (
    `note_tag_id` int unsigned not null auto_increment,
    `note_id` int unsigned not null,
    `tag_id` int unsigned not null,
    CONSTRAINT note_tag_note foreign key (`note_id`) references T_NOTE(`id`),
    CONSTRAINT note_tag_tag foreign key (`tag_id`) references T_TAG(`tag_id`),
    CONSTRAINT note_tag_unique UNIQUE (`tag_id`, `note_id`),
    PRIMARY KEY (`note_tag_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

use noteApp;
ALTER TABLE T_NOTE
Change header title varchar(1000) NOT NULL;

ALTER TABLE T_NOTE
ADD COLUMN last_updated_date timestamp AFTER status;

Error logs:

Hibernate: select next_val as id_val from hibernate_sequence for update
Hibernate: update hibernate_sequence set next_val= ? where next_val=?
test
Hibernate: insert into T_NOTE (DESCRIPTION, LAST_UPDATED_DATE, STATUS, TITLE, ID) values (?, ?, ?, ?, ?)
Hibernate: update T_TAG set TAG_NAME=? where TAG_ID=?
2020-04-11 18:02:40.647 ERROR 3614 --- [nio-8080-exec-1] o.h.i.ExceptionMapperStandardImpl        : HHH000346: Error during managed flush [Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.sandeep.SpringBootDemo.model.Tag#0]]
org.springframework.orm.jpa.JpaOptimisticLockingFailureException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.sandeep.SpringBootDemo.model.Tag#0]; nested exception is javax.persistence.OptimisticLockException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.sandeep.SpringBootDemo.model.Tag#0]
    at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:396)
    at org.springframework.orm.jpa.DefaultJpaDialect.translateExceptionIfPossible(DefaultJpaDialect.java:127)
    at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242)
    at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:545)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:746)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:714)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.

The exception suggests me that Tag is being updated wrongly which raises question that why is it calling update in first place for Tag whereas insert is correctly called for Note ? I tried finding solutions before posting this question but couldn't find any. Thanks in advance!

Edit

Ensure that you update the tags before you save note. Also make sure that the service method saveNote in NoteServiceImpl is also annotated with @Transactional so that rollback will be initiated in case the query gets interrupted for some reason. I was able to reproduce the issue that you faced and guardian is right about how Hibernate handles such situations. You cannot save data to multiple tables in a single SQL command (this is related to Atomicity) but you achieve that using transactions. Here is a fantastic answer related to this.

In your code, you need to change saveNote method in NoteDaoImpl if you still want to save tags while saving data into T_NOTE.

    public void saveNote(Note note) {
        try {
            Session session = this.sessionFactory.getCurrentSession();
            for(Tag tag: note.getTags()){
                session.saveOrUpdate(tag);
            }
            session.save(note);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

One thing that I would like to suggest would be to change the tags from List to Set to avoid repetition of data.

After making the above changes to the code, you should able to do successful POST and GET calls as shown below:

在此处输入图像描述

Hibernate calls update for children on parent insertion because you used a uni-directional @OneToMany instead of @ManyToOne or a bi-directional. no matter what you do, Hibernate models this as a OneToMany. So, Hibernate inserts your parent and children, then calls update on children to add the parent foreign key to children's tables. Now on why you got the exception, as I said, Hibernate calls update on many side of your relationship and but since the default behavior is nullable set to true, it causes your error. Remember you are doing a uni-directional mapping so only one side of the relationship knows about it. Please avoid using uni-directional relationships of OneToMany. you can find many great articles about this matter on Vlad MihalCea website. He is a hibernate master. Set nullable to false and it should solve your problem.

Update was being called due to mismatch of identifier datatype in my DTO and Entity(Note and Tag). As this was save() method, I was using int in NoteDTO and boxed type Integer in Note Entity. So, when I passed payload without id , input object resorted to default value 0. this 0 when converting to Note entity passed a 0 instead of null . So, for a child record of Tag, the id was 0 and thus hibernate might have resorted to update() seeing a non-null value for that child in her execution strategy.

I verified by coverting NoteDto id to Integer and this created correct queries. Below was my payload:

{
"title": "A Java SpringBoot note",
"description": "Desc for the note",
"lastUpdatedDate": "1486696408000",
"status": "New",
"tags": [
    {
    "tagName": "SpringBoot"
    }
]
}

In TagDto:

public class TagDto {

    private Integer tagId; ----> changed from int
    private String tagName;
}

In NoteDto:

public class NoteDto {

    private Integer id; ----> changed from int
    private String title;
}

In NoteDaoImpl,

Session session = this.sessionFactory.getCurrentSession();
session.save(note);

Logs after success:

Hibernate: select next_val as id_val from hibernate_sequence for update
Hibernate: update hibernate_sequence set next_val= ? where next_val=?
Hibernate: select next_val as id_val from hibernate_sequence for update
Hibernate: update hibernate_sequence set next_val= ? where next_val=?
test
Hibernate: insert into T_NOTE (DESCRIPTION, LAST_UPDATED_DATE, STATUS, TITLE, ID) values (?, ?, ?, ?, ?)
Hibernate: insert into T_TAG (TAG_NAME, TAG_ID) values (?, ?)
Hibernate: insert into T_NOTE_TAG (NOTE_ID, TAG_ID) values (?, ?)

I ddnot mark @kavitha-karunakaran's as answer as saving Tag separately was not what I intend to do and that will defy the clean approach.

I didnt mark @guardian's answer as moving to a better approach was not my intention as of now but to correct my current approach of uni-directional mapping.

Thanks, to both of you for suggestions! Cheers!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM