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

26. 명세 (Specifications)

sdafdq 2023. 11. 26. 16:40

이제 SpringDataJpa의 전반적인 부분은 어느정도 배웠고,

 

이제 나머지 기능들에 대해서 소개를 좀 할건데,

 

이것들은 대부분 복잡도에 비해 다른 좋은 대안들이 있어서 사실 잘 안쓴다.

 

명세,

Query By Example,

Projections,

네이티브 쿼리

 

이렇게 4가지에 대해 배울건데,

 

4번째 네이티브 쿼리 빼고는 위에 3가지는 소개 하고, 왜 안쓰는지 알아볼 예정.

 

 

명세라는 것은 뭐냐, 우리가 where문에서 and, or 해서 조건같은 걸 막 넣는데,

그걸 조립해서 쓸 수 있도록 만든 개념 (근데 이건 이미 QueryDSL에서..)

 

그냥 나도 한번 써보는 정도는 보여 줄 거임.

 

public interface MemberRepository extends JpaRepository<Member, Long>, 커스텀할거야 , JpaSpecificationExecutor<Member>{

Spring Data Jpa 인터페이스에다가, 저 

JpaSpecificationExecutor<T>

이거를 상속받아 주면 됨.

 

저거 들어가 보면

public interface JpaSpecificationExecutor<T> {

	Optional<T> findOne(Specification<T> spec);

	List<T> findAll(Specification<T> spec);

	Page<T> findAll(Specification<T> spec, Pageable pageable);

	List<T> findAll(Specification<T> spec, Sort sort);

	long count(Specification<T> spec);

	boolean exists(Specification<T> spec);

	long delete(Specification<T> spec);

	<S extends T, R> R findBy(Specification<T> spec, Function<FluentQuery.FetchableFluentQuery<S>, R> queryFunction);

}

여러 메소드 들이 있는데, 우리가 좀 익숙한 findAll이라던지 이런 R에 대한 것들.

아무래도 뭔가 동적쿼리에 대한 기능 같으므로 find에 대한..

근데 차이점은 Specification<T>라는 객체를 받음.

 

그럼 추측하기로는 저 Specification이라는 것이 동적쿼리를 짤 때 무언가 도움이 되라고 만들어 놓은 객체 같음.

 

이거는 Jpa의 Criteria라는 기술을 쉽게 활용하도록 만든 거라고 함.

 

 

그럼 이제 구현해 보겠음.

이걸 쓰기 위해서는 우리가 따로 그 조합할? Specification<T>에 대한 조합을 우리가 정의해 줘야 함.

public class MemberSpec {
    public static Specification<Member> teamName(final String teamName){
        return new Specification<Member>() {
            @Override
            public Predicate toPredicate(Root<Member> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
                if(!StringUtils.hasText(teamName)) return null;

                Join<Member, Team> t = root.join("team", JoinType.INNER);
                return builder.equal(t.get("name"), teamName);
            }
        };
    }

    public static Specification<Member> username(final String username){
        return (Specification<Member>) (root, query, builder)->{
            return builder.equal(root.get("username"), username);
        };
    }
}

Specification을 return 해야 저걸로 계속 이어서 할 수 있으니까, 리턴타입을 저걸로 하고,

보면 인자로 받은 teamName이 null이면 그냥 null을 return 시켜버리고(빈 Specification을 리턴시키는 게 아니라?),

있으면 Join해서 뭐 저게 Join해서 가져온다 그런 메소드인가 봄. root가 엔티티? 같고, Join해서 Team을 가져오는 거니 t라고 한 듯.

그거를 크리터리아 빌더 builder.equal 해서 t의 이름과 인자로 받은 teamName을 비교하는 equal문의 정보를 가진 객체를 반환. 저게 Specification으로 반환 되는 듯?

 

그 다음 username. 저거는 findByUsername하면 되지 않나? 생각했는데 동적쿼리는 여러개 조건으로 내거는 거니까 저것도 필요가 있었나 봄.

저것도 똑같음. username을 인자로 받아서 root의 username필드와 비교시키는 equal문의 정보를 가진 Specification 객체를 반환

 

그래서,

@Test
public void specBasic(){
    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();

    Specification<Member> spec = MemberSpec.username("m1").and(MemberSpec.teamName("teamA"));
    List<Member> result = memberRepository.findAll(spec);

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

이렇게 테스트 코드 돌려 보면.

 

저 findAll 하는 순간

    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 
    join
        team t1_0 
            on t1_0.team_id=m1_0.team_id 
    where
        m1_0.username=? 
        and t1_0.name=?

이렇게 쿼리 잘 나감.

join했던 Team과,

equal했던 username, team의 name

 

 

위에는 and만 썼는데 and, or, not 등 제공

 

여튼, QueryDSL을 쓰자. Jpa의 Criteria는 쓰지 말자.