일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- wildfly
- dbeaver
- gson
- springboot
- Java
- gradle
- BPMN
- NCP
- Spring
- Windows
- jetbrains
- JavaScript
- useEffect
- nodejs
- react
- log4j2
- database
- IntelliJ
- JPA
- LOG4J
- docker
- mybatis
- Kubernetes
- intellijIDEA
- MySQL
- kubectl
- nginx
- VSCode
- Git
- tibero
- Today
- Total
두 손끝의 창조자
JPA Test를 위한 @BeforeEach 와 트랜잭션 분리 본문
구조
Car
- Body
< Part
구조가 있을 때, 즉 Car
는Body
와 1:1,Body
와 Part
는 1:n 관계가 있다.
JPA에서는 모두 양방향 참조로 Car에서 Part 까지 접근 가능하고, 연관관계 주인은 Car
-Body
에서는Body
가,Body
< Part
에서는Part
가 가지고 있다.
엔티티 저장을 편리하게 하기위해서 모든 OneToXXX 는 cascade 모드를 ALL
로 하였다.
각 엔티티는 Audit 처리를 위해 Audit
MappedSuperclass를 상속하고 이 클래스는 int 타입의 version
필드를 가지고 있다.
이 Audit
엔티티에 @EntityListeners
를 등록해서 Create, Update 시 버전을 올릴려고 AuditListener
을 구현하고 참조한다.
Car
package hellojpa.car;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import static javax.persistence.CascadeType.ALL;
@Entity
public class Car extends Audit {
@Id
private Long id;
@OneToOne(mappedBy = "car", cascade = ALL)
private Body body;
public Car() {
}
public Car(Long id) {
this.id = id;
}
public Body getBody() {
return body;
}
public void setBody(Body body) {
this.body = body;
body.setCar(this);
}
}
Body
package hellojpa.car;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
import static javax.persistence.CascadeType.ALL;
@Entity
public class Body extends Audit {
@Id
@GeneratedValue
private Long id;
@OneToMany(cascade = ALL, mappedBy = "body")
private List<Part> parts = new ArrayList<>();
@OneToOne
private Car car;
public void setCar(Car car) {
this.car = car;
}
public void addPart(Part part) {
this.parts.add(part);
part.setBody(this);
}
public List<Part> getParts() {
return parts;
}
}
Part
package hellojpa.car;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
@Entity
public class Part extends Audit {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne
private Body body;
public void setName(String name) {
this.name = name;
}
public void setBody(Body body) {
this.body = body;
}
}
AuditListener
package hellojpa.car;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
public class AuditListener {
@PrePersist
public void initVersion(Audit audit) {
audit.setVersion(0);
}
@PreUpdate
public void increaseVersion(Audit audit) {
audit.setVersion(audit.getVersion() + 1);
}
}
Audit
package hellojpa.car;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
@MappedSuperclass
@EntityListeners(AuditListener.class)
public class Audit {
private Integer version;
public void setVersion(Integer version) {
this.version = version;
}
public Integer getVersion() {
return version;
}
}
CarRepository
package hellojpa.car;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CarRepository extends JpaRepository<Car, Long> {
}
문제
오딧 테스트를 위해 @BeforeEach 애노테이션으로 데이터를 넣고 @Test 애노테이션으로 테스트를 수행했다.Car
를 리포지토리에서 가져와서 Body
를 통해 Part
를 가져왔고 Part
의 이름을 업데이트 했다.
기대하는 값은 Part
테이블의 version
필드가 1
이 되는 것이다.
그런데 Part
테이블은 정상적으로 1
로 업데이트 됐는데, Car
테이블의 버전도 1
로 바뀌었다. 기대하지 않은 업데이트다.
테스트 코드
package hellojpa;
import hellojpa.car.Body;
import hellojpa.car.Car;
import hellojpa.car.CarRepository;
import hellojpa.car.Part;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
@SpringBootTest
@Rollback(value = false)
@EnableJpaRepositories(basePackages = {"hellojpa"})
@Transactional
public class CarTest {
@Autowired
CarRepository carRepository;
@BeforeEach
void addProc() {
Part part = new Part();
Body body = new Body();
body.addPart(part);
Car car = new Car(1L);
car.setBody(body);
carRepository.save(car);
}
@Test
void doThing() {
Car car = carRepository.findById(1L).get();
car.getBody().getParts().get(0).setName("break");
}
}
실행결과 로그
실행 환경은 스프링부트와 Spring-data-jpa를 사용했는데 혹시 Spring-data-jpa 문제인가 해서 플래인 jpa 프로젝트를 만들어서 똑같은 엔티티를 만들고 main 함수에 엔티티 매니저를 이용해서 기초 데이터를 넣은 후 업데이트 했을때 기대한 대로 Part
테이블만 1
로 업데이트 되었다.
플래인 JPA 테스트 코드
package car;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
new JpaTemplate() {
@Override
void run(EntityManager em) {
Part part = new Part();
Body body = new Body();
body.addPart(part);
Car car = new Car(1L);
car.setBody(body);
em.persist(car);
}
}.execute(emf);
new JpaTemplate() {
@Override
void run(EntityManager em) {
Car car = em.find(Car.class, 1L);
car.getBody().getParts().get(0).setName("break");
}
}.execute(emf);
emf.close();
}
static abstract class JpaTemplate {
public void execute(EntityManagerFactory emf) {
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin();
run(em);
transaction.commit();
em.close();
}
abstract void run(EntityManager em);
}
}
실행결과
더 이해하기 힘든것은 Car
엔티티의 ID를 기존에는 생성자에서 받아서 객체를 만들도록 했는데 @GeneratedValue
로 자동으로 발번하게 하면 또 spring-data-jpa에서도 정상적으로 Part
테이블만 버전이 업데이트 되었다.
실행결과
그래서 트랜잭션의 문제인가 해서 다시 @GeneratedValue
를 지우고 id를 외부에서 입력하게 한 뒤에 @BeforeEach
에 있는 메소드에 @Transactional
을 붙이고 @Test
메소드에 @Transactional
따로 붙여서 해봤는데 여전히 테이블 둘다 업데이트 되었다.
무언가 두 메소드가 구분이 안되는것 같아 데이터를 따로 테이블어 넣어놓고, @BeforeEach
를 삭제한 뒤에 @Test
메소드만 실행하면 또 원하는데로 업데이트가 잘 되었다. 무엇이 문제인가?
@BeforeEach
메소드만 EntityManager
를 직접 사용하게 변경해봤다.
package hellojpa;
import hellojpa.car.Body;
import hellojpa.car.Car;
import hellojpa.car.CarRepository;
import hellojpa.car.Part;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
@SpringBootTest
@Rollback(value = false)
@EnableJpaRepositories(basePackages = {"hellojpa"})
@Transactional
public class CarTest {
@Autowired
CarRepository carRepository;
@PersistenceContext
EntityManager entityManager;
@BeforeEach
void addProc() {
Part part = new Part();
Body body = new Body();
body.addPart(part);
Car car = new Car(1L);
car.setBody(body);
// carRepository.save(car);
entityManager.persist(car);
}
@Test
void doThing() {
Car car = carRepository.findById(1L).get();
car.getBody().getParts().get(0).setName("break");
}
}
이건 또 원하는데로 잘 되었다.
무엇이 문제인가?
쿠쥬플리즈 헬프미?