예제 및 테스트 코드는 github 에서 확인 가능합니다.
- JPA @Where 어노테이션 사용법
- @Where 예시, 사용법
- @Where 테스트 코드
- case1. 기본 엔티티 조회
- case2. Lazy Loading 조회
- case3. JPQL 조회
- case4. QueryDSL 조회
- case5. QueryDSL Join 조회
- case6. QueryDSL DTO 조회
- @Where 주의사항
- case1. native query 를 사용하는 경우
- case2. 식별자로 조회시 1차 캐시의 데이터를 조회하는 경우
- @Where Deprecated
JPA @Where 어노테이션 사용법
안녕하세요. 이번에는 JPA 의 @Where 어노테이션의 사용 방법에 대해 알아보겠습니다.
@Where 을 이용하면 JPA, QueryDSL 을 사용할때 일괄적으로 조건을 추가할 수 있습니다.
예를 들면, 어떤 테이블에서 데이터 삭제시 soft delete
로 처리하는 경우가 있다고 가정해보겠습니다.
이때, 해당 테이블을 조회하는 경우 모든 쿼리 마다 삭제되지 않았다는 상태 조건을 추가해주어야 하는 번거로움이 있습니다.
이와 같은 경우에 굉장히 효율적으로 사용할 수 있습니다.
@Where 예시, 사용법
여기 Team 과 Member 엔티티가 OneToMany 양방향 관계로 이루어져 있습니다.
status 는 Enum 타입으로 ACTIVE, DISABLE
로 soft delete
상태를 구분해보겠습니다.
삭제된 데이터의 status 는 DISABLE
입니다.
Status.java
@Getter
@AllArgsConstructor
public enum Status {
ACTIVE,
DISABLE;
}
Team.java
@Getter
@Entity(name = "teams")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Where(clause = "status = 'ACTIVE'")
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String name;
@OneToMany(mappedBy = "team", fetch = FetchType.LAZY)
private List<Member> members = new ArrayList<>();
@Enumerated(value = EnumType.STRING)
private Status status;
@Builder
public Team(String name, List<Member> members, Status status) {
this.name = name;
this.members = members;
this.status = status;
}
}
Member.java
@Getter
@Entity(name = "members")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Where(clause = "status = 'ACTIVE'")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id", nullable = false)
private Team team;
@Enumerated(value = EnumType.STRING)
private Status status;
@Builder
public Member(String name, Team team, Status status) {
this.name = name;
this.team = team;
this.status = status;
}
public void delete() {
this.status = Status.DISABLE;
}
}
@Where(clause = "status = 'ACTIVE'")
@Where 어노테이션은 다음과 같이 클래스에 선언해서 사용해보겠습니다.
그럼 이제 테스트를 통해서 케이스별로 실제 쿼리에 조건이 어떻게 들어가는지 확인해보겠습니다.
@Where 테스트 코드
테스트 데이터는 다음과 같이 Team 하나에 Member 2개로 테스트를 진행해보겠습니다.
case1. 기본 엔티티 조회
다음과 같이 기본적으로 엔티티 조회시 쿼리가 어떻게 발생하는지 확인해보겠습니다.
@Test
void test() {
Team team = teamRepository.findById(1L);
assertThat(team.isPresent()).isTrue();
}
쿼리 메소드에는 아무 조건을 지정하지 않았지만 정상적으로 status = 'ACTIVE'
조건이 들어간것을 확인할 수 있습니다.
case2. Lazy Loading 조회
@Test
void test() {
Team team = teamRepository.findById(1L).get();
List<Member> members = team.getMembers();
assertThat(members.size()).isEqualTo(1);
}
LazyLoading 의 경우에도 team, member 에 모두 status = 'ACTIVE'
조건이 정상적으로 들어갔네요.
case3. JPQL 조회
@Test
void test() {
List<Member> members = memberRepository.findAll();
assertThat(members.size()).isEqualTo(1);
}
JPQL 의 경우에도 조건이 정상적으로 포함된것을 확인할 수 있습니다.
case4. QueryDSL 조회
@Test
void test() {
QTeam team = QTeam.team;
Team result = jpaQueryFactory.selectFrom(team)
.where(team.id.eq(1L))
.fetchOne();
assertThat(result.getId()).isEqualTo(1L);
}
QueryDSL 도 마찬가지로 잘 조회가 되네요.
case5. QueryDSL Join 조회
@Test
void test() {
QTeam team = QTeam.team;
QMember member = QMember.member;
List<Member> result = jpaQueryFactory.selectFrom(member)
.join(team).on(team.eq(member.team))
.where(member.id.eq(1L))
.fetch();
assertThat(result.size()).isEqualTo(1L);
}
Team 과 Member 를 조인한 경우에도 두 엔티티 모두 조건에 status 가 잘 적용된 것을 보실 수 있습니다.
case6. QueryDSL DTO 조회
QueryDSL 에서 DTO 로 조회하는 경우도 살펴보겠습니다.
// MemberDto.class
@Getter
public class MemberDto {
private Long memberId;
private String name;
private Long teamId;
public MemberDto(Long memberId, String name, Long teamId) {
this.memberId = memberId;
this.name = name;
this.teamId = teamId;
}
}
@Test
void test() {
QTeam team = QTeam.team;
QMember member = QMember.member;
List<MemberDto> result = jpaQueryFactory.select(
Projections.constructor(MemberDto.class,
member.id,
member.name,
team.id
))
.from(member)
.join(team).on(team.eq(member.team))
.where(member.id.eq(1L))
.fetch();
assertThat(result.size()).isEqualTo(1L);
}
QueryDSL DTO 로 조회하는 경우에도 잘 동작하네요.
@Where 주의사항
@Where
어노테이션을 사용하며 동작하지 않는 경우도 한번 살펴보겠습니다.
case1. native query 를 사용하는 경우
MemberRepository.java
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query(value = "select * from members where team_id = :teamId", nativeQuery = true)
List<Member> findByTeamId(Long teamId);
}
@Test
void test() {
List<Member> members = memberRepository.findByTeamId(1L);
System.out.println(members.size());
}
nativeQuery 의 경우 Entity 의 속성을 사용하지 못하기 때문에 위에서 선언한 @Where 어노테이션도 동작하지 않습니다.
case2. 식별자로 조회시 1차 캐시의 데이터를 조회하는 경우
1차 캐시는 JPA 특징 중 하나로 데이터 조회 시 1차 캐시에 데이터가 존재한다면 DB 를 거치지 않고 바로 메모리에서 엔티티를 가져옵니다.
@Test
void test() {
Optional<Member> optionalMember = memberRepository.findById(1L);
if (optionalMember.isPresent()) {
Member member = optionalMember.get();
member.delete();
memberRepository.save(member);
}
Optional<Member> persistMember = memberRepository.findById(1L);
assertThat(persistMember.isEmpty()).isTrue();
}
테스트 코드의 시나리오는 다음과 같습니다.
- ID가 1인 Member 를 soft delete
- 식별자인 ID 1 로 다시 조회
이 경우에는 findById
에서 Member 를 조회하더라도 Member 는 이미 삭제되어서 "status=DISABLE"
이라 조회가 되지 않을것으로 생각할 수 있습니다.
하지만 실제 동작은 그렇지 않습니다.
데이터를 1차 캐시에서 가져왔기때문에 SELECT QUERY 도 발생하지 않으면서 테스트가 실패했습니다.
soft delete 이후 save 시점에는 변경된 UPDATE QUERY 가 아직 쓰기지연 저장소에 존재하기에
식별자로 조회하게 되면 쿼리 수행 없이 변경된 엔티티를 1차 캐시에서 그대로 가져오게 됩니다.
@Where Deprecated
@Where
어노테이션은 hibernate 6.3 버전부터 Deprecated 되었습니다.
해당 문서에서는 이후 버전부터는 SQLRestriction
를 사용하라고 안내하고 있으니
해당 기능을 사용하게 된다면 참고하시면 좋을것 같습니다.
감사합니다.
reference
'JPA' 카테고리의 다른 글
[JPA] OSIV (Open-Session-In-View) 동작원리 및 주의사항 (1) | 2023.12.11 |
---|---|
JPA 영속성 컨텍스트(Persistence Context)의 5가지 특징 (0) | 2022.08.20 |
JPA 영속성 컨텍스트(Persist Context)에 대해 알아보자 (0) | 2022.08.20 |