스프링데이터 + JPA/스프링 데이터 JPA

27. Query By Example

sdafdq 2023. 11. 26. 17:12

이거는 보기에 생각보다 좋아보인다.

@Test
public void queryByExample(){
    Team teamA = new Team("teamA");
    em.persist(teamA);

    Member m1 = new Member("m1", 0, teamA);
    Member m2 = new Member("m2", 0, teamA);
    em.persist(m1);
    em.persist(m2);

    em.flush();
    em.clear();

    Member member = new Member("m1");

    Example<Member> example = Example.of(member);

    List<Member> result = memberRepository.findAll(example);

    assertThat(result.size()).isEqualTo(1);
    assertThat(result.get(0).getUsername()).isEqualTo("m1");
}

이렇게. 말 그대로 예시. 도메인 자체로 예시를 만들어서,

저 findAll에 집어넣는다.

Example이 인자로 들어갈 수 있는 이유는

@NoRepositoryBean
public interface JpaRepository<T, ID> extends ListCrudRepository<T, ID>, ListPagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {

저기 JpaRepository가 상속받은 것 중에

QueryByExampleExecutor<T> 라는 게 있다.

public interface QueryByExampleExecutor<T> {

	<S extends T> Optional<S> findOne(Example<S> example);

	<S extends T> Iterable<S> findAll(Example<S> example);
    .......

얘네가 다 이런 식으로 정의 해놓음.

 

근데 저렇게 해서 쿼리 날렸더니

select
    m1_0.member_id,
    m1_0.age,
    m1_0.create_by,
    m1_0.created_date,
    m1_0.last_modified_by,
    m1_0.last_modified_date,
    m1_0.team_id,
    m1_0.username 
from
    member m1_0 
where
    m1_0.age=? 
    and m1_0.username=?

이렇게 age까지 같이 날라감.

age가 primary 타입이라 무시할 수 없었나 봄. 객체면 그냥 null이라고 무시했을 텐데

0인지 하고 알아서 날라감. 0으로 알아서 넣어졌나봄.

 

그래서 저런 특정 조건들 제외하고 싶으면

 

 

@Test
public void queryByExample(){
    Team teamA = new Team("teamA");
    em.persist(teamA);

    Member m1 = new Member("m1", 0, teamA);
    Member m2 = new Member("m2", 0, teamA);
    em.persist(m1);
    em.persist(m2);

    em.flush();
    em.clear();

    Member member = new Member("m1");

    ExampleMatcher matcher = ExampleMatcher.matching()
            .withIgnorePaths("age");

    Example<Member> example = Example.of(member, matcher);

    List<Member> result = memberRepository.findAll(example);

    assertThat(result.size()).isEqualTo(1);
    assertThat(result.get(0).getUsername()).isEqualTo("m1");
}

이렇게 ExampleMatcher를 만들어, Example 만들때 같이 넣어줌.

그러면

    select
        m1_0.member_id,
        m1_0.age,
        m1_0.create_by,
        m1_0.created_date,
        m1_0.last_modified_by,
        m1_0.last_modified_date,
        m1_0.team_id,
        m1_0.username 
    from
        member m1_0 
    where
        m1_0.username=?

이렇게 제외되서 나감.

matcher는 말 그대로 매칭시켜주는거임.

지금은 withIgnorePaths해서 매치에서 빼는 쿼리를 메소드를 통해 추가시켜 준 거고,

 

보통 

도메인, ExampleMatcher 이걸로 Example을 만드는 데,

ExampleMatcher가 특정 필드와 일치시키는 상세한 정보를 제공함.

저 matcher에 뭐 이것저것 추가시켜 주면 됨. if(~~~) matcher. 이런 식으로.. 

 

 

 

그리고, join도 되긴함. 근데 하자가 좀 있음

@Test
public void queryByExample(){
    Team teamA = new Team("teamA");
    em.persist(teamA);

    Member m1 = new Member("m1", 0, teamA);
    Member m2 = new Member("m2", 0, teamA);
    em.persist(m1);
    em.persist(m2);

    em.flush();
    em.clear();

    Member member = new Member("m1");
    Team team = new Team("teamA");

    member.changeTeam(team);

    ExampleMatcher matcher = ExampleMatcher.matching()
            .withIgnorePaths("age");

    Example<Member> example = Example.of(member, matcher);

    List<Member> result = memberRepository.findAll(example);

    assertThat(result.size()).isEqualTo(1);
    assertThat(result.get(0).getUsername()).isEqualTo("m1");
}

보면 그냥 아예 예시로 만들어버릴 도메인에 연관관계 자체를 넣어주면 됨.

 

솔직히 되게 좋아보임.

물론 matcher로 제외할 거 짜는 건 좀 귀찮아 보이긴 함. (원시타입은 무시가 안되니.)

근데, 일단 JOIN이 inner조인만 됨. left 조인은 안됨.

 

그리고 지원하는 매칭 조건이 한계가 있음.

문자는 시작문자열, 끝 문자열, 포함, regex?  뭐 정규표현식 이러는 데, 여튼 문자는 내가 보기에 꽤 괜찮게 지원 하는데,

다른 속성은 = 해서 정확한 매칭만 지원함.

 

그리고 중첩 제약 조건이 안됨.

뭐 DB에 보면 막 괄호 써서 하는 그런거 

firstname = ?0 or (firstname = ?1 and lastname = ?2)

이런 거

 

 

일단은 그렇게 오래된 기술은 아니니.. 발전의 여지는 있으나, 지금은 QueryDSL을 쓸 듯