javaspringspring-boothibernateevent-listener

Update related entity from event-listener callback


General Problem

We have two entities, Parent and Child with a one-to-many relationship between them. When updating the Parent we want to update some entries in all related children automatically.

For sake of simplicity we are storing the changed property (value) in the child-entity as well.

The actual problem involves changing different properties and calculating a new property in the related children based on those changes.

Getter/Setter ToString... are omitted for simplicity.

@Entity
public class ParentEntity {
  @Id
  @GeneratedValue
  private UUID id;
  private String name;
  private String value;

  @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
  private List<ChildEntity> children;
}

@Entity
public class ChildEntity {
  
  @Id
  @GeneratedValue
  private UUID id;

  @ManyToOne
  @JoinColumn(name="parent_id")
  private ParentEntity parent;

  private String value;
}

Used Approach - Event-Listener

Since the changes are done from different services and methods we are looking for a centralized solution, so we decided to use Event-Listeners in the Parent that triggers the update process in all its children.

Only additional methods are shown

public class ParentEntity {

  @PrePersist
  @PreUpdate
  void onUpdate() {
    if (children != null) {
      this.children.forEach(ChildEntity::update)
    }
  }
}

public class ChildEntity {
  void update() {
    if (parent != null) {
      this.value = parent.getValue();
    }
  }
}

Problem

This approach does not always update the child-entity when changing the parent-entity value.

We identified multiple factors that influence the outcome

We also tried @EntityGraph annotations but discarded the approach since it would be to access-dependent which negates the "one method" approach.

calling the repository-save method within the transaction does not have any effect.

This results in the following success/failed matrix's

Results success: + failed: -

Children list annotated with@OneToMan fetch LAZY and @Fetch mode

no-annotation JOIN SELECT or SUBSELECT
find by id - + -
find by id and access + + +
no-annotation JOIN SELECT or SUBSELECT
find by name - + -
find by name and access + + +

Children list annotated with @OneToMan fetch EAGER and @Fetch mode

no-annotation JOIN SELECT or SUBSELECT
find by id - - -
find by id and access - - +
no-annotation JOIN SELECT or SUBSELECT
find by name + + -
find by name and access + + +

Small note: I could not post find by id and find by name as one table - stackoverflow would not permit it

Updating the Parent to trigger the Child update

In this small example Program I generate a Fixed set of Parent/Child Entities to reduce overhead. The actual Updates are done by calling @Transaction annotated update methods in Spring @Service Beans.

public interface ParentRepository extends JpaRepository<ParentEntity, UUID> {
    Optional<ParentEntity> findByName(UUID name);
}

@Service
@lombok.RequiredArgsConstructor
public class ParentService {
    private final ParentRepository repository;

    @Transactional
    public void updateById(UUID id, String value) {
        ParentEntity parent = repository.findById(id).orElseThrow(() -> new RuntimeException("Unknown Parent"));
        parent.setValue(value);
    }

    @Transactional
    public long updateByIdAndAccess(UUID id, String value) {
        ParentEntity parent = repository.findById(id).orElseThrow(() -> new RuntimeException("Unknown Parent"));
        parent.setValue(value);
        return parent.getChildren().size();
    }


    @Transactional
    public void updateByName(String name, String value) {
        ParentEntity parent = repository.findByName(name).orElseThrow(() -> new RuntimeException("Unknown Parent"));
        parent.setValue(value);
    }

    @Transactional
    public long updateByNameAndAccess(String name, String value) {
        ParentEntity parent = repository.findByName(name).orElseThrow(() -> new RuntimeException("Unknown Parent"));
        parent.setValue(value);
        return parent.getChildren().size();
    }
}

Real-World Relation

The Real-World Application that triggered this Post is a Web-Application with a purchase and support operations for License Entities (the Children) that are handled in Packages (the Parent). The child update mechanism is to calculate the as status of the Child (active, expired, canceled ... ) based on properties from the Parent. The Parent Entity can be loaded and modified from multiple Services based on different Properties.

Additional to custom repository methods, like the example findByName we use the JpaSpecificationExecutor Repository with concatenated Specification objects to find the Parent Entities.

This method extends the possibilities of the children list being accessed prior to the PostUpdate event.

Open Questions

The Source-Code and some SpringBootTests with additional log-output can be found at GitHub


Solution

  • The difference in behavior between findById and findByName when accessing child entities in your Spring Data JPA repository could be related to the way these methods interact with the database and handle the loading of associated entities (children).

    Here are some possible reasons for the observed difference:

    1. Lazy Loading: By default, JPA associations are lazy-loaded, which means that related entities are not loaded from the database until they are explicitly accessed. When you call findById, the associated children collection might not be loaded, especially if it is marked as LAZY. However, when you subsequently access the children collection (e.g., by invoking a method on it), it triggers a database query to fetch the related child entities.
    1. Fetch Mode: The @Fetch annotation can influence how associations are fetched. Your matrix indicates that the @Fetch annotation has different effects based on the combination of fetch types (JOIN, SELECT, or SUBSELECT) and whether you access the children collection within the same transaction.
    1. Transaction Boundaries: If you are working with detached entities and there is a transaction boundary between fetching the parent entity and accessing its children collection, lazy loading might not work as expected. In the case of findByName, if you access the children collection within the same transaction, it could lead to different results compared to findById.

    To further investigate and ensure consistent behavior, you might consider:

    If you provide more details about how you're using these methods in your code and the exact configurations for lazy/eager loading, it might be possible to provide a more specific explanation.