ORM이 가지고 있던 문제와 해결 방법
상속 문제 : 매핑정보에 상속정보를 넣어준다. (@OneToMany, @ManyToOne)
관계 문제 : 매핑정보에 방향정보를 넣어준다. (@JoinColumn, @MappedBy)
탐색 문제 : 매핑/조회 정보로 참조탐색 시점을 관리한다. (@FetchType, fetchJoin())
밀도 문제 : 크기가 큰 멤버 객체는 테이블을 분리하여 상속으로 처리한다. (@embedded)
식별성 문제 : PK 를 객체 Id로 설정하고 EntityManager는 해당 값으로 객체를 식별하여 관리 한다. (@Id, @GeneratedValue)
ORM이 가지는 장점
1. 1차, 2차 캐시
- 1차 캐시
- 영속성 컨텍스트 내부에는 엔티티를 보관하는 저장소가 있는데 이를 1차 캐시라고 한다.
- 일반적으로 트랜잭션을 시작하고 종료할 때까지만 1차 캐시가 유효하다.
- 1차 캐시는 한 트랜잭션 계속해서 원본 객체를 넘겨준다.
- 2차 캐시
- 애플리케이션 범위의 캐시로, 공유 캐시라고도 하며, 애플리케이션을 종료할 때 까지 캐시가 유지된다.
- 2차 캐시는 캐시 한 객체 원본을 넘겨주지 않고 복사본을 만들어서 넘겨준다.
- 복사본을 주는 이유는 여러 트랜잭션에서 동일한 원본객체를 수정하는일이 없도록 하기 위해서이다.
적용 방법
// Team.java
@Entity
@Cacheable
public class Team {
@Id @GeneratedValue
private Long id;
...
}
# application.yml
spring.jpa.properties.hibernate.cache.use_second_level_cache: true
# 2차 캐시 활성화합니다.
spring.jpa.properties.hibernate.cache.region.factory_class: XXX
# 2차 캐시를 처리할 클래스를 지정합니다.
spring.jpa.properties.hibernate.generate_statistics: true
# 하이버네이트가 여러 통계정보를 출력하게 해주는데 캐시 적용 여부를 확인할 수 있습니다.
캐시 모드 설정
# appplication.yml
spring.jpa.properties.javax.persistence.sharedCache.mode: ENABLE_SELECTIVE
ALL
|
모든 엔티티를 캐시합니다.
|
NONE
|
캐시를 사용하지 않습니다.
|
ENABLE_SELECTIVE
|
Cacheable(true)로 설정된 엔티티만 캐시를 적용합니다.
|
DISABLE_SELECTIVE
|
모든 엔티티를 캐시하는데 Cacheable(false)만 캐시하지 습니다.
|
UNSPECIFIED
|
JPA 구현체가 정의한 설정을 따릅니다
|
2. 영속성 컨텍스트(1차 캐시)를 활용한 쓰기지연
- 영속성 4가지 상태 ( 비영속 > 영속 > 준영속 | 삭제)
- ① 비영속(new/transient) - 엔티티 객체가 만들어져서 아직 저장되지 않은 상태로, 영속성컨텍스트와 전혀 관계가 없는 상태
- ② 영속(managed) - 엔티티가 영속성 컨텍스트에 저장되어, 영속성 컨텍스트가 관리할 수 있는 상태
- ③ 준영속(detached) - 엔티티가 영속성 컨텍스트에 저장되어 있다가 분리된 상태로, 영속성컨텍스트가 더 이상 관리하지 않는 상태
- ④ 삭제(removed) - 엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제하겠다고 표시한 상태
- 객체의 영속성 상태는 Entity Manager 의 메소드를 통해 전환된다.
- Raw JPA 관점에서 순서대로 요약정리 해보자면
- persist(),merge() > (영속성 컨텍스트에 저장된 상태) > flush() > (DB에 쿼리가 전송된 상태) > commit() > (DB에 쿼리가 반영된 상태)
예제)
Item item = new Item(); // 1
item.setItemNm("테스트 상품");
EntityManager em = entityManagerFactory.createEntityManager(); // 2
EntityTransaction transaction = em.getTransaction(); // 3
transaction.begin();
em.persist(item); // 4-1
em.flush(item). // 4-2 (DB에 SQL 보내기/commit시 자동수행되어 생략 가능함)
transaction.commit(); // 5
em.close(); // 6
1️⃣ 영속성 컨텍스트에 담을 상품 엔티티 생성
2️⃣ 엔티티 매니저 팩토리로부터 엔티티 매니저를 생성
3️⃣ 데이터 변경 시 무결성을 위해 트랜잭션 시작
4️⃣ 영속성 컨텍스트에 저장된 상태, 아직 DB에 INSERT SQL 보내기 전
5️⃣ 트랜잭션을 DB에 반영, 이 때 실제로 INSERT SQL 커밋 수행
6️⃣ 엔티티 매니저와 엔티티 매니저 팩토리 자원을 close() 호출로 반환
- 쓰기 지연이 발생하는 시점
- flush() 동작이 발생하기 전까지 최적화한다.
- flush() 동작으로 전송된 쿼리는 더이상 쿼리 최적화는 되지 않고, 이후 commit()으로 반영만 가능하다.
- 쓰기 지연 효과
- 여러개의 객체를 생성할 경우 모아서 한번에 쿼리를 전송한다.
- 영속성 상태의 객체가 생성 및 수정이 여러번 일어나더라도 해당 트랜잭션 종료시 쿼리는 1번만 전송될 수 있다.
- 영속성 상태에서 객체가 생성되었다 삭제되었다면 실제 DB에는 아무 동작이 전송되지 않을 수 있다.
- 즉, 여러가지 동작이 많이 발생하더라도 쿼리는 트랜잭션당 최적화 되어 최소쿼리만 날라가게된다.
테이블 매핑 기능
@Entity
- 객체 관점에서의 이름
- 디폴트로 클래스명으로 설정됨
- 엔티티의 이름은 JQL에서 쓰임
@Table
- RDB 의 테이블 이름
- @Entity의 이름이 테이블의 기본값.
- 테이블의 이름은 SQL에서 쓰임
@Id
- 엔티티의 주키를 맵핑할 때 사용.
- 자바의 모든 primitive 타입과 그 랩퍼 타입을 사용할 수 있음
- Date랑 BigDecimal, BigInteger도 사용 가능.
- 복합키를 만드는 맵핑하는 방법도 있지만 그건 논외로..
@GeneratedValue
- 주키의 생성 방법을 맵핑하는 애노테이션
- 생성 전략과 생성기를 설정할 수 있다.
- 기본 전략은 AUTO: 사용하는 DB에 따라 적절한 전략 선택
- TABLE, SEQUENCE, IDENTITY 중 하나.
@Column
- unique
- nullable
- length
- columnDefinition
- ...
@Temporal
- 현재 JPA 2.1까지는 Date와 Calendar만 지원.
@Transient
- 컬럼으로 맵핑하고 싶지 않은 멤버 변수에 사용.
필드 타입 매핑 기능
@Column
- String, Date, Boolean, 과 같은 타입들에 공통으로 사이즈를 제한할 용도로 쓰인다.
- Class 에 @Entity 가 붙어있으면 자동으로 필드들에 @Column 이 붙음
@Enumerated
- Enum 매핑용도로 쓰이며 실무에서는 @Enumerated(EnumType.*STRING*) 으로 사용권장
- Default 타입인 ORDINAL 은 0,1,2.. 값으로 들어가기 때문에 추후 순서가 바뀔 가능성있다.
Composite Value 타입
@Embeddable
@Embedded
@AtrributeOverrides
@AttributeOverride
- 복합키를 선언하는 방법은 2가지가 있습니다.
- @IdClass를 활용하는 복합키는 복합키를 사용할 엔티티 위에 @IdClass(식별자 클래스) 사용
- @EmbeddedId를 활용하는 복합키는 복합키 위에 @EmbeddedId 사용
@IdClass
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class UserChannelId implements Serializable {
private Long user; // UserChannel 의 user 필드명과 동일해야함
private Long channel; // UserChannel 의 channel 필드명과 동일해야함
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserChannelId userChannelId = (UserChannelId) o;
return Objects.equals(getUser(), userChannelId.getUser()) && Objects.equals(getChannel(), userChannelId.getChannel());
}
@Override
public int hashCode() {
return Objects.hash(getUser(), getChannel());
}
}
@Entity
@IdClass(UserChannelId.class)
public class UserChannel {
....
@Id
@ManyToOne
@JoinColumn(name = "user_id")
User user;
@Id
@ManyToOne
@JoinColumn(name = "channel_id")
Channel channel;
...
}
@EmbeddedId
@Entity
public class UserChannel {
@EmbeddedId
private UserChannelId userChannelId;
...
@ManyToOne
@MapsId("user_id")
User user;
@ManyToOne
@MapsId("channel_id")
Channel channel;
...
}
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Embeddable
public class UserChannelId implements Serializable {
@Column(name = "user_id")
private Long userId;
@Column(name = "channel_id")
private Long channelId;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserChannelId userChannelId = (UserChannelId) o;
return Objects.equals(getUser(), userChannelId.getUser()) && Objects.equals(getChannel(), userChannelId.getChannel());
}
@Override
public int hashCode() {
return Objects.hash(getUser(), getChannel());
}
}
Cascade(영속성 전이)
- 사용 위치
- 연관관계의 주인 반대편 - 부모 엔티티(다대일에서 일)
- 즉, @OneToMany 가 있는 쪽 또는 @OneToOne 도 가능
- 예를들어, 게시글과 첨부파일이라면 일에 해당하는 게시글에 설정한다.
- 사용 조건
- 양쪽 엔티티의 라이프사이클이 동일하거나 비슷해야한다.
- 예를들어, 게시글이 삭제되면 첨부파일도 같이 삭제 되어야 한다.
- 대상 엔티티로의 영속성 전이는 현재 엔티티에서만 전이 되어야 한다. (다른곳에서 또 걸면 안됨)
- 예를들어, 첨부파일을 게시글이 아닌 다른곳에서 영속성 전이를 하면 안된다.
- 양쪽 엔티티의 라이프사이클이 동일하거나 비슷해야한다.
- 옵션 종류
- ALL : 전체 상태 전이
- PERSIST : 저장 상태 전이
- REMOVE : 삭제 상태 전이
- MERGE : 업데이트 상태 전이
- REFERESH : 갱신 상태 전이
- DETACH : 비영속성 상태 전이
orphanRemoval (고아 객체 제거)
- 사용 위치
- @OneToMany 또는 @OneToOne 에서 사용 - 부모 엔티티
- 사용법
- Cascade.REMOVE 와 비슷한 용도로 삭제를 전파하는데 쓰인다.
- 부모 객체에서 리스트 요소삭제를 했을경우 해당 자식 객체는 매핑정보가 없어지므로 대신 삭제해준다.
- 요건 DB 에서는 절대 알 수 없는 행동이다. (부모가 자식의 손을 놓고 버리고 간 고아 객체)
- 옵션
- true
- false
Cascade.REMOVE 와 orphanRemoval 차이점
Cascade.REMOVE의 경우 일에 해당하는 부모 엔티티를 em.remove를 통해 직접 삭제할 때,그 아래에 있는 다에 해당하는 자식 엔티티들이 삭제되는 것입니다.
orphanRemoval=true는 위 케이스도 포함하며,일에 해당하는 부모 엔티티의 리스트에서 요소를 삭제하기만 해도 해당 다에 해당하는 자식 엔티티가 delete되는 기능까지 포함하고 있다고 이해하시면 됩니다.
즉, orphanRemoval=true 는 리스트 요소로써의 영속성 전이도 해준다는 뜻
<영속성 전이 최강 조합 : orphanRemoval=true + Cascade.ALL
위 2개를 함께 설정하면 자식 엔티티의 라이프 사이클이 부모 엔티티와 동일해지며, 직접 자식 엔티티의 생명주기를 관리할 수 있게 되므로 자식 엔티티의 Repository 조차 없어도 된다. (따라서, 매핑 테이블에서 많이 쓰임)
Fetch(조회 시점)
- 사용 위치
- Entity 에 FetchType 으로 설정할 수 있다.
- @ElementCollection, @ManyToMany, @OneToMany, @ManyToOne, @OneToOne
- Query 수행시 fetch Join 을 통해서 LAZY 인 경우도 즉시 불러올 수 있다.
- Entity 에 FetchType 으로 설정할 수 있다.
- 사용법
- 기본 LAZY를 설정한 뒤에 필요할때만 fetch Join 을 수행한다.
- 항상 같이 쓰이는 연관관계 일 경우만 EAGER 를 설정한다.
- 옵션(FetchType)
- EAGER : 즉시 로딩 (부모 조회 시 자식도 같이 조회)
- LAZY : 지연 로딩 (자식은 필요할때 따로 조회)
SpringData 구조
SpringData 기능 목록
- 강력한 리포지토리 및 사용자 지정 객체 매핑 추상화
- 리포지토리 메서드 이름에서 동적 쿼리 파생
- 기본 속성을 제공하는 구현 도메인 기본 클래스
- 명료한 추적기능 지원(생성일시, 마지막 변경일시, 생성자, 마지막 변경자)
- 사용자 지정 리포지토리 코드 통합 가능성
- JavaConfig 및 사용자 지정 XML 네임스페이스를 통한 간편한 Spring 통합
- Spring MVC 컨트롤러와의 고급 통합
- 교차 스토어 지속성에 대한 실험적 지원
SpringData와 JpaRepository 원리
- Repository 는 MarkerInterface 로 특별한 기능은 없음
- Repository ~ JpaRepository 까지는 @NotRepositoryBean 이 붙어있는 인터페이스이다.
- JpaRepository<Entity,ID> 붙이면 알맞은 프로그래밍 된 SimpleJpaReository 구현체 빈이 등록된다.
- 어떻게? @SpringBootApplication 을 통해 자동으로 붙여지는 @EnableJpaRepositories 의 JpaRepositoriesRegistrar 를 통해서 등록된다.
- JpaRepositoriesRegistrar 는 ImportBeanDefinitionRegistrar 의 구현체이다
- ImportBeanDefinitionRegistrar 는 프로그래밍을 통해 빈을 주입해준다.
- 어떻게? @SpringBootApplication 을 통해 자동으로 붙여지는 @EnableJpaRepositories 의 JpaRepositoriesRegistrar 를 통해서 등록된다.
- JpaRepository<Entity,ID> 붙이면 알맞은 프로그래밍 된 SimpleJpaReository 구현체 빈이 등록된다.
기존 Repository vs 새로운 JpaRepository
기존 Repository
@Repository 을 클래스에 붙인다.
앞서배운 RawJPA의 Repository 기능만 가진 구현체가 생성된다. (DB별 예외처리 등)
새로운 JpaRepository JpaRepository<Entity,ID> 인터페이스를 인터페이스에 extends 붙인다. @NotRepositoryBean 된 상위 인터페이스들의 기능을 포함한 구현체가 프로그래밍된다. (@NotRepositoryBean = 빈생성 막음)
SpringDataJpa 에 의해 엔티티의 CRUD, 페이징, 정렬 기능 메소드들을 가진 빈이 등록된다. (상위 인터페이스들의 기능)
실무에서 많이 쓰이는 JpaRepository 팁!
JpaRepository 에서 사용할 메소드 제한하기
1. @RepositoryDefinition 을 인터페이스에 붙이는법 (가장 많이 쓰임)
- 어노테이션을 붙이면 BeanDefinition 에 직접 접근하여 프로그래밍으로 주입받을 구현체 메소드들을 지정해서 요청할 수 있다.
@RepositoryDefinition(domainClass = Comment.class, idClass = Long.class)
public interface CommentRepository {
Comment save(Comment comment);
List<Comment> findAll();
}
2. @NoRepositoryBean 인터페이스로 한번더 감싸는법
- 상위 인터페이스 개념을 하나 더 만들어서 열어줄 메소드만 선언해준다.
@NoRepositoryBean
public interface MyRepository<T, ID extends Serializable> extends Repository<T, ID> {
<E extends T> E save(E entity);
List<T> findAll();
}
JpaRepository에 기능 추가하기
ex) delete()
메소드의 내부 기능 확인하기
- delete 호출시 영속성 상태인지 확인한다.
- 영속성 컨텍스트에 없다면(!em.contains(entity)) 엔티티를 조회해서 영속성 상태로 바꾼다.
- Cascade, orphanRemoval 에 의한 자식도 삭제가 누락되지 않도록!!!
- 영속성 컨텍스트에 없다면(!em.contains(entity)) 엔티티를 조회해서 영속성 상태로 바꾼다.
- JpaRepository 의 delete() 는 해당 엔티티를 바로 삭제하지 않는다.
- remove() 메소드를 통해 remove 상태로 바꾼다.
public interface MyRepository {
...여기다가 추가할 메소드 선언...
}
1. delete 쿼리가 바로 날아가도록 개선
@Repository
@Transactional
public class MyRepositoryImpl implements MyRepository {
@Autowired
EntityManager entityManager;
@Override
public void delete(User user) {
entityManager.remove(user);
}
}
2. findAll 할 때 이름만 가져오도록 개선
@Repository
@Transactional
public class MyRepositoryImpl implements MyRepository {
@Autowired
EntityManager entityManager;
@Override
public List<String> findNameAll() {
return entityManager.createQuery("SELECT u.username FROM User AS u", String.class).getResultList();
}
}
페이징 처리 프로세스
- PageRequest 를 사용하여 Pageable에 페이징 정보를 담아 객체화 한다.
- Pageable을 JpaRepository가 상속된 인터페이스의 메서드에 T(Entity)와 함꼐 파라미터로 전달한다.
- 2번의 메서드의 return 으로 Page<T>가 응답 된다.
- 응답된 Page<T>에 담겨진 Page 정보를 바탕으로 로직을 처리하면 된다.
Pageable 요청/ 응답
요청
org.springframework.data.domain.Pageable
Pageable 만드는법
PageRequest.of(int page, int size) : 0부터 시작하는 페이지 번호와 개수. 정렬이 지정되지 않음
PageRequest.of(int page, int size, Sort sort) : 페이지 번호와 개수, 정렬 관련 정보
PageRequest.of(int page int size, Sort sort, Direction direction, String ... props) : 0부터 시작하는 페이지 번호와 개수, 정렬의 방향과 정렬 기준 필드들
Pageable 메서드
pageable.getTotalPages() : 총 페이지 수
pageable.getTotalElements() : 전체 개수
pageable.getNumber() : 현재 페이지 번호
pageable.getSize() : 페이지 당 데이터 개수
pageable.hasnext() : 다음 페이지 존재 여부
pageable.isFirst() : 시작페이지 여부
pageable.getContent(), PageRequest.get() : 실제 컨텐츠를 가지고 오는 메서드. getContext는 List<Entity> 반환, get()은 Stream<Entity> 반환
응답
org.springframework.data.domain.Page
Page<T> 타입
- 게시판 형태의 페이징에서 사용된다.
- 전체 요소 갯수도 함께 조회한다. (totalElements)
Slice<T> 타입
- 더보기 형태의 페이징에서 사용된다.
- 전체 요소 갯수 대신 offset 필드로 조회할 수 있다.
- 따라서 count 쿼리가 발생되지 않고 limit+1 조회를 한다. (offset 은 성능이 안좋아서 현업에서 안씁니다)
List<T> 타입
- 전체 목록보기 형태의 페이징에서 사용된다.
- 기본 타입으로 count 조회가 발생하지 않는다.
- 가장 많이 쓰임
정렬
컬럼 값으로 정렬하기
- Sort 클래스를 사용한다.
Sort sort1 = Sort.by("name").descending(); // 내림차순
Sort sort2 = Sort.by("password").ascending(); // 오름차순
Sort sortAll = sort1.and(sort2); // 2개이상 다중정렬도 가능하다
Pageable pageable = PageRequest.of(0, 10, sortAll); // pageable 생성시 추가
컬럼이 아닌값으로 정렬하기
- @Query 사용시 Alias(쿼리에서 as 로 지정한 문구) 를 기준으로 정렬할 수 있다.
// 아래와 같이 AS user_password 로 Alias(AS) 를 걸어주면
@Query("SELECT u, u.password AS user_password FROM user u WHERE u.username = ?1")
List<User> findByUsername(String username, Sort sort);
// 이렇게 해당 user_password 를 기준으로 정렬할 수 있다.
List<User> users = findByUsername("user", Sort.by("user_password"));
SQL 함수를 사용해서 정렬하기
- JpaSort 를 사용해서 쿼리 함수를 기준으로 정렬할 수 있다.
// 아래와 같이 일반적인 쿼리에서
@Query("SELECT u FROM user u WHERE u.username = ?1") // 이건 없어도됨
List<User> findByUsername(String username, Sort sort);
// 이렇게 쿼리함수 LENGTH() 조건을 걸어서 password 문자길이 기준으로 정렬할 수 있다.
List<User> users = findByUsername("user", JpaSort.unsafe("LENGTH(password)"));
페이징 & 정렬 실무 팁
1. List<T>가 필요하면 응답을 Page<T>로 받지말고 List<T> 로 받아라!
- 전체 count 쿼리가 추가로 발생하는 Page<T> 보다는 List<T>가 대용량 처리할때 더 안정적이고 빠르다!
//전체셀러 목록 조회
@Override
@Transactional(readOnly = true)
public List<SellerProfileResponseDto> allSellerList(PageDTO pageDTO){
// 이부분은 List로 받아도됨 (userRepository 도 수정)
Page<User> usersByUserRole = userRepository
.findUsersByUserRole(UserRole.SELLER, pageDTO.toPageable());
List<SellerProfileResponseDto> sellerProfileResponseDtos = new ArrayList<>();
for(User user:usersByUserRole){
if(!user.getProfile().getIntroduce().isEmpty()) {
sellerProfileResponseDtos.add(new SellerProfileResponseDto(user.getUserName(), user.getProfile().getIntroduce()));
}
}
return sellerProfileResponseDtos;
}
2. Pageable 과 실제 페이지사이의 -1 문제 해결하기
- JPA 페이지는 0부터인데 화면은 1부터시작하는 문제
- -1 처리를 중복으로 해줘야하는 이슈
- PageDTO 를 만들어서 toPageable() 메소드를 사용해보자
public class PageDTO {
@Positive // 0보다 큰수
private Integer currentPage;
private Integer size;
private String sortBy;
public Pageable toPageable() {
return PageRequest.of(currentPage-1, size, Sort.by(sortBy).descending());
}
}
// UserService 일부
public List<User> findAll(PageDTO pageDTO){
return userRepository.findUsers(pageDTO.toPageable());
}
3. Pageable 을 GET API의 요청필드로 받아오기
- Pageable 을 API 요청필드에서 바로 받아올 수 있다.
@GetMapping("/users")
public Page<User> getAllUsers(Pageable pageable) {
return userRepository.findAll(pageable);
}
- http://localhost:8080/users?page=0
- 0번 페이지 부터 20개(default) 조회한다.
- http://localhost:8080/users?page=0&size=5
- 0번 페이지부터 5개 조회한다.
- http://localhost:8080/users?page=0&size=5&sort=id.desc
- 0번 페이지부터 5개 조회 하는데, id 의 역순으로 조회한다.
SpringData 쿼리
기능
- SprintData Common 의 CRUDRepository + PagingAndSortingRepository 이 쿼리기능을 제공
사용 방법
- 프로그래밍되어 제공되는 쿼리명 규칙
- 리턴타입 {접두어}{도입부}By{프로퍼티 표현식}(조건식)[(And|Or){프로퍼티 표현식}(조건식)](OrderBy{프로퍼티}Asc|Desc) (매개변수...)
접두어 | Find, Get, Query, Count, ... |
도입부 | Distinct, First(N), Top(N) |
프로퍼티 표현식 | Person.Address.ZipCode => find(Person)ByAddress_ZipCode(...) |
조건식 | IgnoreCase, Between, LessThan, GreaterThan, Like, Contains, ... |
정렬 조건 | OrderBy{프로퍼티}Asc |
리턴 타입 | E, Optional<E>, List<E>, Page<E>, Slice<E>, Stream<E> |
매개변수 | Pageable, Sort |
QueryDSL
기능
- QueryDSL의 Predicate 인터페이스로 조건문을 여러개를 구성하여 따로 관리할 수 있다.
- findOne(Predicate), findAll(Predicate) 주로 이 2개 메소드가 사용된다.
- findOne = Optional<T> 리턴
- findAll = List<T> | Page<T> | Iterable<T> | Slice<T> 리턴
- findOne(Predicate), findAll(Predicate) 주로 이 2개 메소드가 사용된다.
- Type Safe 기능
- 조건문 구성시에 사용되는 객체, 필드 조건이 실제 타입과 일치한지 체크해준다.
장점
- 문자가 아닌 코드로 쿼리를 작성함으로써, 컴파일 시점에 문법 오류를 쉽게 확인할 수 있다.
- 자동 완성 등 IDE의 도움을 받을 수 있다.
- 동적인 쿼리 작성이 편리하다.
- 쿼리 작성 시 제약 조건 등을 메서드 추출을 통해 재사용할 수 있다.
원리
- QueryDSL 의존성을 추가하면 SpringData에 의해 QueryDslPredicateExecutor 인터페이스가 추가된다.
- QueryDslPredicateExecutor 는 Repository가 QueryDsl 을 실행할 수 있는 인터페이스를 제공하는 역할을 합니다.
Spring 3.X 버전부터는 의존성만 추가하면 빌드에 자동으로 포함되서 실행된다
// application.yml
dependencies {
....
// 9. QueryDSL 적용을 위한 의존성 (SpringBoot3.0 부터는 jakarta 사용해야함)
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}
QuerydslPredicateExecutor 활용
Join이 필요한 쿼리일 경우 (불가능)
- 전체 채널에서 내가 멘션된 쓰레드 목록 조회 기능 만들기
- Mention 엔티티 생성
- User - Mention - Thread 다대다 연관관계 설정
- User, Mention 조건으로 Thread 목록 조회 쿼리수행
- QuerydslPredicateExecutor 로는 Join 연산이 불가능하여 구현 불가능!
- 멤버 컬렉션까지만 조회 가능하며 이것을 묵시적 조인(1 Depth 자동 조인) 이라고 한다.
- 즉, channel 의 threads 까지만 접근 가능한것이 묵시적 조인
- 반면에, Join 연산이 수행되는건 명시적 조인 이라고 한다. (2 Depth 이상 조인)
- 멤버 컬렉션까지만 조회 가능하며 이것을 묵시적 조인(1 Depth 자동 조인) 이라고 한다.
- QuerydslPredicateExecutor 의 단점!
- oin을 수행하지 못한다.
- 그래서 다음강의때 JPQL 과 JPAQueryFactory 을 배워보자!!!!
Join 이 없는 대신 조건이 많은 쿼리 (가능)
해당 채널에서 메세지가 있는 쓰레드 목록 조회 기능 만들기
-
- Channel 조건으로 메세지 본문이 있는 Thread 목록 조회 쿼리수행
이처럼 Join 없이 조건이 많이 추가될수록 QuerydslPredicateExecutor 를 활용할 수 있다.
-
- 사실 현업에서는 요건 잘 안쓰고 JPAQueryFactory 를 주로 쓴다.