2

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 Tags.

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!

instinct
  • 430
  • 1
  • 7
  • 24

3 Answers3

2

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.

Guardian
  • 160
  • 1
  • 13
  • As my children table is association table, I am okay updating there. But Tag table doesn't have direct relationship yet it gets `update`d right after the `insert` on Note(Check logs). Can you please quote this default behaviour from docs? Can you please explain how and where `nullable` is affecting in my case? – instinct Apr 12 '20 at 04:45
  • Please read this article provided by Vlad Mihalcea on this matter. Read the part with a join table. it explains the flush order and children not having the information to store the one to many relationships and why there are update statements. good luck. – Guardian Apr 12 '20 at 06:16
  • That unfortunately doesnt answer my question, not to the level of downvote though. Still thanks! – instinct Apr 12 '20 at 06:51
  • 1
    I was right about the update part as you can read about it here: [link](https://vladmihalcea.com/the-best-way-to-map-a-onetomany-association-with-jpa-and-hibernate/) but about the error on your join column annotation you don't have to set the reference column at all since the default is id. however, You wrote the wrong column name for your ids. please correct that and re-run your code. I think on how to clarify my answer more. cheers! – Guardian Apr 12 '20 at 07:14
1

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:

enter image description here

Kavitha Karunakaran
  • 1,340
  • 1
  • 17
  • 32
  • My apologies! I have appended the sql scripts needed under the sql queries. Please run those to update your table. – instinct Apr 11 '20 at 17:16
  • No worries. Can I see the implementation of your TagDao as well? – Kavitha Karunakaran Apr 11 '20 at 17:29
  • Well, I don't have TagDao as of now. My Goal is to save tags using the hibernate when we save `Note`. – instinct Apr 11 '20 at 17:34
  • I am stil getting the errors after making your change: https://ibb.co/zhqVbWn – instinct Apr 12 '20 at 14:32
  • 1
    Just out of curiosity, how are you sending data to your application? Are you using a REST client like SOAPUI or Postman? In the controller code, I see that `PostMapping` and `PutMapping` are pointing to `"/"`. Ideally, `PutMapping` should have `noteId` passed as params. So, if you do not have that there, in all probability, you should encounter with an `HttpRequestMethodNotSupportedException` saying _Request method POST is not supported_. – Kavitha Karunakaran Apr 12 '20 at 15:47
  • I have integrated swagger in NoteApp accessible at: http://localhost:8080/swagger-ui.html using which I call `POST` with Payload which is hitting at 'POST' endpoint confirmed. You are right about `PostMapping` and `PutMapping`. I will add `id` for `PutMapping` in future. – instinct Apr 12 '20 at 16:22
0

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!

instinct
  • 430
  • 1
  • 7
  • 24