Junit Test - @OneToMany relationship returning the collection as null when @Transactional (propagation = Propagation.REQUIRED) is set

Good Afternoon Guys

First and foremost, I would like to say that my application is working correctly and so is my test, but a case in my test class caught my attention. This application was made with Spring Boot using Spring Data JPA and other concepts such as Service and Repository layer.

In my test class running it with @Transactional (propagation = Propagation.SUPPORTS) my test works without any problem, however, if I run it with @Transactional (propagation = Propagation.REQUIRED) the collection that is in the @OneToMany relationship comes back as null.

In the link below, a person is reporting that the same is true for them, that with @Transactional in the test class the collection of the relationship @OneToMany returns as null, remembering that when the property propagation of @Transactional is not defined by default it is Propagation.REQUIRED.

CASE I

NOTE: I am not having any problems regarding LazyInitializationException as shown in the link above, my application works correctly.

With the @Transactional (propagation = Propagation.REQUIRED) option, I tested the following scenarios:

- Hibernate.initialize() - Did not work
- INNER JOIN FETCH - Did not work
- spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true - Did not work
- FetchType.EAGER - Did not work

So, none of them ended up working.

After that I enabled the option logging.level.org.springframework.transaction.interceptor = TRACE, to check how the transactions were behaving and to my surprise both in Propagation.REQUIRED and Propagation.SUPPORTS they execute in the same way, a transaction is started by the test class and is retrieved in other operations.

Theoretically and logically this does not make any sense, since Propagation.REQUIRED and Propagation.SUPPORTS when there is no transaction the first starts a new transaction and the last one executes non-transactionally and when there is a transaction both use it, then after analysis of the log the second behavior is being adopted, so much so that all the steps are identical.

After that I went to check how it was being returned by the Repository and in Propagation.REQUIRED the collection is coming as null and in Propagation.SUPPORTS the collection is returned without errors, which leads me to believe that it could be some hibernate bug with respect to Propagation.REQUIRED.

Well I’ve tried everything and I couldn’t understand why this is happening with Propagation.REQUIRED, whoever has availability and can help me would be grateful, below will be a part of the classes and the log.

Thanks for the help.

TEST

@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = DemoUsingModelMapperAndOrikaApplication.class)
@Transactional(propagation = Propagation.SUPPORTS)
@ActiveProfiles("test")
class DemoUsingModelMapperAndOrikaApplicationTests {

    private SonController sonController;


    private FamilyController familyController;


    @Autowired
    public DemoUsingModelMapperAndOrikaApplicationTests(SonController sonController, FamilyController familyController) {
        this.sonController = sonController;
        this.familyController = familyController;
    }


    @Test
    public void testAddSonsToMother() {

    	//check to make sure mother now also contains son
        MotherDTO retrievedMother = familyController.getMotherBySon(newSon.getSonId());
    	Assertions.assertTrue( retrievedMother.getSonsId() != null && retrievedMother.getSonsId().size() > 0);
        Assertions.assertEquals(retrievedMother.getSonsId().get(0), retrievedSon.getSonId());
    }

}

Mother Entity

Mother Entity

@Table(name = "mother")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@ToString
public
class Mother{
	
	//...
	
	@OneToMany(
            mappedBy = "mother",
            cascade = CascadeType.ALL,
            fetch = FetchType.LAZY,
            orphanRemoval = true
    )
    private List<Son> sons;
}

Son Entity

Son Entity

@Entity
@Table(name = "son")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@ToString
public
class Son{

	//...
	
	@ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "mother_id")
    private Mother mother;
}

MotherRepository

@Repository
public interface MotherRepository extends JpaRepository<Mother, Long> {

    @Transactional(readOnly = true)
    @Query("select distinct m from Mother m inner join m.sons s where s.sonId = :sonId")
    Mother findMotherBySonId(@Param("sonId") long sonId);
}

FamilyService

@Service("familyServiceImpl")
public class FamilyServiceImpl implements FamilyService {

    private MotherRepository motherRepository;

    @Autowired
    public FamilyServiceImpl(MotherRepository motherRepository) {
        this.motherRepository = motherRepository;
    }

    @Override
    @Transactional(readOnly = true)
    public Mother getMotherBySonId(long sonId){
        return motherRepository.findMotherBySonId(sonId);
    }

}

FamilyFacade

public class FamilyFacade {

    @Qualifier("familyServiceImpl")
    @Autowired
    private FamilyService familyService;

