Server/Spring

JPA - Fetch Join에 ON절 추가하는 방법! (쿼리 개선, 인덱싱)

JaeHoney 2022. 7. 8. 22:23

Fetch Join + ON 절

JPA에서 Fetch Join 시 원하는 인덱스를 태우기 위해서 ON절을 추가하고 싶을 때가 있다. 

 

아래의 예를 들어보자.

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {

    @Id @GeneratedValue
    private Long id;
    ... 
    private int countryCode;

    @OneToMany(fetch = FetchType.LAZY)
    @JoinColumn(name = "credit_id")
    private CreditCard creditCard;

    public Member(... 생략, CreditCard creditCard, int contryCode) {
        ... 생략
        if (creditCard != null) {
            changeCreditCard(creditCard);
        }
        this.countryCode = countryCode;
    }
}

여기서 Member Table은 id와 contryCode를 복합 인덱스로 가지고 있다고 가정하자.

 

다음은 CreditCard 엔터티를 보자.

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class CreditCard {

    @Id
    @GeneratedValue
    private Long id;

    private int countryCode;
    
    public Team(... 생략, int countryCode) {
        this.countryCode = countryCode;
    }
}

credit_card 테이블도 id와 countryCode를 복합 인덱스로 가지고 있다.

 

문제는 Member에서 creditCard를 FetchJoin으로 불러오면 id만 가지고 ON절로 묶어서 가져오게 된다. 그래서 추가로 ON절을 명시해서 인덱스를 타게 만들어 주고 싶다!

@JoinColumns

문제를 해결하기 위해 @JsonColums를 사용했다.

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {

    @Id @GeneratedValue
    private Long id;
    ... 
    private int countryCode;

    @OneToMany(fetch = FetchType.LAZY)
    @JoinColumns({
            @JoinColumn(name = "credit_card_id", referencedColumnName = "id"),
            @JoinColumn(name = "country_code", referencedColumnName = "country_code")
    })
    private CreditCard creditCard;

    public Member(... 생략, CreditCard creditCard, int contryCode) {
        ... 생략
        if (creditCard != null) {
            changeCreditCard(creditCard);
        }
        this.countryCode = countryCode;
    }
}

해당 방식은 정상적으로 동작하지 않고 아래의 에러가 발생한다.

nested exception is org.hibernate.MappingException: Repeated column in mapping
for entity: com.domain.test.Member column: country_code
(should be mapped with insert="false" update="false")

예외 로그를 보면 엔터티의 country_code 컬럼이 중복되어서 테이블과 매핑할 수 없다는 내용이다. Member가 필드로 country_code 컬럼을 이미 매핑하고 있는데 이를 조인을 사용하는 필드에서 JoinColumn으로 또 사용했기 때문이다.

 

예외 메세지에서 제안한 방법은 컬럼 매핑 부분에 아래의 옵션을 추가하는 방법이다.

@Column(insertable = false, updateable = false)
private int countryCode;

문제는 위 방식은 해당 엔터티가 DB로 반영(flush)될 때 해당 컬럼은 제외하게 된다. 즉, 해당 엔터티가 읽기 전용(Read only)가 되어버리므로 의도와 다르다.

@JoinColumnsOrFormula

Hibernate의 @JoinColumnOrFormula를 사용하면 해당 문제를 해결할 수 있다.

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member implements Serializable {

    @Id @GeneratedValue
    private Long id;
    ... 
    private int countryCode;

    @OneToMany(fetch = FetchType.LAZY)
    @JoinColumnsOrFormulas(value = {
        @JoinColumnOrFormula(column = @JoinColumn(name = "credit_card_id", referencedColumnName = "id")),
        @JoinColumnOrFormula(formula = @JoinFormula(value = "country_code", referencedColumnName = "country_code"))
    })
    private CreditCard creditCard;

    public Member(... 생략, CreditCard creditCard, int contryCode) {
        ... 생략
        if (creditCard != null) {
            changeCreditCard(creditCard);
        }
        this.countryCode = countryCode;
    }
}

Hibernate의 @Fomula는 @Column 대신에 사용하게 되고, 실제로 매핑받는 값이 아니라 SQL의 파생된 값(derived value)을 읽기 전용 상태로 표현한다.

 

그래서 결과적으로 조인할 때만 내부적으로 SQL의 ON절을 만들어줘서 인덱싱을 위한 컬럼 조건을 추가할 수 있다.

 


Reference