ManyToMany 关系的非拥有部分在尝试通过它持久化实体时似乎不起作用
The non-owning part of ManyToMany relationship seems to not work when trying to persist entities through it
这可能看起来很长,但这是一个简单的问题。其中大部分是您不必阅读的标准样板代码,但我将其添加到此处以防我在学习教程时出错。
实际问题:我在粗体.
中指定
我正在与 2 个 table 建立多对多关系:subjects
和 students
。我已经为 subjects
和 students
定义了数据库模式,并为多对多关系定义了单独的 table subject_student
。
table架构如下:
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)
);
实体如下:
@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);
}
}
现在问题来了: 这里 Student 实体是拥有部分。
我想将学生添加到特定科目。所以我做了以下操作:
@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);
}
}
但是当我 运行 代码时,它 运行 没问题,但最终 JPA 回滚。所以当我看到数据库时,加入的 table 没有新行。只有当我尝试将学生添加到主题时才会发生这种情况,即 尝试通过非拥有方 。正确的方法(给学生添加科目很好。这也和这段代码非常相似)。
这让我很困惑。
现在我有一个怀疑:
联接 table 是 subject_student
,但根据惯例它应该是 student_subject
。那是罪魁祸首吗?
或者是否有更深层次的原因导致这不起作用?
我也在此处添加驱动程序代码。
@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);
}
编辑:为第一次测试添加日志:
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.
第二次测试的日志(检索修改后的值):
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.
persist
仅用于保存新实体。如果你对数据库id做了findById
,这意味着该实体已经存在于数据库中,所以你不需要持久化。
如果您尝试保留已创建的实体,您应该会得到一个 EntityExistsException
并且那一定是回滚的肇事者。
实际上,如果您将方法(或class)标记为@Transactional
,这意味着在它结束时,如果没有异常,实体更改将持久保存在数据库中,而无需做其他事情。
如果实体已经在另一个持久化上下文(即 EntityManager
实例)中获得,您应该使用 merge
将它们附加到当前持久化上下文。这看起来是你 Student
列表的情况。
所以,您的方法应该如下所示:
public void addStudentsToSubject(int subjectId, List<Student> students) {
Subject subject = findById(subjectId);
students.forEach(student -> {
em.merge(student);
subject.addStudent(student);
student.addSubject(subject);
});
}
更新
Spring 测试默认回滚事务。只需将 @Rollback(false)
添加到您的 @Test
应该是 enough.You 也可以将其添加到 class 级别。
这可能看起来很长,但这是一个简单的问题。其中大部分是您不必阅读的标准样板代码,但我将其添加到此处以防我在学习教程时出错。
实际问题:我在粗体.
中指定我正在与 2 个 table 建立多对多关系:subjects
和 students
。我已经为 subjects
和 students
定义了数据库模式,并为多对多关系定义了单独的 table subject_student
。
table架构如下:
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)
);
实体如下:
@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);
}
}
现在问题来了: 这里 Student 实体是拥有部分。
我想将学生添加到特定科目。所以我做了以下操作:
@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);
}
}
但是当我 运行 代码时,它 运行 没问题,但最终 JPA 回滚。所以当我看到数据库时,加入的 table 没有新行。只有当我尝试将学生添加到主题时才会发生这种情况,即 尝试通过非拥有方 。正确的方法(给学生添加科目很好。这也和这段代码非常相似)。
这让我很困惑。
现在我有一个怀疑:
联接 table 是 subject_student
,但根据惯例它应该是 student_subject
。那是罪魁祸首吗?
或者是否有更深层次的原因导致这不起作用?
我也在此处添加驱动程序代码。
@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);
}
编辑:为第一次测试添加日志:
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.
第二次测试的日志(检索修改后的值):
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.
persist
仅用于保存新实体。如果你对数据库id做了findById
,这意味着该实体已经存在于数据库中,所以你不需要持久化。
如果您尝试保留已创建的实体,您应该会得到一个 EntityExistsException
并且那一定是回滚的肇事者。
实际上,如果您将方法(或class)标记为@Transactional
,这意味着在它结束时,如果没有异常,实体更改将持久保存在数据库中,而无需做其他事情。
如果实体已经在另一个持久化上下文(即 EntityManager
实例)中获得,您应该使用 merge
将它们附加到当前持久化上下文。这看起来是你 Student
列表的情况。
所以,您的方法应该如下所示:
public void addStudentsToSubject(int subjectId, List<Student> students) {
Subject subject = findById(subjectId);
students.forEach(student -> {
em.merge(student);
subject.addStudent(student);
student.addSubject(subject);
});
}
更新
Spring 测试默认回滚事务。只需将 @Rollback(false)
添加到您的 @Test
应该是 enough.You 也可以将其添加到 class 级别。