3

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

jordan.baucke
  • 4,308
  • 10
  • 54
  • 77

1 Answers1

4

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

Naros
  • 19,928
  • 3
  • 41
  • 71
  • Sorry if i misunderstand this, so basically this is just updating a field in the parent class to trigger the entitylistener in the parent class? – Cherple Feb 12 '19 at 08:00
  • Not exactly. The OP wanted a way to notify the parent when a child object's state changed that would influence some state on the parent. The former is meant to act as a callback from the child's entity-lifecycle to the parent. The latter is meant to illustrate that the transient state can be managed separately inside a decorator object that essentially wraps the entity instead. The benefit of the latter is you avoid unnecessary cpu cycles with these callbacks from child->parent and instead turn that process into delegation when needed, especially if you only need this in UI/biz logic. – Naros Feb 12 '19 at 16:02
  • I'm not sure if my scenario is the same. My issue is when i only update my child table's fields, my entitylistener at my parent class doesn't pick up this change, or maybe it detects no change in my parent so it wasn't called. Why this is important is because i am taking my parent's "lastModifiedDate" field to see if a record has been updated, but this lastModifiedDate wont change if only the child table was modified. Right now i am simply toggling a boolean field in my parent class from my controller everytime an update was done. – Cherple Feb 13 '19 at 01:35
  • So when the child is modified, have the child's entity listener make a call to a method on the parent that does its entity-listener logic. This way when the parent or child is modified in anyway, you can guarantee that both entity listener scenarios invoke your parent's business logic. You should then be able to remove the boolean possibly then. – Naros Feb 13 '19 at 14:02
  • I see, that sounds logical. But if my parent has many child tables, wouldn't that mean i would have to create (eg. 10) different entity listeners for each child just to call the parent? If that's the case then i will just treat this as a situation where it depends on different circumstances. Just wanted to be clear on the solution thats all. But thanks for replying to an old post! :) – Cherple Feb 14 '19 at 01:02