fix(timeline): add @Transactional(readOnly=true) to TimelineService.assemble()

Without the annotation, Hibernate closes its sub-transaction after
eventRepository.findAll() returns, leaving TimelineEvent entities
detached. Accessing ev.getPersons() or doc.getReceivers() on those
detached entities throws LazyInitializationException in production
(constitution §1.6). @DataJpaTest and @Transactional test classes
masked the bug by keeping an outer session alive.

Fixes: @felix / @markus review blockers on PR #826

Refs #777
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-13 17:23:10 +02:00
parent 1de314f49b
commit 590b00d2d7
2 changed files with 80 additions and 0 deletions

View File

@@ -12,6 +12,7 @@ import org.raddatz.familienarchiv.person.Person;
import org.raddatz.familienarchiv.person.PersonService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Comparator;
@@ -56,7 +57,14 @@ public class TimelineService {
* Assembles the timeline for the given filter. All filters are ANDed.
* Throws {@link DomainException} (bad request) when fromYear &gt; toYear.
* Throws {@link DomainException} (not found) when personId refers to an unknown person.
*
* <p>{@code @Transactional(readOnly=true)} is required here — unlike simple scalar reads,
* this method accesses lazy collections ({@link TimelineEvent#getPersons()},
* {@link org.raddatz.familienarchiv.document.Document#getReceivers()}) after the
* repository sub-transaction closes. Without this annotation those accesses throw
* {@link org.hibernate.LazyInitializationException} in production (constitution §1.6).
*/
@Transactional(readOnly = true)
public TimelineDTO assemble(TimelineFilter filter) {
if (filter.fromYear() != null && filter.toYear() != null
&& filter.fromYear() > filter.toYear()) {