实体 ID 存在但在 equals() 方法中显示为 null (Hibernate)

Entity id exists but is appears as null inside equals() method (Hibernate)

我有三个 classes:WorkPositionEmployeeEmployeeCode。 Employee 代表在某个地方工作的人,employee 在工作中可以有多个(工作)职位,employee's codes 代表员工(一个或多个代码)。对于每个 WorkPosition,必须分配一个默认的 EmployeeCode(字段 defaultCode),如果有任何员工代码,则显示哪个代码代表该职位的员工。

WorkPosition class:

@Entity
@Table(name = "work_position")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class WorkPosition{

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequence_generator")
    @SequenceGenerator(name = "sequence_generator")
    private Long id;

    @ManyToOne(optional = false, fetch = FetchType.LAZY)
    @NotNull
    private Employee employee;

    @ManyToOne(fetch = FetchType.LAZY)
    private EmployeeCode defaultCode;

    // other fields, getters, setters, equals and hash ...

Employee class:

@Entity
@Table(name = "employee")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Employee{

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequence_generator")
    @SequenceGenerator(name = "sequence_generator")
    private Long id;

    @OneToMany(mappedBy = "employee", fetch = FetchType.LAZY)
    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
    private Set<EmployeeCode> employeeCodes;

    @OneToMany(mappedBy = "employee", fetch = FetchType.LAZY)
    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
    private Set<WorkPosition> workPositions;

    // other fields, getters, setters, equals and hash ...

EmployeeCode class:

@Entity
@Table(name = "employee_code")
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class EmployeeCode {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequence_generator")
    @SequenceGenerator(name = "sequence_generator")
    private Long id;

    @ManyToOne(optional = false, fetch = FetchType.LAZY)
    @NotNull
    private Employee employee;

    @OneToMany(mappedBy = "defaultCode", fetch = FetchType.LAZY)
    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
    private Set<WorkPosition> defaultCodes;

    public boolean equals(Object o) {
        if (this == o) {
            return true;
        } else if (!(o instanceof EmployeeCode)) {
            return false;
        } else {
            return this.id != null && this.id.equals(((EmployeeCode)o).id);
        }
    }

    // other fields, getters, setters, hash ...

因此,在我的示例中,一个员工的 WorkPositions 之间的唯一区别是 defaultCode,它可能在 WorkPositions 之间不同。
我有一个表格,我可以在其中操作与 WorkPosition 相关的所有数据。例如,我可以更改 WorkPosition 的 defaultCode and/or 删除一个 EmployeeCode。当我保存表单时,我必须检查是否删除了一个 EmployeeCode,该 EmployeeCode 被设置为与保存的 WorkPosition 相关的任何 WorkPositions 的 defaultCode。如果是这样,我会重新分配它,否则我将无法删除 EmployeeCode,因为我会得到 ConstraintViolationException,因为 WorkPosition 仍会引用我希望删除的 EmployeeCode。
假设我有一个员工,有两个 EmployeeCodes(EC1 和 EC2)和两个 WorkPositions(WP1 和 WP2)。 WP1 的 DefaultCode 是 EC1,WP2 的 defaultCode 是 EC2。我保存了 WP1 的表格,但我没有删除任何东西。为了检查相关 WorkPosition (WP2) 的 defaultCode (EC2) 是否仍然存在,我遍历了所有剩余的代码(savedWorkPosition.getEmployeeCodes(),其中 savedWorkPosition 等于 WP1)并检查它是否仍然包含 defaultCode(relatedWorkPosition.getDefaultCode() 其中从 db 查询 relatedWorkPosition 并且它引用 EC2).

newDefaultCode = savedWorkPosition.getEmployeeCodes() // [EC1, EC2]
    .stream()
    .filter(code -> code.equals(relatedWorkPosition.getDefaultCode()))
    .findFirst()
    .orElseGet(() -> ...);

然而,equals()(查看上面的 EmployeeCode class)returns 为假。我在调试equals方法的时候,发现参数对象(EC2)的id是null。当我在 equals 之前的过滤器调用中注销 id 时,我得到了正确的 id。我可以做 .filter(code -> code.getId().equals(relatedWorkPosition.getDefaultCode().getId())) 并且它有效,但这似乎是错误的。 为什么equals方法中的id为空?
我认为这可能是在持久性上下文中对实体的状态做一些事情,而 Hibernate 做了一些我不明白的事情。我使用了 this answer 的一些帮助来注销实体的状态:

Why is the id in the equals method null?

我会根据你之前说的回答(因为,我自己也经历过这样的事情):

However, the equals() (look at the EmployeeCode class above) returns false. When I debugged the equals method, I found out that the id of the parameter object (EC2) is null. When I log out the id in the filter call before the equals, I get the correct id. I could do .filter(code -> code.getId().equals(relatedWorkPosition.getDefaultCode().getId())) and it works, but this seems wrong ...

问题是结合使用 @ManyToOne(fetch=lazy) 和您当前在 class EmployeeCode 中实现的等号...当您将 ManyToOne 关系声明为lazy,然后加载 contains/wraps 这种关系的实体,hibernate 不会加载关系或实体,而是注入 扩展 [=43= 的代理 class ] 从你的 Entity class ...代理 class 充当拦截器,仅当它的 声明的方法被调用...

这是棘手的部分:用于创建此类代理的库会创建截获实体的精确副本,其中包括相同的实例变量 您在实体 Class 中声明的(所有这些都使用默认 JAVA 值初始化)......当您将此类代理传递给 equals方法(公平地说,它可以是任何方法)并且该方法的逻辑访问提供的参数中的实例变量,您将访问代理的虚拟变量,而不是您 want/expect 的虚拟变量。这就是为什么您在 equals 实施中看到这种奇怪行为的原因...

为了解决这个问题并避免错误,根据经验,我建议替换实例变量的使用,并在提供的参数上调用 getter 和 setter 方法......在你的情况下,它将是这样的:

    public boolean equals(Object o) {
        if (this == o) {
            return true;
        } else if (!(o instanceof EmployeeCode)) {
            return false;
        } else {
            return this.id != null 
                && this.id.equals(((EmployeeCode)o).getId());
        }
    }

你可能想知道为什么:

this.id != null && this.id.equals(((EmployeeCode)o).getId());

而不是:

this.getId() != null
    && this.getId().equals(((EmployeeCode)o).getId());

原因很简单:假设调用equals方法的java对象是一个proxy/lazy实体......当你调用这样的方法时,代理的逻辑加载真实的实体并在其上调用真实的 equals 方法...代理 EmployeeCode class 的符号表示可能如下所示(注意,这不是真正的实现,只是一个示例更好地理解这个概念):

class EmployeeCodeProxy extends EmployeeCode {
   // same instance variables as EmployeeCode ...

   // the entity to be loaded ...
   private EmployeeCode $entity;
   ....
   public boolean equals(Object o) {
       if (this.$entity == null) {
           this.$entity = loadEntityFromPersistenceLayer();
       }
       return this.$entity.equals(o);
   }
   ...
}