This is the expected behavior when bi-directional associations are not synchronized.
1 transaction for the whole test: 1 persistence context
When using @DataJpaTest alone, the test method executes with 1 transaction, 1 persistence context, 1 first-level cache, because @Transactional is applied on all test methods.
In this case, the first classroom and the one returned by classRoomRepository.getClassRoom(classroom.getId()) are the same instance because Hibernate uses its first-level cache to return the classroom instance, which was constructed with an empty Set, and ignores the ResultSet record from your query.
It can be verified:
Classroom classroom = new Classroom(); // constructs the Classroom with an empty Set
classRoomRepository.save(classroom);
Classroom classroom2 = classRoomRepository.getClassRoom(classroom.getId());
System.out.println("same? " + (classroom==classroom2));
// output: same? true
// and classroom2.persons is empty :)
The fix: bi-directional association synchronization
As Hibernate ignores your query result, the @OneToMany is still empty after the query.
In other words, you "forgot" to add the person in Classroom.persons.
You'll have to manually synchronize your bi-directional association in the setters and adders (or any method that manipulates these associations, including your constructor), or use Hibernate bytecode enhancement with enableAssociationManagement (magic, but use carefully).
Let's write a (fluent) Classroom.addPerson adder that adds a Person in this Classroom, and updates the Person:
public Classroom addPerson(Person person) {
this.persons.add(person);
person.setClassroom(this);
return this;
}
Note that you should also add a Classroom.removePerson method, that sets Person.classroom to null after removing the person from the Set.
Then rewrite your test to make it pass:
Classroom classroom = new Classroom();
classRoomRepository.save(classroom);
Person person = new Person();
classroom.addPerson(person);
personRepository.save(person);
In this case, you manually added the person to the set and kept the other side of the association in sync, which is a natural way of doing things.
But if you want to stick with your Person(Classroom classroom) constructor:
public Person(Classroom classroom) {
classroom.addPerson(this); // add this person to the classroom
}
If you want to be able to manipulate this association in both ways, you could also use a Person.setClassroom setter but it's a bit heavy:
public Person setClassroom(Classroom classroom) {
this.classroom = classroom;
if(classroom != null)
this.classroom.getPersons().add(this);
else
this.classroom.getPersons().remove(this);
return this;
}
You manually kept both sides of the association in sync, so you're not relying on Hibernate fetching the collection.
Your test will pass, and I added a check to ensure that the association is in sync:
Classroom classroom = new Classroom();
classRoomRepository.save(classroom);
Person person = new Person(classroom);
// check that the classroom contains the person
Assertions.assertThat(classroom.getPersons().contains(person)).isTrue();
personRepository.save(person);
Assertions.assertThat(classRoomRepository.getClassRoom(classroom.getId())
.getPersons()).hasSize(1);
But keep in mind that the call to classRoomRepository.getClassRoom(classroom.getId()) is useless, as Hibernate ignores the result if it's already present in the persistence context.
You should only use your first classroom instance.
Multiple transactions: multiple persistence contexts
When you added @Transactional(propagation = Propagation.NOT_SUPPORTED), you chose not to use a transaction, so 3 transactions, 3 persistence contexts and 3 first-level caches are used (one for the first save, one for the second, and one for the query).
Same for @SpringBootTest which does not use @Transactional at all.
In these 2 cases, you're manipulating different instances, So Hibernates uses the ResultSet record from the query to provide your classroom with fetched persons, as expected.
System.out.println("same? " + (classroom==classroom2));
// output: same? false
For more information check this article, and Vlad's answer to Edison's question:
https://vladmihalcea.com/jpa-hibernate-first-level-cache/
If there’s already a managed entity with the same id, then the
ResultSet record is ignored.
You can also check https://vladmihalcea.com/jpa-hibernate-synchronize-bidirectional-entity-associations/ about bi-directional association synchronization.
If you're interested by Hibernate bytecode enhancement and magic bi-directional association synchronization, read https://docs.jboss.org/hibernate/orm/current/topical/html_single/bytecode/BytecodeEnhancement.html
@Transactional(propagation = Propagation.NOT_SUPPORTED)also worked for me. But I don't know the exact reason