devoong2
devoong2
devoong2
전체 방문자
오늘
어제

인기 글

최근 글

  • Category (35)
    • Java (4)
    • Spring (13)
    • JPA (4)
    • DesignPattern (1)
    • 동시성 (Concurrency) (4)
    • 회고 (1)
    • Redis (1)
    • Network (3)
    • Kafka (2)
    • Spring Batch (2)

최근 댓글

반응형
hELLO · Designed By 정상우.
devoong2

devoong2

[JPA] @Where 어노테이션 사용법
JPA

[JPA] @Where 어노테이션 사용법

2024. 3. 8. 17:56
반응형

 

예제 및 테스트 코드는 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();
}

테스트 코드의 시나리오는 다음과 같습니다.

  1. ID가 1인 Member 를 soft delete
  2. 식별자인 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

  • https://cheese10yun.github.io/jpa-where/
  • https://docs.jboss.org/hibernate/stable/orm/javadocs/org/hibernate/annotations/Where.html
반응형

'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
    'JPA' 카테고리의 다른 글
    • [JPA] OSIV (Open-Session-In-View) 동작원리 및 주의사항
    • JPA 영속성 컨텍스트(Persistence Context)의 5가지 특징
    • JPA 영속성 컨텍스트(Persist Context)에 대해 알아보자
    devoong2
    devoong2
    github 주소: https://github.com/limwoobin

    티스토리툴바