Home Hibernate @Where
Post
Cancel

Hibernate @Where

Hibernate @Where

@Where 란?

떄때로 커스텀한 SQL을 이용해 엔티티 혹은 컬렉션을 필터링하고 싶은 경우가 있습니다. 이 경우 Hibernate 에서 제공하는 어노테이션중 @Where 이라는 어노테이션을 사용해 쉽게 필터링 할 수 있습니다.

1
2
3
4
5
@Target({TYPE, METHOD, FIELD})
@Retention(RUNTIME)
public @interface Where {
	String clause();
}

어노테이션 자체도 간단합니다. where 절을 clause 멤버변수에 작성하라고 안내하고 있습니다.

Entity에 적용하기

간단한 예제 엔티티를 작성해 보겠습니다. 부모 엔티티 (MainOrder)와 자식 엔티티(SubOrder)가 존재하며, 양방향으로 매핑을 해주도록 하겠습니다.

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
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString(exclude = {"activeSubOrderList", "deActivatedSubOrderList", "fromNameLikeCList"})
@Table(name = "main_order")
public class MainOrder {

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

    private LocalDate orderDate = LocalDate.now();

    @OneToMany(mappedBy = "mainOrder", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    @Where(clause = "active = true")
    private List<SubOrder> activeSubOrderList;

    @OneToMany(mappedBy = "mainOrder", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    @Where(clause = "active = false")
    private List<SubOrder> deActivatedSubOrderList;

    @OneToMany(mappedBy = "mainOrder", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    @Where(clause = "LOWER(from_name) LIKE 'c%'")
    private List<SubOrder> fromNameLikeCList;

}
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
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "sub_order")
public class SubOrder {

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

    private String fromName;
    private String fromPhoneNumber;
    private String fromAddress;

    private String toName;
    private String toPhoneNumber;
    private String toAddress;

    @ManyToOne(optional = false, fetch = FetchType.LAZY)
    private MainOrder mainOrder;

    private boolean active;

}

@Where 어노테이션을 통해 각각의 조건에 부합하는 자식 엔티티 리스트를 가져올수 있도록 하였습니다.

@Where 어노테이션의 sql 문의 필드명은 데이터베이스의 필드명이어야 합니다.
예를들어 위 MainOrder 엔티티의 fromNameLikeCList 의 경우 “LOWER(fromName) LIKE ‘c%’” 라고 작성하면 에러가 발생합니다.

조회 결과 확인하기

엔티티 객체를 작성하였으니 이제 더미데이터를 넣고 테스트를 수행해보겠습니다. 더미데이터는 java-faker 라이브러리를 사용하였습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void initData() {
    var mainOrderList = new ArrayList<MainOrder>();

    for (int i = 0; i < 5; i ++) {
        var mainOrder = new MainOrder();
        mainOrderList.add(mainOrderRepository.save(mainOrder));
    }

    for (int i = 0; i < 100; i ++) {
        var subOrder = new SubOrder();
        subOrder.setActive(i % 3 == 0);

        subOrder.setFromName(JavaFakerGenerator.getName());
        subOrder.setFromPhoneNumber(JavaFakerGenerator.getPhoneNumber());
        subOrder.setFromAddress(JavaFakerGenerator.getAddress());

        subOrder.setToName(JavaFakerGenerator.getName());
        subOrder.setToPhoneNumber(JavaFakerGenerator.getPhoneNumber());
        subOrder.setToAddress(JavaFakerGenerator.getAddress());

        subOrder.setMainOrder(mainOrderList.get(i % 5));
        subOrderRepository.save(subOrder);
    }
}

활성:비활성의 비율을 약 1:2 정도의 비율로 세팅하여 테스트 데이터를 생성하였습니다.

1
2
3
4
5
6
7
8
9
10
11
@Test
@Transactional
void whereAnnotationTest1() {
    var mainOrderList = mainOrderRepository.findAll();

    mainOrderList.forEach(mainOrder -> {
        mainOrder.getActiveSubOrderList();
        mainOrder.getDeActivatedSubOrderList();
        mainOrder.getFromNameLikeCList();
    });
}

테스트 코드입니다. 테스트를 수행한다기 보다 쿼리를 확인할 목적으로 작성하였습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
select
    activesubo0_.main_order_id as main_ord9_2_0_,
    activesubo0_.id as id1_2_0_,
    activesubo0_.id as id1_2_1_,
    activesubo0_.active as active2_2_1_,
    activesubo0_.from_address as from_add3_2_1_,
    activesubo0_.from_name as from_nam4_2_1_,
    activesubo0_.from_phone_number as from_pho5_2_1_,
    activesubo0_.main_order_id as main_ord9_2_1_,
    activesubo0_.to_address as to_addre6_2_1_,
    activesubo0_.to_name as to_name7_2_1_,
    activesubo0_.to_phone_number as to_phone8_2_1_ 
from
    sub_order activesubo0_ 
where
    (
        activesubo0_.active = true
    ) 
    and activesubo0_.main_order_id=1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
select
    deactivate0_.main_order_id as main_ord9_2_0_,
    deactivate0_.id as id1_2_0_,
    deactivate0_.id as id1_2_1_,
    deactivate0_.active as active2_2_1_,
    deactivate0_.from_address as from_add3_2_1_,
    deactivate0_.from_name as from_nam4_2_1_,
    deactivate0_.from_phone_number as from_pho5_2_1_,
    deactivate0_.main_order_id as main_ord9_2_1_,
    deactivate0_.to_address as to_addre6_2_1_,
    deactivate0_.to_name as to_name7_2_1_,
    deactivate0_.to_phone_number as to_phone8_2_1_ 
from
    sub_order deactivate0_ 
where
    (
        deactivate0_.active = false
    ) 
    and deactivate0_.main_order_id=1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
select
    fromnameli0_.main_order_id as main_ord9_2_0_,
    fromnameli0_.id as id1_2_0_,
    fromnameli0_.id as id1_2_1_,
    fromnameli0_.active as active2_2_1_,
    fromnameli0_.from_address as from_add3_2_1_,
    fromnameli0_.from_name as from_nam4_2_1_,
    fromnameli0_.from_phone_number as from_pho5_2_1_,
    fromnameli0_.main_order_id as main_ord9_2_1_,
    fromnameli0_.to_address as to_addre6_2_1_,
    fromnameli0_.to_name as to_name7_2_1_,
    fromnameli0_.to_phone_number as to_phone8_2_1_ 
from
    sub_order fromnameli0_ 
where
    (
        LOWER(fromnameli0_.from_name) LIKE 'c%'
    ) 
    and fromnameli0_.main_order_id=1

세 쿼리를 보니 모두 @Where 어노테이션에 작성한 sql문 대로 잘 수행된것을 확인할 수 있습니다.

QueryDSL 에서 사용하기

QueryDSL 에서도 쉽게 사용할 수 있습니다. 예를들어 fromName이 c로 시작하는 SubOrder가 존재하는 경우에만 MainOrder를 가져와야 한다고 가정해보겠습니다.

평소라면 where절에 직접 서브쿼리를 작성해야 하겠지만, @Where 어노테이션을 사용하면 쉽게 표현할 수 있습니다. 다음은 예제 / 테스트 코드입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
@Transactional
void whereAnnotationTest2() {

    var mq = QMainOrder.mainOrder;
    var sq = QSubOrder.subOrder;

    var subQueryFetch = jpaQueryFactory
            .select(mq)
            .from(mq)
            .where(JPAExpressions.select(sq.mainOrder.count()).from(sq).where(mq.eq(sq.mainOrder).and(sq.fromName.startsWithIgnoreCase("c"))).gt(0L))
            .fetch();

    var whereAnnotationFetch = jpaQueryFactory
            .select(mq)
            .from(mq)
            .where(mq.fromNameLikeCList.size().gt(0L))
            .fetch();

    Assert.isTrue(subQueryFetch.size() == whereAnnotationFetch.size(), "test failed");
}

첫번째 subQueryFetch의 경우 where절에 직접 서브쿼리를 작성하였고, 두번째 whereAnnotationFetch의 경우 단순히 @OneToMany로 매핑된 엔티티의 사이즈를 불러와 비교하는 방식을 사용하였습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
select
    mainorder0_.id as id1_0_,
    mainorder0_.order_date as order_da2_0_ 
from
    main_order mainorder0_ 
where
    (
        select
            count(suborder1_.main_order_id) 
        from
            sub_order suborder1_ cross 
        join
            main_order mainorder2_ 
        where
            suborder1_.main_order_id=mainorder2_.id 
            and mainorder0_.id=suborder1_.main_order_id 
            and (
                lower(suborder1_.from_name) like 'c%' escape '!'
            )
    )>0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
select
    mainorder0_.id as id1_0_,
    mainorder0_.order_date as order_da2_0_ 
from
    main_order mainorder0_ 
where
    (
        select
            count(fromnameli1_.main_order_id) 
        from
            sub_order fromnameli1_ 
        where
            mainorder0_.id = fromnameli1_.main_order_id 
            and (
                LOWER(fromnameli1_.from_name) LIKE 'c%'
            ) 
    )>0

테스트도 통과하고 결과도 동일한것을 확인할 수 있습니다. 이처럼 QueryDSL에도 쉽게 사용가능한 것을 확인할 수 있습니다.

This post is licensed under CC BY 4.0 by the author.