[JPA] TransientPropertyValueException 해결 방법

Woo Yong·2024년 3월 21일
0

JPA

목록 보기
4/7
post-thumbnail

Spring Data JPA에 대해서 공부하던 TransientPropertyValueException 오류에 대해서 정리해보려고한다.

OrdersUsers 엔티티를 작성했으며, 아래와 같이 코드를 작성했다.

Orders 엔티티

@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class Orders {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @ManyToOne
    Users user ;
}

Users 엔티티

@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
public class Users {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToMany(mappedBy = "user")
    @Builder.Default
    private List<Orders> orders = new ArrayList<>();
}

Users와 Orders는 1:N 양방향 관계를 가지고 있으며, Users는 여러개의 Orders를 가질 수 있고, Orders는 하나의 Users를 가질 수 있다.

그리고 Cascade 옵션을 테스트하기 위해 각 연관관계 필드에 옵션을 명시적으로 작성하지 않았다.

Cascade 옵션 없이 1쪽에서 save

양쪽에서 Cascade 옵션 없이 N쪽에서 저장하는 테스트 코드이다.
1개의 User 객체와 2개의 Orders를 생성하고, 각각의 관계 필드(연관관계 매핑 필드)에 할당해주었다.

    @Test
    public void savaTest1(){
        Users user = Users.builder().name("user").build();
        Orders order1 = Orders.builder().name("order1").user(user).build();
        Orders order2 = Orders.builder().name("order2").user(user).build();
        user.getOrders().add(order1);
        user.getOrders().add(order2);

        userDao.save(user);
    }

그리고 UserDao를 통해서 save() 메서드를 호출해보자.

console을 확인해보면 User 객체 하나만 insert 쿼리가 발생한 것을 볼 수 있다.

이러한 결과는 당연한 결과이다.
왜냐하면 Users와 Orders의 연관관계의 주인은 Users가 아니라 Orders이기 때문이다.

@OneToMany(mappedBy = "user")
@Builder.Default
private List<Orders> orders = new ArrayList<>();

Users가 가지고 있는 orders 필드에 작성된 @OneToMany()에서 mappedBy 속성으로 연관관계 주인을 설정해주었기 때문이다. (실제로 물리 DB에서 FK를 가지는 테이블을 연관관계 주인으로 설정되는 것이 권장되며 이에 대한 설명은 생략하도록 하겠다....)

Cascade 옵션 없이 N쪽에서 save (feat. TransientPropertyValueException)

그렇다면 연관관계의 주인인 Orders의 Dao 객체로 save를 수행해보자.

    @Test
    public void savaTest1(){
        Users user = Users.builder().name("user").build();
        Orders order1 = Orders.builder().name("order1").user(user).build();
        Orders order2 = Orders.builder().name("order2").user(user).build();
        user.getOrders().add(order1);
        user.getOrders().add(order2);

        orderDao.save(order1);
    }

나는 당연히 1개의 order와 user 객체에 대해서 insert 쿼리가 발생할 줄 알았다.

order의 객체에 대해서는 insert 쿼리가 발생했지만, org.hibernate.TransientPropertyValueException의 예외가 발생하였다.

원인을 찾아보니 FK로 쓰는 객체가 아직 저장이 안되서 발생하는 예외라고 한다.
Orders를 저장하는데 Users가 저장이 안되서 발생하는 오류라는 것 이다.

해결방법

위 예외를 해결하는 방법은 연관관계 주인의 관계 필드에 cascade = CascadeType.ALL, PERSIST을 설정해주면된다.

cascade 속성에 대해 간단하게 정리하면 부모 엔티티의 변경이 자식 엔티티에 어떻게 전파하는지 설정하는 것이다.

insert 쿼리가 발생했을 때 어떠한 값이 들어갔는지 확인해보면 user_id 값(FK값)에 NULL 이 들어간 것을 볼 수 있다.

즉, FK값을 넣기 위한 객체가 NULL이라는 것이다 !!매우 중요!!!!
FK값으로 넣기 위한 객체가 영속성 컨텍스트 내에서 NULL이라는 것이다. 존재하는 외래키 제약조건과는 별개이다.

그러면 cacade 속성을 지정해주고 다시 실행해보자.

실행결과를 확인해보면 변경사항을 자식엔티티에 전파하여 Users 객체를 먼저 insert 쿼리를 실행 하고 영속성 컨텍스트에 저장하게 된다.
그리고 영속성 컨텍스트에 존재하는 객체를 Orders의 user(관계필드)필드에 넣어서 Orders 테이블에 insert 쿼리를 실행한 것을 확인할 수 있다.

따라서 해결방법은 cacade 속성을 지정해주어서 부모 엔티티의 변경이 자식 엔티티에 전파되도록 설정해주어야한다.

profile
Back-End Developer

0개의 댓글

관련 채용 정보