강의를 들으며 생각 정리 + "자바 ORM 표준 JPA 프로그래밍" 책 참고
지금부터 실무에서 자주 쓰이지는 않지만 알고 있으면 가끔 유용할 수 있는 기능들에 대해 알아볼 것이다.
왜 실무에서 자주 안쓰이는지에 대해서도 알아보면서 편한 마음으로 공부해보자.
Specifications (명세)
명세는 다음과 같이 JPA 조회에서 Specification이라는 조건을 추가할 수 있는 것이다.
스프링 데이터 JPA는 JPA Criteria를 활용해서 명세를 사용할 수 있도록 지원한다.
<JpaSpecificationExecutor>
public interface JpaSpecificationExecutor<T> {
Optional<T> findOne(@Nullable Specification<T> spec);
List<T> findAll(@Nullable Specification<T> spec);
...
}
먼저 스프링 데이터 JPA 리포지토리에서 JpaSpecificationExecutor을 상속한다.
public interface MemberRepository extends JpaRepository<Member, Long>,
JpaSpecificationExecutor<Member> {
}
명세 정의 코드
</repository/MemberSpec>
public class MemberSpec {
public static Specification<Member> teamName(final String teamName) {
return (Specification<Member>) (root, query, builder) -> {
if (StringUtils.isEmpty(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) -> builder.equal(root.get("username"), username);
}
}
JPA Criteria의 Root, CriteriaQuery, CriteriaBuilder 클래스를 파라미터로 받아 조건으로 사용할 메서드 구현
테스트
@Test
public void specBasic() {
//given
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();
//when
Specification<Member> spec = MemberSpec.username("m1").and(MemberSpec.teamName("teamA"));
List<Member> result = memberRepository.findAll(spec);
//then
Assertions.assertThat(result.size()).isEqualTo(1);
}
명세들은 where(), and(), or(), not() 으로 조립할 수 있다.
findAll을 보면 회원 이름 명세(username)와 팀 이름 명세(teamName)을 and로 조합해서 검색 조건으로 사용한다.
<한계>
JPA Criteria는 조금만 복잡해져도 거의 읽을 수가 없다. 결국 실무에서는 거의 쓰지 않는 방식이다. 복잡한 쿼리에 대해서는 QueryDSL을 사용하자.
Query By Example
스프링 데이터 JPA 메서드들을 포함하는 JpaRepository는 QueryByExampleExecutor 인터페이스를 상속한다.
<QueryByExampleExecutor>
public interface QueryByExampleExecutor<T> {
<S extends T> Optional<S> findOne(Example<S> example);
<S extends T> Iterable<S> findAll(Example<S> example);
...
}
QueryByExampleExecutor는 이처럼 조회 메서드에 Example 인터페이스를 인자로 받을 수 있게 지원한다.
Example은 명세와 비슷하게 조건을 부여하는 기능을 하는데, 엔티티 자체를 조건으로 한다.. 테스트를 통해 알아보자.
테스트
@Test
public void queryByExample() {
//given
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();
//when
//Probe
Member member = new Member("m1");
Team team = new Team("teamA");
member.setTeam(team);
ExampleMatcher matcher = ExampleMatcher.matching()
.withIgnorePaths("age");
Example<Member> example = Example.of(member, matcher);
List<Member> result = memberRepository.findAll(example);
//then
Assertions.assertThat(result.get(0).getUsername()).isEqualTo("m1");
}
<Probe>
필드에 데이터가 있는 실제 도메인 객체
Query By Example은 이렇게 필드 데이터가 있는 실제 도메인을 명세 조건으로 사용한다.
예제에서는 username이 "m1"이고, teamName이 "teamA"인 Member 엔티티를 조건으로 사용할 것이다.
<ExampleMatcher>
특정 필드를 일치시키는 상세한 정보 제공, 재사용 가능
Example 안에서도 ExampleMatcher를 통해 조건을 부여할 수 있다.
예제에서는 age 필드를 무시하는 조건을 추가했다.
<Example>
Probe와 ExampleMatcher로 구성, 쿼리를 생성하는데 사용
<실제 생성 쿼리>
select
member0_.member_id as member_i1_1_,
member0_.age as age6_1_,
member0_.team_id as team_id8_1_,
member0_.username as username7_1_
from
member member0_
inner join
team team1_
on member0_.team_id=team1_.team_id
where
member0_.username=?
and team1_.name=?
age는 무시하면서 Probe의 데이터들에 대해 '=' 매칭을 지원한다.
inner join을 통해 연관관계 정보 역시 조회 가능하다.
장점
동적 쿼리를 편리하게 처리
도메인 객체를 그대로 사용
데이터 저장소를 RDB에서 NOSQL로 변경해도 코드 변경이 없도록 추상화 되어 있음
-> QueryByExampleExecutor은 JPA가 아닌 Spring Data로 패키징 되어 있다.
단점
조인이 가능하지만 내부 조인만 가능하다. 외부 조인은 안 된다.
매칭 조건이 매우 단순하다.
-> 거의 '=' 매칭만 지원한다고 보면 된다.
정리
실무에서 사용하기에는 매칭 조건이 너무 단순하고, 외부 조인이 안 된다.
실무에서는 QueryDSL을 사용하자.
Projections
Projection은 엔티티 대신에 DTO를 편리하게 조회할 때 사용한다.
전체 엔티티가 아니라 만약 회원 이름만 딱 조회하고 싶다면?
<인터페이스 기반 Closed Projections>
프로퍼티 형식(getter)의 인터페이스를 제공하면, 구현체는 스프링 데이터 JPA가 제공한다.
</repository/UsernameOnly>
public interface UsernameOnly {
String getUsername();
}
조회할 엔티티의 필드를 getter 형식으로 지정하면 해당 필드만 선택해서 조회한다.
</repository/MemberRepository>
public interface MemberRepository ... {
List<UsernameOnly> findProjectionsByUsername(String username);
}
반환 타입으로 인지한다.
테스트
@Test
public void projections() {
//given
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();
//when
List<UsernameOnly> result = memberRepository.findProjectionsByUsername("m1");
//then
for (UsernameOnly usernameOnly : result) {
System.out.println("usernameOnly = " + usernameOnly);
}
}
<발생 쿼리>
select m.username from member m
where m.username=‘m1’;
select 절에서 username만 조회한다.
<인터페이스 기반 Open Projections>
다음과 같이 스프링의 SpEL 문법도 지원한다.
</repository/UsernameOnly - 수정>
public interface UsernameOnly {
@Value("#{target.username + ' ' + target.age}")
String getUsername();
}
단, SpEL 문법을 사용하면, DB에서 엔티티 필드를 모두 조회해온 다음에 계산한다.
<클래스 기반 Projection>
구체적인 DTO 형식도 가능하다. 생성자의 파라미터 이름으로 매칭한다.
</repository/UsernameOnlyDto>
public class UsernameOnlyDto {
private final String username;
public UsernameOnlyDto(String username) {
this.username = username;
}
public String getUsername() {
return username;
}
}
<동적 Projections>
다음과 같이 Generic type을 주면, 동적으로 프로젝션 데이터를 변경 가능하다.
<T> List<T> findProjectionsByUsername(String username, Class<T> type);
<중첩 구조 처리>
</repository/NestedClosedProjections>
public interface NestedClosedProjections {
String getUsername();
TeamInfo getTeam();
interface TeamInfo {
String getName();
}
}
<발생 쿼리>
select
m.username as col_0_0_,
t.teamid as col_1_0_,
t.teamid as teamid1_2_,
t.name as name2_2_
from
member m
left outer join
team t
on m.teamid=t.teamid
where
m.username=?
<주의>
프로젝션 대상이 root 엔티티면, select절 최적화 가능
프로젝션 대상이 root가 아니면 left join 처리 -> 모든 필드를 select해서 엔티티로 조회한 다음에 계산
<정리>
프로젝션 대상이 root 엔티티면 유용하다.
프로젝션 대상이 root 엔티티를 넘어가면 select 최적화가 안된다.
실무의 복잡한 쿼리를 해결하기에 한계가 있다 -> 엔티티가 하나를 넘어가는 순간 복잡해진다.
실무에서는 단순할 때만 사용하고, 조금만 복잡해지면 QueryDSL을 사용하자.
네이티브 쿼리
직접 SQL문을 작성하는 방식이다.
</repository/MemberRepository>
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query(value = "select * from member where username = ?", nativeQuery = true)
Member findByNativeQuery(String username);
}
nativeQuery 옵션을 키면 네이티브 쿼리를 사용할 수 있다.
그러나 네이티브 쿼리를 엔티티가 아닌 DTO로 변환 하는 경우 굉장히 복잡해지기 때문에 권장하지 않는 방식이다.
최근에는 Projections을 활용한 네이티브 쿼리 방식을 지원한다. (정적 쿼리)
@Query(value = "select m.member_id as id, m.username, t.name as teamName " +
"from member m left join team t",
countQuery = "select count(*) from member",
nativeQuery = true)
Page<MemberProjection> findByNativeProjection(Pageable pageable);
Page 타입을 사용할 수 있다. 이 때, countQuery는 항상 명시해줘야 한다.
MemberProjection DTO를 사용한다.
</repository/MemberProjection>
public interface MemberProjection {
Long getId();
String getUsername();
String getTeamName();
}
사실 네이티브 쿼리까지 가기 전에 JPQL로 해결되는 경우가 대부분이다. 가급적 네이티브 쿼리는 사용하지 않는 것이 좋다. 정말 어쩔 수 없는 경우에는 JdbcTemplate, myBatis 같은 외부 라이브러리를 사용해서 사용자 정의 리포지토리로 구현하는 것이 효율적이다. 특히 동적 쿼리 같은 경우는 가급적 외부 라이브러리를 사용하자.
'java > jpa' 카테고리의 다른 글
[JPA] DDL-AUTO 테이블 자동 생성 전략 - SQL Column 매칭 (0) | 2021.06.10 |
---|---|
[JPA] CommandAcceptanceException: Error executing DDL (0) | 2021.06.10 |
[Spring Data JPA] 스프링 데이터 JPA 분석 (0) | 2021.05.30 |
[Spring Data JPA] 확장 기능 (0) | 2021.05.30 |
[Spring Data JPA] 쿼리 메소드 기능 (0) | 2021.05.24 |