    @Autowired
    private ModelMapper modelMapper;

    @Transactional(readOnly = true)
    public MotherDTO getMotherBySonId(long sonId){
        return convertToMotherDTOModelMapper(familyService.getMotherBySonId(sonId));
    }

}

FamilyController

@RestController
@RequestMapping("/family")
public class FamilyController {

    private FamilyFacade familyFacade;

    /**
     * Here we are using the principle of IoC and DI to instantiate the {@link FamilyFacade}
     * @param familyFacade - {@link FamilyFacade}
     */
    @Autowired
    public void setFamilyFacade(FamilyFacade familyFacade) {
        this.familyFacade = familyFacade;
    }


    @GetMapping("/mother/son/{sonId}")
    public MotherDTO getMotherBySon(@PathVariable("sonId") long sonId) {
        return familyFacade.getMotherBySonId(sonId);
    }

}

LOG - Propagation.SUPPORTS

2020-04-05 17:57:08.102  INFO 8072 --- [           main] o.s.t.c.transaction.TransactionContext   : Began transaction (1) for test context [DefaultTestContext@e15b7e8 testClass = DemoUsingModelMapperAndOrikaApplicationTests, testInstance = com.example.family.demo.DemoUsingModelMapperAndOrikaApplicationTests@43f7f48d, testMethod = testAddSonsToMother@DemoUsingModelMapperAndOrikaApplicationTests, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@1b2abca6 testClass = DemoUsingModelMapperAndOrikaApplicationTests, locations = '{}', classes = '{class com.example.family.demo.DemoUsingModelMapperAndOrikaApplication, class com.example.family.demo.DemoUsingModelMapperAndOrikaApplication}', contextInitializerClasses = '[]', activeProfiles = '{test}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@4c9f8c13, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@be64738, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@2667f029, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@4ac3c60d], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true]]; transaction manager [org.springframework.orm.jpa.JpaTransactionManager@6fe9c048]; rollback [true]
2020-04-05 17:57:08.181 TRACE 8072 --- [           main] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.example.family.demo.family.facade.FamilyFacade.saveMother]
2020-04-05 17:57:08.193 TRACE 8072 --- [           main] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.example.family.demo.family.service.FamilyServiceImpl.saveMother]
2020-04-05 17:57:08.204 TRACE 8072 --- [           main] o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
2020-04-05 16:21:35.110 TRACE 29840 --- [           main] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.example.family.demo.family.facade.FamilyFacade.getMotherBySonId]
2020-04-05 16:21:35.111 TRACE 29840 --- [           main] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.example.family.demo.family.service.FamilyServiceImpl.getMotherBySonId]
2020-04-05 16:21:35.112 TRACE 29840 --- [           main] o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findMotherBySonId]
Hibernate: select distinct mother0_.mother_id as mother_i1_0_, mother0_.mother_age as mother_a2_0_, mother0_.mother_name as mother_n3_0_ from mother mother0_ inner join son sons1_ on mother0_.mother_id=sons1_.mother_id where sons1_.son_id=?
2020-04-05 16:21:35.136 TRACE 29840 --- [           main] o.s.t.i.TransactionInterceptor           : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findMotherBySonId]
2020-04-05 16:21:35.143 TRACE 29840 --- [           main] o.s.t.i.TransactionInterceptor           : Completing transaction for [com.example.family.demo.family.service.FamilyServiceImpl.getMotherBySonId]
Hibernate: select sons0_.mother_id as mother_i7_1_0_, sons0_.son_id as son_id1_1_0_, sons0_.son_id as son_id1_1_1_, sons0_.mother_id as mother_i7_1_1_, sons0_.son_age as son_age2_1_1_, sons0_.son_birth_date as son_birt3_1_1_, sons0_.son_name as son_name4_1_1_, sons0_.son_notes as son_note5_1_1_, sons0_.son_type as son_type6_1_1_ from son sons0_ where sons0_.mother_id=?
2020-04-05 16:21:35.168 TRACE 29840 --- [           main] o.s.t.i.TransactionInterceptor           : Completing transaction for [com.example.family.demo.family.facade.FamilyFacade.getMotherBySonId]
2020-04-05 16:21:35.185  INFO 29840 --- [           main] o.s.t.c.transaction.TransactionContext   : Rolled back transaction for test: [DefaultTestContext@342c38f8 testClass = DemoUsingModelMapperAndOrikaApplicationTests, testInstance = com.example.family.demo.DemoUsingModelMapperAndOrikaApplicationTests@3966c679, testMethod = testAddSonsToMother@DemoUsingModelMapperAndOrikaApplicationTests, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@c88a337 testClass = DemoUsingModelMapperAndOrikaApplicationTests, locations = '{}', classes = '{class com.example.family.demo.DemoUsingModelMapperAndOrikaApplication, class com.example.family.demo.DemoUsingModelMapperAndOrikaApplication}', contextInitializerClasses = '[]', activeProfiles = '{test}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@48e4374, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@4a22f9e2, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@9225652, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@4f209819], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true]]

LOG - Propagation.REQUIRED

2020-04-05 17:57:08.102  INFO 8072 --- [           main] o.s.t.c.transaction.TransactionContext   : Began transaction (1) for test context [DefaultTestContext@e15b7e8 testClass = DemoUsingModelMapperAndOrikaApplicationTests, testInstance = com.example.family.demo.DemoUsingModelMapperAndOrikaApplicationTests@43f7f48d, testMethod = testAddSonsToMother@DemoUsingModelMapperAndOrikaApplicationTests, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@1b2abca6 testClass = DemoUsingModelMapperAndOrikaApplicationTests, locations = '{}', classes = '{class com.example.family.demo.DemoUsingModelMapperAndOrikaApplication, class com.example.family.demo.DemoUsingModelMapperAndOrikaApplication}', contextInitializerClasses = '[]', activeProfiles = '{test}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@4c9f8c13, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@be64738, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@2667f029, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@4ac3c60d], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true]]; transaction manager [org.springframework.orm.jpa.JpaTransactionManager@6fe9c048]; rollback [true]
2020-04-05 17:57:08.181 TRACE 8072 --- [           main] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.example.family.demo.family.facade.FamilyFacade.saveMother]
2020-04-05 17:57:08.193 TRACE 8072 --- [           main] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.example.family.demo.family.service.FamilyServiceImpl.saveMother]
2020-04-05 17:57:08.204 TRACE 8072 --- [           main] o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
2020-04-05 16:24:16.772 TRACE 29946 --- [           main] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.example.family.demo.family.facade.FamilyFacade.getMotherBySonId]
2020-04-05 16:24:16.773 TRACE 29946 --- [           main] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.example.family.demo.family.service.FamilyServiceImpl.getMotherBySonId]
2020-04-05 16:24:16.774 TRACE 29946 --- [           main] o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findMotherBySonId]
Hibernate: select distinct mother0_.mother_id as mother_i1_0_, mother0_.mother_age as mother_a2_0_, mother0_.mother_name as mother_n3_0_ from mother mother0_ inner join son sons1_ on mother0_.mother_id=sons1_.mother_id where sons1_.son_id=?
2020-04-05 16:24:16.781 TRACE 29946 --- [           main] o.s.t.i.TransactionInterceptor           : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findMotherBySonId]
2020-04-05 16:24:16.781 TRACE 29946 --- [           main] o.s.t.i.TransactionInterceptor           : Completing transaction for [com.example.family.demo.family.service.FamilyServiceImpl.getMotherBySonId]
2020-04-05 16:24:16.781 TRACE 29946 --- [           main] o.s.t.i.TransactionInterceptor           : Completing transaction for [com.example.family.demo.family.facade.FamilyFacade.getMotherBySonId]
2020-04-05 16:24:16.802  INFO 29946 --- [           main] o.s.t.c.transaction.TransactionContext   : Rolled back transaction for test: [DefaultTestContext@342c38f8 testClass = DemoUsingModelMapperAndOrikaApplicationTests, testInstance = com.example.family.demo.DemoUsingModelMapperAndOrikaApplicationTests@447543ee, testMethod = testAddSonsToMother@DemoUsingModelMapperAndOrikaApplicationTests, testException = org.opentest4j.AssertionFailedError: expected: <true> but was: <false>, mergedContextConfiguration = [WebMergedContextConfiguration@c88a337 testClass = DemoUsingModelMapperAndOrikaApplicationTests, locations = '{}', classes = '{class com.example.family.demo.DemoUsingModelMapperAndOrikaApplication, class com.example.family.demo.DemoUsingModelMapperAndOrikaApplication}', contextInitializerClasses = '[]', activeProfiles = '{test}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@48e4374, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@4a22f9e2, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@9225652, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@4f209819], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.populatedRequestContextHolder' -> true, 'org.springframework.test.context.web.ServletTestExecutionListener.resetRequestContextHolder' -> true]]