Search code examples
hibernatejpaspring-dataspring-data-jpaentitylisteners

Spring Data JPA @EntityListeners w/ @Transient @PostLoad, Properties Not Updated


We have two Spring Data JPA Entities (parent,child), the count of children with a field set to a particular value, influences the value of a parent's record in a @Transient property set during @PostLoad

Parent:

@Entity
public class Parent {
   @Transient
   private boolean status = false;

   @OneToMany
   @Where("STATUS = true")
   private Set<Children> childrens;

   @PostLoad
   public void postload(){ 
     if(childrens.size() > 0) this.status = true;
   }
....
} 

Children:

@Entity
@EntityListeners({ ParentListener.class })
public class children {

      private Boolean status = false;

      @ManyToOne
      private Parent parent;
}

In my controller / service classes (which are NOT annotated as @Transactional, I come along and update the status value of a Children record:

@Service 
public class ChildrenService {

...
    public void doStuff(Children child) {
        child.status = true;
        childRepository.save(child);
    }
} 

Now the ParentListener kicks in, and I want to log when the parent's status value changes.

class ParentListener {

    @PostUpdate // AFTER Children record updated
    public void childPostPersist(Children child) {
         AutowireHelper.autowire(this);
         // here child.parent.status == false (original value)
         // but I have set this child record equal to true, and 
         // have triggered the `save` method, but assuming I am still
         // in the session transaction or flush phase, the 
         // parent related record's status is not updated?
         System.out.println(child.parent.status); // prints false
         Parent currentParent = parentRepository.getOne(child.parent.getId());
         System.out.println(currentParent.status); // prints false
    }
}

What am I misunderstanding about @Transactional, @Postload and transactions/sessions and EntityListeners?

PS. AutowireHelper is reference from here


Solution

  • I believe you're misunderstanding the subtle differences between the three different reactive callbacks @PostPersist, @PostUpdate, and @PostLoad.

    The @PostLoad callback is only fired when an entity is first loaded into the persistence context or when an entity's state is being refreshed. The former occurs when you perform a find or query and the latter happens when you call refresh on an entity instance.

    Similarly, the @PostPersist callback fires after a transient entity has been persisted for the first time while the @PostUpdate callback fires after an existing entity has been updated.

    When dealing with Spring Data, when you call the save method on the Repository, that method could lead to the persistence provider invoking either the persist or merge operation depending upon whether the entity object is transient/new or whether its an existing, potentially detached, entity instance.

    That said, you'll likely require a series of listener callbacks to manage the life cycle you're after. This is because when you modify your Child entity and save it, that won't necessarily propagate a listener callback on the association.

    public class Children {
       /**
        * When the change is persisted or updated, make sure to trigger
        * the callback on the parent to update its status accordingly
        */
       @PostPersist
       @PostUpdate
       public void updateParentAssociationStatusOnPersistOrUpdate() {
         if ( parent != null ) {
           parent.updateStatusOnPersistOrUpdateOrLoad();
         }
       }
    }
    
    public class Parent {
       /**
        * When the parent is loaded, refreshed, or after being persisted
        * or updated, this method will insure that the status based on 
        * child association is properly maintained.
        */
       @PostLoad
       @PostPersist
       @PostUpdate
       public void updateStatusOnPersistOrUpdateOrLoad() {
         if ( children != null && !children.isEmpty() ) {
           setStatus( true );
         }
       }
    }
    

    Depending on the use case, if you are trying to maintain this transient status state for some non-persistence task, I'd probably suggest to use the decorator pattern here rather than the entity life cycle because its important to keep unlike concerns separate in the first place.

    One way to implement this decorator pattern might consist of the following:

    // An interface that defines the parent's contract
    public interface Parent {
      default boolean getStatus() {
        return false;
      }
      // other methods
    }
    
    // Entity implementation of the parent contract
    @Entity(name = "Parent")
    public class ParentImpl implements Parent {
      // implement all non-default methods
      // #getStatus will be implemented in the decorator 
    }
    
    // A view/decorator object that implements the parent contract
    public class ParentView implements Parent {
       private final Parent parent;
    
       public ParentView(Parent parent) {
         this.parent = parent;
       }
    
       // all methods delegate to the parent 
    
       @Override
       public boolean getStatus() {
         return !parent.getChildren().isEmpty();
       }
    }
    

    Now just pass a List<ParentView> to the upper layers rather than List<Parent>.