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;
}
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();
}
}
}
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
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();
}
}
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.
The Source-Code and some SpringBootTests with additional log-output can be found at GitHub
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:
To further investigate and ensure consistent behavior, you might consider:
Using fetch joins in your queries (JOIN FETCH) to ensure that child entities are loaded eagerly when needed.
Checking the generated SQL queries in your logs to understand how Hibernate is fetching the entities.
Ensuring that transactions are managed appropriately, and entities are not detached when you expect them to be managed.
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.