Hibernate 为子实体调用更新,而在 save() 操作期间为父实体插入

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

TL;DR

对于 NoteTag 之间的 OneToMany 使用第三个 table Note_tag 的映射,save() 无法保存 Note实体。

背景:

所以我正在开发这个 NoteApp(Github repo location) which saves a Note with title, description, status and tag(tag being a String value). As a feature update,我想为一个笔记添加多个标签。为此,我创建了一个标签 table 和标签的第三个关联 table 使用失败的直接前向注释 @JoinTable 注释。此功能导致我在保存注释实体时遇到上述问题。

我在屏幕后面用的是什么:

Java 1.8, Hiberanate, SpringBoot

here

上有关技术堆栈的更多详细信息

我已经在做的事情:

Save() Note 没有 Tag

我的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**/
}

我的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;
}

我的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;
}

我的TagDto.java:

public class TagDto {

    private int tagId;
    private String tagName;
}

我的 Table 创建查询:

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;

错误日志:

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.

异常提示我错误地更新了标签,这引发了一个问题,即为什么它首先为 Tag 调用更新,而为 Note 正确调用插入? 在发布此问题之前,我尝试寻找解决方案,但找不到任何解决方案。 提前致谢!

编辑

确保在保存笔记之前更新标签。还要确保 NoteServiceImpl 中的服务方法 saveNote 也用 @Transactional 注释,以便在查询由于某种原因被中断时启动回滚。我能够重现您遇到的问题,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 是与此相关的绝妙答案。

在您的代码中,如果您仍想在将数据保存到 T_NOTE 中的同时保存标签,则需要更改 NoteDaoImpl 中的 saveNote 方法。

    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();
        }
    }

我想建议的一件事是将 tagsList 更改为 Set 以避免重复数据。

对代码进行上述更改后,您应该能够成功执行 POST 和 GET 调用,如下所示:

Hibernate 在插入父项时为子项调用更新,因为您使用了单向 @OneToMany 而不是 @ManyToOne 或双向。无论您做什么,Hibernate 都会将其建模为 OneToMany。因此,Hibernate 插入您的父项和子项,然后调用子项的更新以将父项外键添加到子项的表中。现在关于为什么你得到异常,正如我所说,Hibernate 在你的关系的许多方面调用更新,但是由于默认行为是 nullable 设置为 true,它会导致你的错误。请记住,您正在进行单向映射,因此只有关系的一方知道它。请避免使用 OneToMany 的单向关系。您可以在 Vlad MihalCea 网站上找到许多关于此事的精彩文章。他是一个冬眠大师。将 nullable 设置为 false,它应该可以解决您的问题。

Update 由于我的 DTO 和实体(注释和标记)中的标识符数据类型不匹配而被调用。由于这是 save() 方法,我在 NoteDTO 中使用 int 并在 Note Entity 中使用盒装类型 Integer。因此,当我在没有 id 的情况下传递有效负载时,输入对象采用默认值 0。当转换为 Note 实体时,这个 0 传递了 0 而不是 null。因此,对于 Tag 的子记录,id 为 0,因此 hibernate 可能会求助于 update() 在她的执行策略中看到该子记录的非空值。

我通过将 NoteDto id 转换为 Integer 进行了验证,这创建了正确的查询。以下是我的有效载荷:

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

在 TagDto 中:

public class TagDto {

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

在备注中:

public class NoteDto {

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

在NoteDaoImpl中,

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

成功后的日志:

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 (?, ?)

我没有将 @kavitha-karunakaran's 标记为答案,因为单独保存 Tag 不是我打算做的,这将违背干净的方法。

我没有标记 @guardian's 答案,因为转向更好的方法不是我现在的意图,而是为了纠正我目前的单向映射方法。

谢谢两位的建议!干杯!