This might seem lengthy but it is a simple question. Most of it is standard boilerplate code which you don't have to read but I'm adding it here in case I made a mistake when following tutorials.
The actual problem: I have specified in bold.
I am working with a many-to-many relationship with 2 tables: subjects
and students
. I have already defined the database schema for subjects
and students
with separate table subject_student
for many-to-many relationship.
The table schema is as follows:
create table subjects
(
id int not null auto_increment,
name varchar(100) not null unique,
primary key (id)
);
create table students
(
id int not null auto_increment,
name varchar(100) not null,
passport_id int not null unique,
primary key (id),
foreign key (passport_id) references passports (id)
);
create table subject_student
(
subject_id int not null,
student_id int not null,
primary key (subject_id, student_id),
foreign key (subject_id) references subjects (id),
foreign key (student_id) references students (id)
);
The entities as follows:
@Entity
@Table(name = "students")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(nullable = false, length = 100)
private String name;
@OneToOne(fetch = FetchType.LAZY)
private Passport passport;
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "subject_student",
joinColumns = @JoinColumn(name = "student_id"), // join column on owning side
inverseJoinColumns = @JoinColumn(name = "subject_id") // join column on other side
)
@ToString.Exclude
private List<Subject> subjects = new ArrayList<>();
// constructors
public void addSubject(Subject subject) {
subjects.add(subject);
}
public void removeSubject(Subject subject) {
subjects.remove(subject);
}
}
@Entity
@Table(name = "subjects")
public class Subject {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(nullable = false, length = 100, unique = true)
private String name;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "subject")
private List<Review> reviews = new ArrayList<>();
@ManyToMany(fetch = FetchType.LAZY, mappedBy = "subjects")
@ToString.Exclude
private List<Student> students = new ArrayList<>();
public Subject(String name) {
this.name = name;
}
public void addReview(Review review) {
reviews.add(review);
}
public void addStudent(Student student) {
students.add(student);
}
}
Now to the problem: Here Student entity is the owning part.
I wanted to add students to a particular subject. So I did following:
@Transactional
public class SubjectRepository {
@PersistenceContext
EntityManager em;
public void addStudentsToSubject(int subjectId, List<Student> students) {
Subject subject = findById(subjectId);
students.forEach(student -> {
subject.addStudent(student);
student.addSubject(subject);
em.persist(student);
});
em.persist(subject);
}
}
But when I run the code, it runs fine but in the end JPA Rollbacks. So when I see database, the joined table has no new rows. This happens only when I try to add students to subject i.e., trying to go through non-owning side. The proper way (adding subjects to students works fine. That too is very similar to this piece of code).
This leaves me confused.
Now I have a suspect:
The join table is subject_student
but according to convention it should've been student_subject
. Is that the culprit?
Or is there any deeper reasons why this doesn't work?
I'm adding the driver code as well here.
@Test
@Transactional
public void test_addStudentsToSubject() {
// adding students to non-owning side subject
int subjectId = 10_002;
Student s1 = studentRepository.findById(20_002);
Student s2 = studentRepository.findById(20_003);
List<Student> students = Arrays.asList(s1, s2);
subjectRepository.addStudentsToSubject(subjectId, students);
}
@Test
@Transactional
public void getSubjectsAndStudent() {
int id = 10_002;
Subject subject = subjectRepository.findById(id);
log.info("Subject = {}", subject);
List<Student> students = subject.getStudents();
log.info("Subject = {}, taken students = {}", subject, students);
}
EDIT: adding logs for first test:
2021-12-27 22:54:02.015 INFO 12352 --- [ main] s.l.j.r.relationship.ManyToManyTests : Starting ManyToManyTests using Java 17 on Ahroo with PID 12352 (started by msi in E:\Code\Tutorials\jpa_hibernate)
2021-12-27 22:54:02.017 INFO 12352 --- [ main] s.l.j.r.relationship.ManyToManyTests : No active profile set, falling back to default profiles: default
2021-12-27 22:54:02.551 INFO 12352 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2021-12-27 22:54:02.567 INFO 12352 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 9 ms. Found 0 JPA repository interfaces.
2021-12-27 22:54:03.047 INFO 12352 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
2021-12-27 22:54:03.089 INFO 12352 --- [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 5.6.3.Final
2021-12-27 22:54:03.209 INFO 12352 --- [ main] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
2021-12-27 22:54:03.322 INFO 12352 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2021-12-27 22:54:03.615 INFO 12352 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2021-12-27 22:54:03.636 INFO 12352 --- [ main] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.MySQL8Dialect
2021-12-27 22:54:04.258 INFO 12352 --- [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2021-12-27 22:54:04.335 INFO 12352 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2021-12-27 22:54:05.102 INFO 12352 --- [ main] s.l.j.r.relationship.ManyToManyTests : Started ManyToManyTests in 3.376 seconds (JVM running for 4.431)
2021-12-27 22:54:05.103 INFO 12352 --- [ main] s.l.j.JpaHibernateApplication : ----------------------------------------------------------------------------------------------------------
2021-12-27 22:54:05.173 INFO 12352 --- [ main] o.s.t.c.transaction.TransactionContext : Began transaction (1) for test context [DefaultTestContext@2a225dd7 testClass = ManyToManyTests, testInstance = spring.learn.jpa_hibernate.repository.relationship.ManyToManyTests@155318b5, testMethod = test_addStudentsToSubject_Method2@ManyToManyTests, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@61eaec38 testClass = ManyToManyTests, locations = '{}', classes = '{class spring.learn.jpa_hibernate.JpaHibernateApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@3d1cfad4, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@2e55dd0c, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@625732, org.springframework.boot.test.autoconfigure.actuate.metrics.MetricsExportContextCustomizerFactory$DisableMetricExportContextCustomizer@4e096385, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@793be5ca, org.springframework.boot.test.context.SpringBootTestArgs@1, org.springframework.boot.test.context.SpringBootTestWebEnvironment@1554909b], 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, 'org.springframework.test.context.event.ApplicationEventsTestExecutionListener.recordApplicationEvents' -> false]]; transaction manager [org.springframework.orm.jpa.JpaTransactionManager@41d53813]; rollback [true]
Hibernate: select student0_.id as id1_5_0_, student0_.name as name2_5_0_, student0_.passport_id as passport3_5_0_ from students student0_ where student0_.id=?
Hibernate: select student0_.id as id1_5_0_, student0_.name as name2_5_0_, student0_.passport_id as passport3_5_0_ from students student0_ where student0_.id=?
Hibernate: select subject0_.id as id1_7_0_, subject0_.name as name2_7_0_ from subjects subject0_ where subject0_.id=?
Hibernate: select subjects0_.student_id as student_1_6_0_, subjects0_.subject_id as subject_2_6_0_, subject1_.id as id1_7_1_, subject1_.name as name2_7_1_ from subject_student subjects0_ inner join subjects subject1_ on subjects0_.subject_id=subject1_.id where subjects0_.student_id=?
Hibernate: select subjects0_.student_id as student_1_6_0_, subjects0_.subject_id as subject_2_6_0_, subject1_.id as id1_7_1_, subject1_.name as name2_7_1_ from subject_student subjects0_ inner join subjects subject1_ on subjects0_.subject_id=subject1_.id where subjects0_.student_id=?
2021-12-27 22:54:05.435 INFO 12352 --- [ main] o.h.c.i.AbstractPersistentCollection : HHH000496: Detaching an uninitialized collection with queued operations from a session: [spring.learn.jpa_hibernate.entity.relationship.Subject.students#10002]
2021-12-27 22:54:05.437 INFO 12352 --- [ main] i.StatisticalLoggingSessionEventListener : Session Metrics {
482901 nanoseconds spent acquiring 1 JDBC connections;
0 nanoseconds spent releasing 0 JDBC connections;
15821999 nanoseconds spent preparing 5 JDBC statements;
14897401 nanoseconds spent executing 5 JDBC statements;
0 nanoseconds spent executing 0 JDBC batches;
0 nanoseconds spent performing 0 L2C puts;
0 nanoseconds spent performing 0 L2C hits;
0 nanoseconds spent performing 0 L2C misses;
0 nanoseconds spent executing 0 flushes (flushing a total of 0 entities and 0 collections);
0 nanoseconds spent executing 0 partial-flushes (flushing a total of 0 entities and 0 collections)
}
2021-12-27 22:54:05.437 INFO 12352 --- [ main] o.s.t.c.transaction.TransactionContext : Rolled back transaction for test: [DefaultTestContext@2a225dd7 testClass = ManyToManyTests, testInstance = spring.learn.jpa_hibernate.repository.relationship.ManyToManyTests@155318b5, testMethod = test_addStudentsToSubject_Method2@ManyToManyTests, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@61eaec38 testClass = ManyToManyTests, locations = '{}', classes = '{class spring.learn.jpa_hibernate.JpaHibernateApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@3d1cfad4, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@2e55dd0c, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@625732, org.springframework.boot.test.autoconfigure.actuate.metrics.MetricsExportContextCustomizerFactory$DisableMetricExportContextCustomizer@4e096385, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@793be5ca, org.springframework.boot.test.context.SpringBootTestArgs@1, org.springframework.boot.test.context.SpringBootTestWebEnvironment@1554909b], 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, 'org.springframework.test.context.event.ApplicationEventsTestExecutionListener.recordApplicationEvents' -> false]]
2021-12-27 22:54:05.450 INFO 12352 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2021-12-27 22:54:05.452 INFO 12352 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2021-12-27 22:54:05.460 INFO 12352 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
logs for 2nd test (which retrieves modified values):
22:56:51.185 [main] DEBUG org.springframework.test.context.support.TestPropertySourceUtils - Adding inlined properties to environment: {spring.jmx.enabled=false, org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}
2021-12-27 22:56:51.463 INFO 12240 --- [ main] s.l.j.r.relationship.ManyToManyTests : Starting ManyToManyTests using Java 17 on Ahroo with PID 12240 (started by msi in E:\Code\Tutorials\jpa_hibernate)
2021-12-27 22:56:51.465 INFO 12240 --- [ main] s.l.j.r.relationship.ManyToManyTests : No active profile set, falling back to default profiles: default
2021-12-27 22:56:51.996 INFO 12240 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2021-12-27 22:56:52.016 INFO 12240 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 12 ms. Found 0 JPA repository interfaces.
2021-12-27 22:56:52.507 INFO 12240 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
2021-12-27 22:56:52.557 INFO 12240 --- [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 5.6.3.Final
2021-12-27 22:56:52.683 INFO 12240 --- [ main] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
2021-12-27 22:56:52.790 INFO 12240 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2021-12-27 22:56:53.099 INFO 12240 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2021-12-27 22:56:53.121 INFO 12240 --- [ main] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.MySQL8Dialect
2021-12-27 22:56:53.781 INFO 12240 --- [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2021-12-27 22:56:53.857 INFO 12240 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2021-12-27 22:56:54.656 INFO 12240 --- [ main] s.l.j.r.relationship.ManyToManyTests : Started ManyToManyTests in 3.47 seconds (JVM running for 4.387)
2021-12-27 22:56:54.658 INFO 12240 --- [ main] s.l.j.JpaHibernateApplication : ----------------------------------------------------------------------------------------------------------
2021-12-27 22:56:54.723 INFO 12240 --- [ main] o.s.t.c.transaction.TransactionContext : Began transaction (1) for test context [DefaultTestContext@2a225dd7 testClass = ManyToManyTests, testInstance = spring.learn.jpa_hibernate.repository.relationship.ManyToManyTests@37393dab, testMethod = getSubjectsAndStudent@ManyToManyTests, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@61eaec38 testClass = ManyToManyTests, locations = '{}', classes = '{class spring.learn.jpa_hibernate.JpaHibernateApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@3d1cfad4, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@2e55dd0c, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@625732, org.springframework.boot.test.autoconfigure.actuate.metrics.MetricsExportContextCustomizerFactory$DisableMetricExportContextCustomizer@4e096385, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@793be5ca, org.springframework.boot.test.context.SpringBootTestArgs@1, org.springframework.boot.test.context.SpringBootTestWebEnvironment@1554909b], 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, 'org.springframework.test.context.event.ApplicationEventsTestExecutionListener.recordApplicationEvents' -> false]]; transaction manager [org.springframework.orm.jpa.JpaTransactionManager@4cbb11e4]; rollback [true]
Hibernate: select subject0_.id as id1_7_0_, subject0_.name as name2_7_0_ from subjects subject0_ where subject0_.id=?
2021-12-27 22:56:54.949 INFO 12240 --- [ main] s.l.j.r.relationship.ManyToManyTests : Subject = Subject(id=10002, name=History)
Hibernate: select students0_.subject_id as subject_2_6_0_, students0_.student_id as student_1_6_0_, student1_.id as id1_5_1_, student1_.name as name2_5_1_, student1_.passport_id as passport3_5_1_ from subject_student students0_ inner join students student1_ on students0_.student_id=student1_.id where students0_.subject_id=?
2021-12-27 22:56:54.951 INFO 12240 --- [ main] s.l.j.r.relationship.ManyToManyTests : Subject = Subject(id=10002, name=History), taken students = [Student(id=20001, name=Adam)]
2021-12-27 22:56:54.976 INFO 12240 --- [ main] i.StatisticalLoggingSessionEventListener : Session Metrics {
497999 nanoseconds spent acquiring 1 JDBC connections;
0 nanoseconds spent releasing 0 JDBC connections;
18084498 nanoseconds spent preparing 2 JDBC statements;
11998799 nanoseconds spent executing 2 JDBC statements;
0 nanoseconds spent executing 0 JDBC batches;
0 nanoseconds spent performing 0 L2C puts;
0 nanoseconds spent performing 0 L2C hits;
0 nanoseconds spent performing 0 L2C misses;
0 nanoseconds spent executing 0 flushes (flushing a total of 0 entities and 0 collections);
0 nanoseconds spent executing 0 partial-flushes (flushing a total of 0 entities and 0 collections)
}
2021-12-27 22:56:54.977 INFO 12240 --- [ main] o.s.t.c.transaction.TransactionContext : Rolled back transaction for test: [DefaultTestContext@2a225dd7 testClass = ManyToManyTests, testInstance = spring.learn.jpa_hibernate.repository.relationship.ManyToManyTests@37393dab, testMethod = getSubjectsAndStudent@ManyToManyTests, testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@61eaec38 testClass = ManyToManyTests, locations = '{}', classes = '{class spring.learn.jpa_hibernate.JpaHibernateApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@3d1cfad4, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@2e55dd0c, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@625732, org.springframework.boot.test.autoconfigure.actuate.metrics.MetricsExportContextCustomizerFactory$DisableMetricExportContextCustomizer@4e096385, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@793be5ca, org.springframework.boot.test.context.SpringBootTestArgs@1, org.springframework.boot.test.context.SpringBootTestWebEnvironment@1554909b], 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, 'org.springframework.test.context.event.ApplicationEventsTestExecutionListener.recordApplicationEvents' -> false]]
2021-12-27 22:56:54.989 INFO 12240 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2021-12-27 22:56:54.991 INFO 12240 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2021-12-27 22:56:54.999 INFO 12240 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
CodePudding user response:
persist
is used only for saving new entities. If you did findById
with the database id, this means that the entity already existed in the database, so you don't need to persist.
If you try to persist and already created entity you should be getting an EntityExistsException
and that must be the Rollback perpetrator.
Actually, if you marked the method (or class) as @Transactional
it means that at the end of it, if no exceptions, the entities changes will be persisted in the database without having to do anything else.
It case the entities has been obtained in another Persistence Context (that is, the EntityManager
instance), you should use merge
to attach them to the current Persistence Context. That looks the situation of you Student
s list.
So, you method should look like this:
public void addStudentsToSubject(int subjectId, List<Student> students) {
Subject subject = findById(subjectId);
students.forEach(student -> {
em.merge(student);
subject.addStudent(student);
student.addSubject(subject);
});
}
Update
Spring test by default rollbacks transactions. Just adding @Rollback(false)
to your @Test
should be enough.You can add it at class level too.