为什么不在插入时设置多对一关系的外键字段?

Why isn't the foreign key field of a Many-to-One relationship being set on insert?

我的 Spring 网络应用程序允许用户更新 "Employee" 记录以更改字段或添加与此 "Employee" 记录相关的新 "Phone" 记录。但是,在添加新的 "Phone" 记录后提交 "Employee" 记录进行更新时,它会抛出 SQL 错误异常。

问题是 "Phone" table 到 "Employee" table 的 "employee_id" 外键没有在最终的 SQL 插入语句提交到数据库。但是,在 updated/merged "EmployeeEntity" 对象引用的 "PhoneEntity" JPA 实体对象中,与 employee_id 数据库字段关联的 属性 不为空,它被设置为 "EmployeeEnity" 对象 updated/merged.

根据我对 JPA 的理解,实体 属性 与数据库字段关联应该在实体记录的插入语句提交到数据库时设置它,但在这种情况下它不是导致此错误。

我试过使用调试器单步执行,我已经验证创建的 PhoneEntity 对象是 EmployeeEntityphones 属性 的成员,并且相同的 PhoneEntityemployee 属性 在双向关系中设置为相同的 EmployeeEntity 对象(具有相同的对象 ID)。

我还设置了 hibernate.show_sql=true 以查看 SQL 语句被提交到数据库,它包括语句(省略号表示更多字段):

Hibernate: 
    insert 
    into
        phone
        (id, employee_id, ...) 
    values
        (?, ?, ...)

这意味着它正在为新的 PhoneEntity 对象插入一个新的 phone

在尝试 运行 这个插入语句后,它给出了 SQL 错误 "Column 'employee_id' cannot be null"。但是就像我之前说的,我已经用调试器检查过 employee 属性 确实设置为 EmployeeEntity 对象。

这是我的代码的简化示例:

@Entity
@Table(name = "employee")
public class EmployeeEntity implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", unique = true, nullable = false)
    private Integer id;

    @OneToMany(mappedBy="employee", cascade = {CascadeType.PERSIST})
    private Set<PhoneEntity> phones = new HashSet<>();

...
}

@Entity
@Table(name = "phone")
public class PhoneEntity implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", unique = true, nullable = false)
    private Integer id;

    @ManyToOne
    @JoinColumn(name = "employee_id", nullable = false)
    private EmployeeEntity employee;

...
}

具有由以下 SQL 语句创建的结构的 table。

CREATE TABLE employee (
    id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
...
);

CREATE TABLE phone (
    id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    employee_id INT NOT NULL,
...
    FOREIGN KEY(employee_id) REFERENCES employee(id)
);

下面是它实际将更新提交给实体管理器以对数据库进行更新的地方。

    public void update(EmployeeDomain employee) {
        EmployeeEntity entity = employeeDomainToEntity.transform(employee)
        getEntityManager().merge(entity);
    }

EmployeeEntityPhoneEntity 对象是通过转换类似的域对象创建的,这些域对象又从 http 请求反序列化。我会在代码的这一部分包含更多内容,但正如我所提到的,我已经通过我的调试器确认提交给合并的实际实体对象已经处于我们预期的形式 phones 字段和 employee 字段设置正确,因此最终实体应该是正确的。

我认为问题在于您正在使用合并。 实体的级联类型设置应该是:

@OneToMany(mappedBy="employee", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    private Set<PhoneEntity> phones = new HashSet<>();

在官方JPA specification document(2.1 版)的“3.2.7.1 合并分离实体状态”部分(第 85 页)我们发现:

For all entities Y referenced by relationships from X having the cascade element value cascade=MERGE or cascade=ALL, Y is merged recursively as Y'. For all such Y referenced by X, X' is set to reference Y'. (Note that if X is managed then X is the same object as X'.)

这说明您缺少 cascade=MERGE 用于 phones 字段的注释。

正如 thanh ngo 的回答中所提出的,上述定义(或:解释)因此转化为:

@OneToMany(mappedBy="employee", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private Set<PhoneEntity> phones = new HashSet<>();

或者,您也可以使用 cascade=CascadeType.ALL。但是,这也包括 CascadeType.REMOVE 等可能并不总是预期的操作。

希望对您有所帮助。