如果在插入数据时发生任何错误,我想使用 JPA 在一个事务中保存两个实体,应该回滚更改

I want to save two entities in one transaction with JPA if any error occure while inserting the data the changes should be rolled back

例如我有两个实体:第一个是学生,另一个是地址。我想在两个具有外键关系的表上插入数据,但它们是一个条件:如果在任何表中插入数据时发生任何错误,所有更改都应回滚。

我该怎么做请帮忙

class Student{
   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private int id;

   private String firstName;

   @OneToOne(fetch = FetchType.EAGER)
   @JoinColumn(name="address_id",referencedColumnName = "id")
   private Address address;
}

class Address {

   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private int id;

   private String city;

}

@Service
public class StudentService {
    
    @Autowired
    private StudentRepo studentRepo;
    
    @Autowired
    private AddressRepo addressRepo;

    public Student createStudent(StudentRequest studentRequest) {
        
        Student student=new Student(studentRequest);
        
        Address address=new Address();
        
        address.setCity(studentRequest.getCity());
        address.setStreet(studentRequest.getStreet());
        address=addressRepo.save(address);
        
        student.setAddress(address);
        
        student=studentRepo.save(student);
        
        return student;
        
    }

这是一个非常笼统的问题,因此我会尽力为您指出正确的方向。为此,我假设:

  • 您正在使用 spring-data-jpa
  • 您已经配置了一个数据源
  • 您的应用程序正确启动
  • 您已经配置了 @Repository 或者您知道如何通过 @PersistenceContext
  • 使用 EntityManager
  • 我将采用 Repository 解决方案

好消息是:使用 OneToOneOneToManyManyToOneManyToMany 标记的持久对象是一项核心功能,因此其中大部分已经由 JPA/Hibernate 处理。您只需确保阅读文档遵循文档

第一步是理解Cascading。默认情况下,级联 打开。网络上有大量资源可以帮助您理解这一点,我将向您指出 this one。这意味着您需要在 OneToOne 注释上设置 cascade 属性(参见参考教程)。

Student.java

class Student{
   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private int id;

   private String firstName;

   @OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
   @JoinColumn(name="address_id",referencedColumnName = "id")
   private Address address;
}

然后,使用正常的 @Repository:

public interface StudentRepository extends JpaRepository<Student, Long> {
}

现在您已经拥有正确工作的映射和存储库。

完成此操作后,您应该准备好开始了,前提是上述假设成立。您现在可以为学生编写一个小测试用例,例如

@DataJpaTest
class StudentRepositoryTest {

@Autowired
private StudentRepository studentRepository;

@Autowired
private AddressRepository addressRepository;

@BeforeEach
void setup() {
    assumeThat(studentRepository).isNotNull();
    assumeThat(addressRepository).isNotNull();
}

@Test
void testSaveStudent {
   assumeThat(studentRepository.count()).isZero();
   assumeThat(addressRepository.count()).isZero();
   var student = new Student();
   var address = new Address();
   address.setCity("New York");
   student.setFirstName("Max");
   student.setAddress(address);
   studentRepository.save(student);
   assertThat(studentRepository.count()).isOne();
   assertThat(addressRepository.count()).isOne();
}

第二:关于您的交易问题。默认情况下,当您调用存储库的 save 方法时,会启动一个事务。因此,在最基本的情况下,您不一定需要交易。不过,一旦您处理了更复杂的业务案例,就有必要了解 JPA 的事务管理器。事务通常发生在服务层。有关交易的更多信息,请参阅下面的链接。

@Service
@Transactional(readOnly = true)
public class StudentService {
   @Autowired
   private StudentRepository studentRepository;

   @Transactional
   public Student assignStudentToAddress(Student s, Address a) {
      s.setAddress(a);
      return studentRepository.save(s);
   }
}

一些想法:

  • 你确定一个学生只能住在一个地址吗?这意味着生活大爆炸(合租公寓)中的生活安排将行不通。出于测试目的,这当然有效。
  • 考虑相反的情况:如果我们有一个地址屏幕并且我们想要将学生添加到其中怎么办?

更多资源:

使用无状态 EJB:

@Stateless
public StudentDAO {
    @PersistenceContext
    private EntityManager em;

    public Student createStudent(StudentRequest studentRequest) {

        Student student=new Student(studentRequest);

        Address address=new Address();

        address.setCity(studentRequest.getCity());
        address.setStreet(studentRequest.getStreet());
        em.persist(address);

        student.setAddress(address);
        em.persist(student);

        return student;

    }    
}