Skip to content

JdbcAggregateTemplate.streamAll(Class<T>) fires AfterConvertEvent/AfterConvertCallback twice per entity #2282

@qkrtkdwns3410

Description

@qkrtkdwns3410

Context
While looking into the codebase, I noticed a slight inconsistency in JdbcAggregateTemplate.streamAll(Class<T>). It seems this method is currently only used in test code (AbstractJdbcAggregateTemplateIntegrationTests, MyBatisDataAccessStrategyUnitTests), so this might not be a critical issue. However, I wanted to share this finding as its behavior differs from the other streamAll overloads.

Description
JdbcAggregateTemplate.streamAll(Class<T> domainType) currently triggers AfterConvertEvent and AfterConvertCallback twice per entity instead of once.

Current Implementation
The implementation calls triggerAfterConvert twice on the same entities:

public <T> Stream<T> streamAll(Class<T> domainType) {
    Iterable<T> items = triggerAfterConvert(accessStrategy.findAll(domainType)); // 1st trigger (eager)
    return StreamSupport.stream(items.spliterator(), false).map(this::triggerAfterConvert); // 2nd trigger
}

triggerAfterConvert(Iterable<T>) eagerly iterates every element and calls triggerAfterConvert(T) on each (firing AfterConvertEvent + AfterConvertCallback). Then .map(this::triggerAfterConvert) fires them again when the stream is consumed.

Eager vs Lazy Evaluation
Additionally, the method uses accessStrategy.findAll() (eager — loads all results into List<T>) instead of accessStrategy.streamAll() (lazy cursor-backed stream via queryForStream).

All three sibling overloads added in the same commit use the lazy pattern:

// streamAll(Class, Sort) — consistent
return accessStrategy.streamAll(domainType, sort).map(this::triggerAfterConvert);

// streamAll(Query, Class) — consistent
return accessStrategy.streamAll(query, domainType).map(this::triggerAfterConvert);

// streamAllByIds(...) — consistent
return accessStrategy.streamAllByIds(ids, domainType).map(this::triggerAfterConvert);

Expected vs Current Behavior

  • Expected: AfterConvertEvent and AfterConvertCallback fire exactly once per entity.
  • Current: Both fire twice per entity.

Verified with a counter listener: 3 entities → 6 events (expected 3).

streamAll(Class)       AfterConvertEvent count: 6  ← current
streamAll(Class, Sort) AfterConvertEvent count: 3  ← expected

Reproduction

AtomicInteger count = new AtomicInteger();

// Register AfterConvertEvent listener for Department
context.addApplicationListener((AfterConvertEvent<?> e) -> {
    if (e.getEntity() instanceof Department) count.incrementAndGet();
});

// Save 3 entities, then:
template.streamAll(Department.class).collect(Collectors.toList());

assertThat(count.get()).isEqualTo(3); // FAILS — actual: 6

Suggested Change

public <T> Stream<T> streamAll(Class<T> domainType) {
    return accessStrategy.streamAll(domainType).map(this::triggerAfterConvert);
}

This minor refactoring would:

  1. Remove the double trigger (events fire once per entity).
  2. Make the stream truly lazy (cursor-backed via queryForStream), aligning it with all sibling overloads.

Metadata

Metadata

Assignees

Labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions