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

Time:11-04

I have three classes: WorkPosition, Employee and EmployeeCode. Employee represents a person who works somewhere, employee can have many (work)positions at work, and employee's codes represent the employee (one or more codes). For each WorkPosition, a default EmployeeCode (field defaultCode) must be assigned, if there are any codes for the employee, that shows which code represents the employee in this position.

  • Employee -> WorkPosition is a one-to-many relationship
  • Employee -> EmployeeCode is a one-to-many relationship
  • EmployeeCode -> WorkPosition is a one-to-many relationship

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

So, in my example, the only difference between one Employee's WorkPositions is the defaultCode, that may differ between WorkPositions.
I have a form, where I can manipulate all data related to a WorkPosition. For example, I can change the defaultCode of the WorkPosition and/or delete an EmployeeCode. When I save the form, I must check if an EmployeeCode was deleted that was set as defaultCode for any of the WorkPositions related to the saved WorkPosition. If so, I reassign it, otherwise I wouldn't be able to delete the EmployeeCode as I would get a ConstraintViolationException as the WorkPosition would be still referencing the EmployeeCode I wish to delete.
Let's say I have an Employee with two EmployeeCodes (EC1 and EC2) and two WorkPositions (WP1 and WP2). DefaultCode of WP1 is EC1 and defaultCode of WP2 is EC2. I save the form of WP1, but I do not delete anything. To check if the defaultCode (EC2) of a related WorkPosition (WP2) still exists, I loop over all the remaining codes (savedWorkPosition.getEmployeeCodes() where savedWorkPosition equals to WP1) and check whether it still contains the defaultCode (relatedWorkPosition.getDefaultCode() where relatedWorkPosition is queried from db and it references EC2).

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

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. Why is the id in the equals method null?
I think it might be to do something with the state of the entity in the persistance context and Hibernate does something I do not understand. I used some help from this answer to log out the state of the entities:

  • entityManager.contains(relatedWorkPosition.getDefaultCode()) returns true
  • entityManagerFactory.getPersistenceUnitUtil().getIdentifier(relatedWorkPosition.getDefaultCode()) returns correct id.
  • entityManager.contains(<any code in savedWorkPosition.getEmployeeCodes()>) returns false
  • entityManagerFactory.getPersistenceUnitUtil().getIdentifier(<any code in savedWorkPosition.getEmployeeCodes()>) returns correct id.

CodePudding user response:

Why is the id in the equals method null?

I'll provide my answer based on what you said previously (because, I've experienced something like this by myself):

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

The problem is the combination of using @ManyToOne(fetch=lazy) and your current implementation of the equals in the class EmployeeCode ... When you declare a ManyToOne relationship as lazy, and you load the entity that contains/wraps such relationship, hibernate does not load the relationship or entity, instead it injects a proxy class that extends from your Entity class ... the proxy class acts an interceptor and loads the real entity data from the persistence layer only when one its declared methods is called ...

Here is the tricky part: the library used for such proxy creation, creates an exact copy of the intercepted entity, which includes the same instance variables that you declared in your entity Class (all of them initialized with default JAVA values) ... When you pass such proxy to an equals method (to be fair, it could be any method) and the method's logic accesses instance variables in the provided arguments, you will be accessing the dummy variables of the proxy and not the ones you want/expect. That's the reason why you're seeing that weird behaviour on your equals implementation ...

To fix this and avoid bugs, as a rule of thumb, I recommend to replace the use of instance variables and call the getter and setter methods on the provided arguments instead ... in your case, it will be something like this:

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

You may wonder why:

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

and not:

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

The reason is simple: assumming that the java object, on which the equals method is called, is a proxy/lazy entity ... when you invoke such method, the proxy's logic loads the real entity and call the real equals method on it ... A symbolic representation of proxy EmployeeCode class could look like this (beware, it's not the real implemention, is just an example to understand better the concept):

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

Happy Coding ...

  • Related