JPA 제휴 편의 방법 위치에 대한 나의 생각

1. 문제

JPA에서 연관 편의 메소드는 양방향 연관을 즉시 만들기 위해 작성된 메소드입니다. 보지마.
이때, 엔티티 A(다수)와 B(하나)가 양방향 관계를 갖는다면 연관 편의 메소드는 총 3가지 경우로 작성할 수 있다.

  • (1) A를 등록하고 B를 관리합니다.
  • (2) B에 쓰고 A에게 투여한다.
  • (3) A와 B를 각각 씁니다.

방법 (3)은 여기에서 피해야 할 패턴입니다. 그러나 두 위치에서 메서드 호출을 생략하면 논리 문제가 발생하고 코드 유지 관리가 복잡해집니다.

그렇다면 (1)과 (2)의 경우 연관 편의 메서드를 정의하는 가장 좋은 방법은 어디일까요?

이 기사에서 나는 이 질문에 대한 나의 생각을 포착하려고 노력할 것이다.

2. 간단한 양방향 ManyToOne 관계

아래와 같이 두 개의 엔터티가 있다고 가정해 보겠습니다.

(핀.자바)

@Entity
@Table(name = "pin")
public class Pin extends BaseTimeEntity {

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

    @OneToMany(mappedBy = "pin", cascade = CascadeType.ALL, orphanRemoval = true)
    @Builder.Default
    private List<PinTag> pinTags = new ArrayList<>();

    ...
}

(이미지.자바)

@Entity
@Table(name = "picture")
public class Picture extends BaseTimeEntity {

    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Id
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    private Pin pin;
}

연결 편의 메서드가 핀 엔터티에 있어야 합니까? 그림 엔터티에 있어야 합니까?

사실, 그것들을 어디에 두느냐는 중요하지 않습니다. 그러나 저는 관례로 핀 엔티티를 고수하는 경향이 있습니다.

그 이유는 이는 비즈니스 로직 관점에서 Pin 엔터티는 이미지를 사용하고 Picture 엔터티는 핀을 사용하지 않기 때문입니다.보지마. 실제로 코드를 작성할 때 핀 관련 코드에 이미지를 저장/업데이트/삭제하고, 이미지 쪽이 핀에 거의 닿지 않기 때문입니다.

게다가, 캐스케이드 옵션을 통해 자녀의 라이프 사이클을 관리하는 부모의 측면을 고려할 때 핀의 관점에서 자신의 이미지 목록을 관리하는 것은 의도를 명확하게 할 수 있습니다.

따라서 아래와 같이 Pin 엔터티에 대한 편리한 연결 방법을 작성합니다.

@Entity
@Table(name = "pin")
public class Pin extends BaseTimeEntity {

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

    @OneToMany(mappedBy = "pin", cascade = CascadeType.ALL, orphanRemoval = true)
    @Builder.Default
    private List<PinTag> pinTags = new ArrayList<>();


    ####### 연관관계 편의 메서드 #######
    public void addPicture(Picture picture) {
        if(!getPictures().contains(picture)){
            getPictures().add(picture);
        }
        picture.setPin(this);
    }

    public void removePicture(Picture picture) {
        this.getPictures().remove(picture);
        picture.setPin(null);
    }
}

3. ManyToMany 관계는 어떻습니까?

ManyToMany 관계를 해결하려면 연결 엔터티를 사용하여 두 엔터티를 함께 연결합니다.

아래의 엔터티 관계는 사용자가 여러 서클에 가입할 수 있고 서클에 여러 사용자가 있을 수 있다는 요구 사항에서 발생합니다.

그리고 user-UserCircle과 circle-UserCircle에 각각 양방향 매핑을 적용했습니다.

(사용자.자바)

@Entity
@Table(name = "users")
public class Users extends BaseTimeEntity {

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

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
    @Builder.Default
    private List<UserCircle> userCircleList = new ArrayList<>();
    
    ...
}

(서클. 자바)

@Entity
@Table(name = "circle")
public class Circle extends BaseTimeEntity {

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

    @OneToMany(mappedBy = "circle", cascade = CascadeType.ALL, orphanRemoval = true)
    @Builder.Default
    private List<UserCircle> userCircleList = new ArrayList<>();

    ...
}

(UserCircle.java)

@Entity
@Table(name = "user_circle")
public class UserCircle extends BaseTimeEntity {

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

    @ManyToOne(fetch = FetchType.LAZY)
    private Users user;

    @ManyToOne(fetch = FetchType.LAZY)
    private Circle circle;
    
    ...
}

자, 이런 상황에서 사용자와 서클 엔터티에 대한 편리한 매핑 방법을 작성하는 것이 좋을까요?

내 추측은 아니오입니다. 이 경우 UserCircle 측에 연관 편의 메소드를 작성하는 것이 더 좋다고 생각합니다.하다.

다음 테스트 코드를 통해 그 이유를 살펴보겠습니다. 매핑 방법(addUserCircle)이 사용자 및 원에 있다고 가정합니다.


테스트 코드 – 실패

이 테스트 코드는 실패합니다. 이는 Circle에 작성된 Association Convenience 메소드가 호출되었지만 사용자 측에서 생성된 Association Convenience 메소드가 호출되지 않았기 때문입니다.

이 테스트 코드가 성공하려면 u1.addUserCircle(uc1); 호출된 메서드를 추가로 호출했어야 합니다.

이 상황은 소개에서 논의되었습니다 양측에서 연관편의법을 이용하여 상황을 회피하는 이유(3)와 유사해진다.

따라서 ManyToMany 관계를 해결하기 위해 연결 엔터티의 세 엔터티를 관리하는 간단한 연결 메서드를 만드는 것이 좋습니다.

@Entity
@Table(name = "user_circle")
public class UserCircle extends BaseTimeEntity {

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

    @ManyToOne(fetch = FetchType.LAZY)
    private Users user;

    @ManyToOne(fetch = FetchType.LAZY)
    private Circle circle;
    
    ####### 연관관계 편의 메서드 #######
    public void setUserAndCircle(Users user, Circle circle) {
        addUserCircleForUser(user);
        addUserCircleForCircle(circle);
        setUser(user);
        setCircle(circle);
    }

    private void addUserCircleForUser(Users user) {
        if(!user.getUserCircleList().contains(this)){
            user.getUserCircleList().add(this);
        }
    }
    
    private void setUser(Users user) {
        this.user = user;
    }

    private void addUserCircleForCircle(Circle circle) {
        if(!circle.getUserCircleList().contains(this)){
            circle.getUserCircleList().add(this);
        }
    }
    private void setCircle(Circle circle) {
        this.circle = circle;
    }
    
}

그리고 테스트 코드를 아래와 같이 작성하면, 테스트는 라인 매핑 편의 메서드 호출로 성공합니다.하다. 또한 User 및 Circle 매개변수의 필수 설정은 컴파일 타임에 잘못된 논리를 사용하는 것을 방지합니다.당신은 그것을 만들 것입니다


테스트 코드 – 성공

4. 구성

지금까지 말한 내용을 요약하면 크게 두 가지입니다.

  • ManyToOne 관계의 두 엔터티가 양방향 연결인 경우 부모 측에 편리한 연결 방법을 작성합니다.
  • 중간 연결 엔터티와 양방향 연결을 수행할 때 ManyToMany 관계를 해결하려면 편리한 연결 메서드를 작성합니다.

실용적인 연상법을 쓰는 방식은 사람마다 다른 것 같고, 제가 생각한 방식이 정답은 아니니 참고만 하시면 좋을 것 같습니다.