My Space

반응형

1. 공통 인터페이스 설정

//DataJpaApplication.java
@SpringBootApplication
//@EnableJpaRepositories(basePackages = "jpabook.jpashop.repository")
public class DataJpaApplication {
	public static void main(String[] args) {
		SpringApplication.run(DataJpaApplication.class, args);
	}
}

위와 같이 @EnableJpaRepositories(basePackages = "jpabook.jpashop.repository")
속성을 넣어 줘야 했지만, Spring boot에서는 별도로 repository 위치를 지정해주지 않아도 된다.

 

인터페이스에 JpaRepository를 상속하게 되면, spring data-jpa가 구현 클래스를 알아서 만들어 인젝션을 해준다.

 

예제 코드

//MemberRepository.java
//Repository를 직접 구현할때는 @Repository의 어노테이션이 필요했지만,
//JpaRepository인터페이스를 상속 받아 사용할 때는 생략이 가능하다.
//이유는, 컴포넌트 스캔을 스프링 데이터JPA가 자동으로 처리하기 때문.
//  또한, JPA 예외를 스프링 예외로 변환하는 과정도 자동으로 처리.
import org.springframework.data.jpa.repository.JpaRepository;
import study.datajpa.entity.Member;

public interface MemberRepository extends JpaRepository<Member, Long> {
}

//MemberRepositoryTest.java  (테스트 소스)
class MemberRepositoryTest {

    @Autowired
    MemberRepository memberRepository;

    @Test
    public void testMember() {
    	System.out.println("memberRepository.getClass() = " + memberRepository.getClass());
    }
}

테스트 소스의 출력 값인 memberRepository.getClass()는 아래와 같이 프록시가 찍히게 된다.

 

2. 공통 인터페이스 분석

- JpaRepository 인터페이스 : 공통 CRUD 제공

- 제네릭은 <엔티티 타입, 식별자 타입> 설정

공통 인터페이스 구성

주의

- T findOne(ID) -> Optional<T> findById(ID) 변경

제네릭 타입

- T: 엔티티

- ID: 엔티티의 식별자 타입

- S: 엔티티와 그 자식 타입

주요 메서드

- save(S): 새로운 엔티티는 저장하고 이미 있는 엔티티는 병합한다.

- delete(T): 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove() 호출

- findById(ID): 엔티티 하나를 조회한다. 내부에서 EntityManager.find() 호출

- getOne(ID): 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference() 호출

- findAll(...): 모든 엔티티를 조회한다. 정렬(sort)이나 페이징(Pageable) 조건을 파라미터로 제공할 수 있다.

 

※참고 : JpaRepository 는 대부분 공통 메서드를 제공한다.

 

3. 쿼리 메소드 기능

쿼리 메소드 기능 3가지

  • 메소드 이름으로 쿼리 생성
  • 메소드 이름으로 JPA NamedQuery 호출
  • @Query 어노테이션을 사용해서 리파지토리 인터페이스에 쿼리 직접 정의

 

메소드 이름으로 쿼리 생성

메소드 이름을 분석해서 JPQL 쿼리 실행

ex) 이름과 나이를 기준으로 회원을 조회하기

 

//순수 JPA 코드
@Repository
public class MemberJpaRepository {
    @PersistenceContext
    EntityManager em;
    
    public List<Member> findByUsernameAndAgeGreaterThan(String username, int age) {
            return em.createQuery("select m from Member m where m.username = :username " +
                    "and m.age > :age")
                    .setParameter("username", username)
                    .setParameter("age", age)
                    .getResultList();
    }
}

//스프링 데이터 JPA 코드
public interface MemberRepository extends JpaRepository<Member, Long> {
	//메소드 이름을 규칙에 맞게 정의하면 자동으로 쿼리 생성
    List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}
//테스트 코드
@Test
public void findByUsernameAndAgeGreaterThan() {
        Member m1 = new Member("AAA", 10);
        Member m2 = new Member("AAA", 20);
        memberRepository.save(m1);
        memberRepository.save(m2);

        List<Member> result = memberRepository.findByUsernameAndAgeGreaterThan("AAA", 15);

        Assertions.assertThat(result.get(0).getUsername()).isEqualTo("AAA");
        Assertions.assertThat(result.get(0).getAge()).isEqualTo(20);
        Assertions.assertThat(result.size()).isEqualTo(1);
}

 

쿼리 실행 결과

쿼리 메소드 필터 조건은 스프링 데이터 JPA 공식 문서를 참고하면 된다.

https://spring.io

 

Spring makes Java simple.

Level up your Java code and explore what Spring can do for you.

spring.io

Projects > Spring Data JPA의 LERN 탭에서 Reference Doc. 문서 참고

 

스프링 데이터 JPA가 제공하는 쿼리 메소드 기능 일부

  • 조회: find...By, read...By, query...By, get...By, 예)findHelloBy 처럼 ...에 식별하기 위한 내용(설명)이 들어가도 된다.
  • COUNT: count...By 반환타입 long
  • EXISTS: exists...By 반환타입 boolean
  • 삭제: delete...By, remove...By 반환타입 long
  • DISTINCT: findDistinct, findMemberDistinctBy
  • LIMIT: findFirst3, findFirst, findTop, findTop3

※ 참 고

이 기능은 엔티티의 필드명이 변경되면 인터페이스에 정의한 메서드 이름도 꼭 함께 변경해야 한다.

그렇지 않으면 애플리케이션 시작하는 시점에 오류 발생.

이렇게 애플리케이션 로딩 시점에 오류를 인지할 수 있는 것이 스프링 데이터 JPA의 매우 큰 장점.

가장 좋은 것은 컴파일 시점에 오류를 발견하는 것.

 

JPA NamedQuery(잘 사용 안함)

엔티티에 @NamedQuery를 선언하고 그것을 가져다 쓰는 방식

사용 코드

//Member.java
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id, username, age"})
//NamedQuery 선언
@NamedQuery(name = "Member.findByUsername",
            query = "select m from Member m where m.username = :username")
public class Member {
    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;
    private String username;
    private int age;
    
    ...
}
//순수 JPA 코드
@Repository
public class MemberJpaRepository {
    @PersistenceContext
    EntityManager em;
    
    public List<Member> findByUsername(String username) {
            return em.createNamedQuery("Member.findByUsername", Member.class)
                    .setParameter("username", username)
                    .getResultList();
    }
}

//스프링 데이터 JPA 코드
public interface MemberRepository extends JpaRepository<Member, Long> {
    //@Query(name = "Member.findByUsername")
    List<Member> findByUsername(@Param("username") String username);
    
    //네임드 쿼리를 우선순위로 찾을때, 기본적으로 엔티티명.메서드 이름으로 조회를 한다. 
    //때문에 위의 주석처럼 @Query를 생략해도 가능하다.
    //만약 실행할 Named 쿼리가 없을 경우 메서드 이름으로 쿼리 생성 전략을 사용한다.
}

 

@Query, 리포지토리 메소드에 쿼리 정의하기

리포지토리 메서드 위에 @Query 어노테이션으로 직접 정적 쿼리 입력

//MemberRepository.java
public interface MemberRepository extends JpaRepository<Member, Long> {
    @Query("select m from Member m where m.username = :username and m.age = :age")
    List<Member> findUser(@Param("username") String username, @Param("age") int age);
}

- 실행할 메서드에 정적 쿼리를 직접 작성하므로 이름 없는 Named 쿼리라 할 수 있다.

- JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있음(매우 큰 장점!)

 

※ 참 고

실무에서는 메소드 이름으로 쿼리 생성 기능은 파라미터가 증가하면 메서드 이름이 매우 지저분해지기 때문에,

@Query 기능을 자주 사용한다.

 

@Query, 값, DTO 조회하기

//MemberRepository.java
public interface MemberRepository extends JpaRepository<Member, Long> {
    //username 문자열 리턴
    @Query("select m.username from Member m")
    List<String> findUsernameList();

    //Memberdto 리턴
    //new operation 사용. (추후 쿼리dsl 사용시 패키지명을 입력하지 않아도 된다.)
    @Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) " +
            "from Member m join m.team t")
    List<MemberDto> findMemberDto();
}

//MemberDto.java
@Data
public class MemberDto {
    private Long id;
    private String username;
    private String teamName;

    public MemberDto(Long id, String username, String teamName) {
        this.id = id;
        this.username = username;
        this.teamName = teamName;
    }
}

 

4. 파라미터 바인딩

위치 기반과 이름 기반으로 나뉘어 지는데,

코드 가독성과 유지보수를 위해 이름 기반을 사용할 것.(위치 기반은 순서가 변경 되었을때 문제가 된다.)

 select m from Member m where m.username = ?0 //위치 기반 
 select m from Member m where m.username = :name //이름 기반

 

※ 참 고(컬렉션 파라미터 바인딩)

파라미터 바인딩은 컬렉션을 in 절로 해서 바인딩도 가능하다.

사용 코드

//MemberRepository.java
public interface MemberRepository extends JpaRepository<Member, Long> {
    //파라미터 바인딩
    @Query("select m from Member m where m.username = :name")
    List<Member> findByName(@Param("name") String name);
    
    //컬렉션 바인딩
    @Query("select m from Member m where m.username in :names")
    List<Member> findByNames(@Param("names") Collection<String> names);
}

//컬렉션 바인딩 테스트 코드
@Test
public void findByNames() {
        Member m1 = new Member("AAA", 10);
        Member m2 = new Member("BBB", 20);
        memberRepository.save(m1);
        memberRepository.save(m2);

        List<Member> result = memberRepository.findByNames(Arrays.asList("AAA", "BBB"));

        for (Member member : result) {
            System.out.println("member = " + member);
        }
}

결과 쿼리
select member0_.member_id as member_i1_0_, member0_.age as age2_0_, member0_.team_id as team_id4_0_, member0_.username as username3_0_ 
from member member0_ where member0_.username in ('AAA' , 'BBB');

 

5. 반환 타입

스프링 데이터 JPA는 유연한 반환 타입 지원

//MemberRepository.java
public interface MemberRepository extends JpaRepository<Member, Long> {
    //컬렉션 조회
    List<Member> findListByUsername(String name);
    //단건 조회
    Member findMemberByUsername(String name);
    //단건optional 조회
    Optional<Member> findOptionalByUsername(String name);
}

조회 결과 값이 많거나 없으면

컬렉션

=> 결과 없음: 빈 컬렉션 반환 (null이 아닌 size가 0인 빈 컬렉션을 반환한다.)

단건 조회

=> 결과 없음: null 반환

      결과가 2건 이상: javax.persistence.NonUniqueResultException 예외 발생

 

6. 페이징과 정렬

순수 JPA 페이징과 정렬

@Repository
public class MemberJpaRepository {
    @PersistenceContext
    EntityManager em;	

    //이름순으로 정렬한 데이터 조회
    public List<Member> findByPage(int age, int offset, int limit) {
        return em.createQuery("select m from Member m where m.age = :age order by m.username desc")
                .setParameter("age", age)
                .setFirstResult(offset)
                .setMaxResults(limit)
                .getResultList();
    }
    //페이징시 필요한 데이터 총 갯수
    public long totalCount(int age) {
        return em.createQuery("select count(m) from Member m where m.age = :age", Long.class)
                .setParameter("age", age)
                .getSingleResult();
    }
}

순수 JPA를 이용한 페이징 처리는 위처럼 정렬한 데이터와 총 갯수를 추출해서
페이지 계산을 적용하여 구현해야 한다.

하지만, 아래에서 설명할 스프링 데이터 JPA가 제공하는 Pageable인터페이스를 사용하면
간단하게 해결이 가능하다.

 

스프링 데이터 JPA 페이징과 정렬

▷ 페이징과 정렬 파라미터

  • org.springframework.data.domain.Sort: 정렬 기능
  • org.springframework.data.domain.Pageable: 페이징 기능(내부에 Sort 포함)

 

▷ 특별한 반환 타입

  • org.springframework.data.domain.Page: 추가 count쿼리 결과를 포함하는 페이징
  • org.springframework.data.domain.Slice: 추가 count쿼리 없이 다음 페이지만 확인 가능(내부적으로 limit+1 조회)
    • 위의 Slice는 페이지를 계산하는게 아니라 더보기로 다음 데이터를 하나씩 불러오는 것.
  • List(자바 컬렉션): 추가 count 쿼리 없이 결과만 반환

 

사용 코드

//MemberRepository.java
public interface MemberRepository extends JpaRepository<Member, Long> {
    //메소드 이름으로 쿼리생성
    //Page 반환 : count 쿼리 동작
    Page<Member> findPageByAge(int age, Pageable pageable);
    //Slice 반환 : count 쿼리 x
    Slice<Member> findSliceByAge(int age, Pageable pageable);
    //List 반환 : count 쿼리 x
    List<Member> findListByAge(int age, Pageable pageable);
}

//테스트 코드
@Test
public void paging() {
        //given
        memberRepository.save(new Member("member1", 10));
        memberRepository.save(new Member("member2", 10));
        memberRepository.save(new Member("member3", 10));
        memberRepository.save(new Member("member4", 10));
        memberRepository.save(new Member("member5", 10));
        int age = 10;
        PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));

        //when
        Page<Member> result = memberRepository.findPageByAge(age, pageRequest);

        //then
        List<Member> content = result.getContent(); //조회된 데이터
        Assertions.assertThat(content.size()).isEqualTo(3); //조회된 데이터 수
        Assertions.assertThat(result.getTotalElements()).isEqualTo(5); //전체 데이터 수
        Assertions.assertThat(result.getNumber()).isEqualTo(0); //페이지 번호
        Assertions.assertThat(result.getTotalPages()).isEqualTo(2); //전체 페이지 번호
        Assertions.assertThat(result.isFirst()).isTrue(); //첫번째 항목인가
        Assertions.assertThat(result.hasNext()).isTrue(); //다음 페이지가 있는가
}

//설명
두 번째 파라미터로 받은 Pagable은 인터페이스다. 
따라서 실제 사용할 때는 해당 인터페이스를 구현한 org.springframework.data.domain.PageRequest 객체를 사용한다.
PageRequest 생성자의 첫 번째 파라미터에는 현재 페이지를, 두 번째 파라미터에는 조회할 데이터 수를 입력한다. 
여기에 추가로 정렬 정보도 파라미터로 사용할 수 있다. 참고로 페이지는 0부터 시작한다.

 

Page 인터페이스는 Slice를 상속받는다.

카운트 쿼리 동작

 

※ 참 고(count 쿼리 분리)

여러개의 테이블이 조인되어 있는 데이터의 count를 뽑을 경우 성능 저하가 일어 날 수 있다.

이때, 메인이 되는 테이블에 대해 left join이 일어날 경우 다른 테이블들을 조인해도 데이터 갯수가 같으므로 메인이 되는 테이블의 카운트만 조회하여 최적화를 할 수 있다.

public interface MemberRepository extends JpaRepository<Member, Long> {
    //count 쿼리 분리
    @Query(value = "select m from Member m left join m.team t",
            countQuery = "select count(m) from Member m")
    Page<Member> findCountByAge(int age, Pageable pageable);
}

//쿼리 결과
데이터 조회시 team과 join을 하는 쿼리가 사용되지만,
카운트는 member만 조회하여 불필요한 join이 발생하지 않아 성능 최적화가 된다.

 

※ 페이지를 유지하면서 엔티티를 DTO로 변환

특히, API를 외부에 노출할때 엔티티를 직접 이용하는 것이 아니라 DTO를 생성하여 스펙을 맞춰 주어야 한다.

때문에 Page<Member>에서 Member가 아닌 MemberDto로 변환을 해주어야 한다.

사용 코드

//MemberDto.java
@Data
public class MemberDto {
    private Long id;
    private String username;
    private String teamName;

    public MemberDto(Long id, String username, String teamName) {
        this.id = id;
        this.username = username;
        this.teamName = teamName;
    }
}

//MemberRepository 테스트 코드
Page<Member> result = memberRepository.findPageByAge(age, pageRequest);
//페이징 값이 담겨있는 result를 dto로 변환
Page<MemberDto> toMap = result.map(m -> new MemberDto(m.getId(), m.getUsername(), null));

데이터를 외부에 노출할땐 Dto로 변환한 값을 전송해 주면 된다.

 

요 약

- Page(count O)

- Slice(count X)는 추가로 limit+1을 조회한다. 그래서 다음 페이지 여부 확인(최근의 더보기 같은 모바일 리스트) 

- List(count X)

- 카운트 쿼리 분리(복잡한 sql에서 사용, 데이터는 left join, 카운트는 left join 안해도 됨)

  전체 카운트 쿼리는 매우 무겁기 때문에 실무에서 매우 중요하다!!

 

7. 벌크성 수정 쿼리

JPA는 영속성이란게 존재하여 엔티티에 수정 사항이 발생할 시 변경감지를 통해 

트랜잭션 커밋 시점에 비교하여 수정이된다.

ex) 회원들의 나이를 1씩 증가시키려고 할때, 변경 감지를 이용하려면 전체 회원들의 정보를 가져와서 수정해줘야 하는데,

벌크성 수정 쿼리는 DB에 직접 update를 하는 것으로 일반적인 mybatis등의 방식이다.

 

순수 JPA 사용 코드

@Repository
public class MemberJpaRepository {
    @PersistenceContext
    EntityManager em;
    
    //벌크성 수정
    public int bulkAgePlus(int age) {
        return em.createQuery("update Member m set m.age = m.age + 1" +
                " where m.age >= :age")
                .setParameter("age", age)
                .executeUpdate();
    }
}

 스프링 데이터 JPA 사용 코드

public interface MemberRepository extends JpaRepository<Member, Long> {
    //벌크성 수정
    @Modifying
    @Query("update Member m set m.age = m.age + 1 where m.age >= :age")
    int bulkAgePlus(@Param("age") int age);
}

//테스트 코드
@Test
public void bulkUpdate() {
        //given
        memberRepository.save(new Member("member1", 10));
        memberRepository.save(new Member("member2", 19));
        memberRepository.save(new Member("member3", 20));
        memberRepository.save(new Member("member4", 30));
        memberRepository.save(new Member("member5", 40));
        Member member6 = new Member("member6", 50);
        memberRepository.save(member6);
        
        //when
        int updateCount = memberRepository.bulkAgePlus(20);

        em.flush();
        em.clear();
        //영속성 컨텍스트 초기화로 인해 아래와 같이 나이가 변경되도 변경 감지가 일어나지 않는다.
        member6.setAge(member6.getAge()+2);

        List<Member> result = memberRepository.findByUsername("member6");
        Member member6 = result.get(0);
        //em.flush, em.clear을 하지 않으면 영속성 컨텍스트에 남아있는 member6의 나이 50이 출력된다
        System.out.println("member6 = " + member6.getAge());  //member6 = 51

        //then
        Assertions.assertThat(updateCount).isEqualTo(4);
}

//추가 옵션
//clearAutomatically=true 옵션을 사용하면 별도로 em.clear()을 안해줘도 된다.
@Modifying(clearAutomatically = true)
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);

 

- 벌크 수정, 삭제 쿼리는 @Modifying 어노테이션을 사용

  사용하지 않으면 getSingleResult, getResultList로 반환하는 줄 알고 org.hibernate.hql.internal.QueryExecutionRequestException: Not supported for 

DML operations 이라는 예외 발생

 

- 벌크성 쿼리를 실행하고 나서 영속성 컨텍스트 초기화 옵션: @Modifying(clearAutomatically=true) 기본은 false

  이 옵션 없이 회원을 findById 등으로 다시 조회하면 영속성 컨텍스트에 이전 값이 남아서 문제가 될 수 있다.

  만약 다시 조회해야 하면, 영속성 컨텍스트 초기화를 꼭 해야한다.  

 

※ 정 리

벌크 연산은 영속성 컨텍스트를 무시하고 실행하기 때문에, 영속성 컨텍스트에 남아있는 엔티티의 상태와 DB에 엔티티 상태가 달라질 수 있다.

권장 방안

=> 1. 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산을 먼저 수행한다.

     2. 부득이하게 영속성 컨텍스트에 엔티티가 있을 경우 벌크 연산 직후 영속성 컨텍스트를 초기화 한다.

 

8. @EntityGraph

연관된 엔티티들을 SQL 한번에 조회하는 방법

엔티티그래프 사용법에 앞서 JPA의 지연로딩과 페치 조인에 대해 먼저 알아야 한다.

 

아래의 Member 엔티티를 보면 member -> team은 지연 로딩 관계이다.

//Member.java
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id, username, age"})
public class Member {
    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;
    private String username;
    private int age;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

	...
}

지연로딩 사용 코드

@Test
public void findMemberLazy() {
        //given
        //member1 -> teamA
        //member2 -> teamB
        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");
        teamRepository.save(teamA);
        teamRepository.save(teamB);
        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member1", 10, teamB);
        memberRepository.save(member1);
        memberRepository.save(member2);

        em.flush();
        em.clear();

        //when
        List<Member> members = memberRepository.findAll();

        //then
        for (Member member : members) {
            System.out.println("member.getUsername() = " + member.getUsername());
            //member.getTeam()은 db에서 조회하기 전인 프록시 데이터 형태이다. team을 조회하는 sql문이 생성되지 않음
            //출력값 => member.getTeam().getClass() = class study.datajpa.entity.Team$HibernateProxy$TaiCEYpU
            System.out.println("member.getTeam().getClass() = " + member.getTeam().getClass());
            //team 엔티티 안의 name를 조회하므로 team을 조회하는 sql문이 지연 로딩으로 생성됨.
            System.out.println("member.getTeam().getName() = " + member.getTeam().getName());
        }
}

위의 코드는 member과 team간에 지연로딩으로 인해 team 엔티티 내부의 필드를 조회할때 team을 조회하는 쿼리가 생성되고, member에서 조회하는 team 자체는 프록시 데이터로 존재한다.

 

fetch join 사용 코드

위의 지연로딩 사용 코드에서 fetch join만 걸어준다

//MemberRepository.java
public interface MemberRepository extends JpaRepository<Member, Long> {
    //fetch join
    //left join을 하면 left outer join으로 쿼리가 생성되고,
    //     join을 하면 inner join으로 쿼리가 생성된다.
    @Query("select m from Member m left join fetch m.team t")
    List<Member> findMemberFetchJoin();
}

//테스트 코드
//when
List<Member> members = memberRepository.findMemberFetchJoin();
//then
for (Member member : members) {
  //페치 조인으로 member조회시 team과 join되어 sql문이 생성
  System.out.println("member.getUsername() = " + member.getUsername());
  //페치 조인으로 team도 조회가 되서 프록시가 아닌 원데이터가 출력
  //출력값 => member.getTeam().getClass() = class study.datajpa.entity.Team
  System.out.println("member.getTeam().getClass() = " + member.getTeam().getClass());
  System.out.println("member.getTeam().getName() = " + member.getTeam().getName());
}

쿼리 결과

 

스프링 데이터 JPA는 JPA가 제공하는 엔티티 그래프 기능을 편리하게 사용하도록 도와준다.

이 기능을 사용하면 위의 사용코드 JPQL없이 페치 조인을 사용할 수 있다.(JPQL + 엔티티 그래프도 가능)

사용 코드

//MemberRepository.java
public interface MemberRepository extends JpaRepository<Member, Long> {
    //엔티티 그래프
    
    //공통 메서드 오버라이드
    @Override
    @EntityGraph(attributePaths = {"team"})
    List<Member> findAll();
   
    //JPQL + 엔티티 그래프
    @EntityGraph(attributePaths = {"team"})
    @Query("select m from Member m")
    List<Member> findMemberEntityGraph();
    
    //메서드 이름으로 쿼리에서 특히 편하다.
    @EntityGraph(attributePaths = {"team"})
    List<Member> findByUsername(String username);
}

위의 3개의 메서드 모두 쿼리 결과는 같다.
member와 team간에 left outer join

엔티티 그래프는
JPA의 fetch join을 편리하게 해주는 기능.

※ EntityGraph 정리

- 사실상 페치 조인(FETCH JOIN)의 간편 버전

- LEFT OUTER JOIN 사용

 

추가로 엔티티에 설정을 하는 NamedEntityGraph도 있다.

사용 코드

//Member.java
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id, username, age"})
//NamedEntityGraph 정의
@NamedEntityGraph(name = "Member.all", attributeNodes = @NamedAttributeNode("team"))
@NamedQuery(name = "Member.findByUsername",
            query = "select m from Member m where m.username = :username")
public class Member {
    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;
    private String username;
    private int age;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;
    
    ...
}

//MemberRepository.java
@EntityGraph("Member.all")
@Query("select m from Member m")
List<Member> findMemberEntityGraph();

 

9. JPA Hint & Lock

JPA 쿼리 힌트(SQL 힌트가 아니라 JPA 구현체에 제공하는 힌트)

 

JPA Hint 사용 코드

//MemberRepository.java
public interface MemberRepository extends JpaRepository<Member, Long> {
    //쿼리 힌트
    @QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
    Member findReadOnlyByUsername(String username);
}

//테스트 코드
@Test
public void queryHint() {
        //given
        Member member = new Member("member1", 10);
        memberRepository.save(member);
        em.flush();
        em.clear();

        //when
        Member findMember = memberRepository.findReadOnlyByUsername("member1");
        findMember.setUsername("changeMember");
        em.flush(); //update 쿼리 실행x
}

테스트 코드의 findMember를 DB에서 조회하였으므로 영속성 컨텍스트에 저장되어
setUsername를 했을 경우 트랜잭션 커밋 시점에 변경감지로 update 쿼리가 나가야 하나,
쿼리 힌트에 readOnly메서드를 사용하여 update가 일어나지 않는다.

 

JPA Lock 사용 코드

//MemberRepository.java
public interface MemberRepository extends JpaRepository<Member, Long> {
    //Lock
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    List<Member> findLockByUsername(String username);
}

//테스트 코드 실행 sql문
select
        member0_.member_id as member_i1_0_,
        member0_.age as age2_0_,
        member0_.team_id as team_id4_0_,
        member0_.username as username3_0_ 
    from
        member member0_ 
    where
        member0_.username=? for update


Lock어노테이션 사용시 쿼리 마지막에 for update 붙는다.

※ 참 고 (for update)

select 문 가장 뒤에 작성을 하며, select 문의 결과를 조작(insert, update, delete) 하고

커밋 또는 롤백을 하기전 까지 다른 쿼리가 동작 하지 못하게 하는 예약어.

=> 여러 요청들이 동시에 일어날 경우 데이터의 정합성이 떨어지지 않도록 동기화처리 하는 것.

 

10. 사용자 정의 리포지토리 구현(실무에서 많이 사용)

- 스프링 데이터 JPA 리포지토리는 인터페이스만 정의하고 구현체는 스프링이 자동 생성

- 스프링 데이터 JPA가 제공하는 인터페이스를 직접 구현하면 구현해야 하는 기능이 너무 많음.

- 다양한 이유로 인터페이스의 메서드를 직접 구현 하려면?

  • JPA 직접 사용(EntityManager)
  • 스프링 JDBC Template 사용
  • Mybatis 사용
  • 데이터베이스 커넥션 직접 사용 등등
  • Querydsl 사용(많이 사용)

방법

=> 1. 새로운 사용자 정의 인터페이스를 생성한다.

      (MemberRepositoryCustom.java 인터페이스 생성)

      2. 생성한 인터페이스명+Impl 이름의 클래스를 만들어 구현한다.

      (MemberRepositoryCustomImpl.java에 MemberRepositoryCustom.java 상속받고 구현)

      3. 스프링 데이터 JPA를 상속한 리파지토리 인터페이스에 생성한 사용자 정의 인터페이스도 상속받는다.

      (기존 MemberRepository.java에 추가로 MemberRepositoryCustom를 상속 받는다)

 

사용 코드

1. 사용자 정의 인터페이스 생성
//MemberRepositoryCustom.java
public interface MemberRepositoryCustom {
    List<Member> findMemberCustom();
}

2. 새로 만든 사용자정의 인터페이스 구현체 생성
네이밍 규약은 사용자정의 인터페이스명 + Impl
스프링 데이터 JPA가 인식해서 스프링 빈으로 등록
//MemberRepositoryImpl.java
@RequiredArgsConstructor
public class MemberRepositoryCustomImpl implements MemberRepositoryCustom {
    private final EntityManager em;

    @Override
    public List<Member> findMemberCustom() {
        return em.createQuery("select m from Member m")
                .getResultList();
    }
}

3. 기존 스프링 데이터 JPA를 상속한 인터페이스에 새로 만든 인터페이스도 상속 받는다.
//MemberRepository.java
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
	...
}

//테스트 코드
@Test
public void custom() {
        List<Member> memberCustom = memberRepository.findMemberCustom();
}

 

11. Auditing

데이터를 입력, 수정(엔티티를 생성, 변경)할 때 변경한 사람과 시간을 자동으로 받아오는 기능.

(등록일, 수정일, 등록자, 수정자)

 

순수 JPA 사용 코드

//JpaBaseEntity.java  상속할 공통 엔티티 생성
@MappedSuperclass	//공통매핑정보
@Getter
public class JpaBaseEntity {

    @Column(updatable = false)
    private LocalDateTime createDate;
    private LocalDateTime updateDate;

    @PrePersist
    public void prePersist() {
        LocalDateTime now = LocalDateTime.now();
        createDate = now;
        updateDate = now;
    }

    @PreUpdate
    public void preUpdate() {
        updateDate = LocalDateTime.now();
    }
}

//Member.java
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id, username, age"})
public class Member extends JpaBaseEntity{	//공통 엔티티 상속
	...
}

//테스트 코드
@Test
public void JpaEventBaseEntity() throws Exception {
        //given
        Member member = new Member("member1");
        memberRepository.save(member);

        Thread.sleep(100);
        member.setUsername("member2");

        em.flush(); //변경 감지로 인한 update 쿼리가 실행됨
        em.clear();

        //when
        Member findMember = memberRepository.findById(member.getId()).get();

        //then
        System.out.println("findMember.createDate = " + findMember.getCreateDate());
        System.out.println("findMember.updateDate = " + findMember.getUpdateDate());
}

 

스프링 데이터 JPA 사용 코드 (설정을 해줘야 한다.)

1. 스프링 부트 설정 클래스에 @EnableJpaAuditing 적용

2. 생성할 공통 엔티티에 @EntityListeners(AuditingEntityListener.class) 적용

 

사용 코드

//메인 애플리케이션인 DataJpaApplication.java
@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {
	public static void main(String[] args) {
		SpringApplication.run(DataJpaApplication.class, args);
	}

	//등록자, 수정자를 처리해주는 AuditorAware 스프링 빈 등록
	//실무에서는 세션 정보나 스프링 시큐리티 로그인 정보에서 ID를 받는다.
	@Bean
	public AuditorAware<String> auditorProvider() {
		return () -> Optional.of(UUID.randomUUID().toString());
	}
}

//BaseEntity.java 공통 엔티티 생성
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass	//이 옵션을 줘야 상속받는 엔티티에 필드가 생성됨
@Getter
public class BaseEntity {
    @CreatedDate    //생성일
    @Column(updatable = false)
    private LocalDateTime createdDate;
    @LastModifiedDate   //수정일
    private LocalDateTime lastModifiedDate;

    @CreatedBy      //생성자
    @Column(updatable = false)
    private String createdBy;
    @LastModifiedBy //수정자
    private String lastModifiedBy;
}

//Member.java 엔티티에 공통 엔티티 상속
@Entity
public class Member extends BaseEntity{
	...
}

//테스트 코드
@Test
public void JpaEventBaseEntity() throws Exception {
        //given
        Member member = new Member("member1");
        memberRepository.save(member);

        Thread.sleep(100);
        member.setUsername("member2");

        em.flush(); //변경 감지로 인한 update 쿼리가 실행됨
        em.clear();

        //when
        Member findMember = memberRepository.findById(member.getId()).get();

        //then
        System.out.println("findMember.createDate = " + findMember.getCreatedDate());
        System.out.println("findMember.updateDate = " + findMember.getLastModifiedDate());
        System.out.println("findMember.createBy = " + findMember.getCreatedBy());
        System.out.println("findMember.lastModifiedBy = " + findMember.getLastModifiedBy());
}

BaseEntity의 입력 또는 수정이 일어날 때마다 DataJpaApplicaion.java에 있는

auditorProvidor()이 호출되어 값을 리턴한다.

 

※ 참 고

위의 사용 코드에서 BaseEntity의 @MappedSuperclass가 없으면 상속받는 Member 테이블에 BaseEntity필드가 생성되지 않는다.

메인 애플리케이션에 @EnableJpaAuditing와 BaseEntity.java의 @EntityListeners(AuditingEntityListener.class)가 있어야 값이 들어간다.

 

♠ 실무에서 대부분의 엔티티는 등록시간, 수정이 필요하지만 등록자, 수정자는 없을 수도 있다.

이를 대비해 날짜Base엔티티, 작성자Base엔티티를 분리하여 원하는 타입을 상속해서 사용할 수 있다.

사용 코드

public class BaseTimeEntity {
        @CreatedDate
        @Column(updatable = false)
        private LocalDateTime createdDate;
        @LastModifiedDate
        private LocalDateTime lastModifiedDate;
}
public class BaseEntity extends BaseTimeEntity {
        @CreatedBy
        @Column(updatable = false)
        private String createdBy;
        @LastModifiedBy
        private String lastModifiedBy;
}

 

※ 참 고(MappedSuperclass )

- 공통 매핑 정보가 필요할 때 사용

- 상속매핑이 아니라 공통정보 테이블은 따로 생성이 안된다.

- 상속관계 매핑X (별도의 테이블이 생성되지 않는다)

- 엔티티X, 테이블과 매핑X

- 조회, 검색 불가(em.find(BaseEntity) 불가)

- 직접 생성해서 사용할 일이 없으므로 추상 클래스 권장

- 테이블과 관계 없고, 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할

- 참고, @Entity클래스는 엔티티나 @MappedSuperclass로 지정한 클래스만 상속이 가능

 

12. web 확장 - 도메인 클래스 컨버터

HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아서 바인딩

 

사용 코드

@RestController
@RequiredArgsConstructor
public class MemberController {
    private final MemberRepository memberRepository;

    @GetMapping("/members/{id}")
    public String findMember(@PathVariable("id") Long id) {
        Member member = memberRepository.findById(id).get();
        return member.getUsername();
    }
	
    //도메인 클래스 컨버터 적용. 위의 메서드와 결과값 동일
    @GetMapping("/members/{id}")
    public String findMember(@PathVariable("id") Member member) {
        return member.getUsername();
    }
	
    //구동시 초기 데이터 등록
    @PostConstruct
    public void init() {
        memberRepository.save(new Member("userA"));
    }
}

- HTTP 요청은 회원 id를 파라미터로 받지만, 도메인 클래스 컨버터가 중간에 동작하여
  회원 엔티티 객체를 반환한다.
- 도메인 클래스 컨버터로 리파지토리를 사용해서 엔티티를 찾는다.

※ 주 의

도메인 클래스 컨버터로 엔티티를 파라미터로 받으면, 이 엔티티는 단순 조회용으로만 사용해야 한다.

트랜잭션이 없는 범위에서 엔티티를 조회했으므로, 엔티티를 변경해도 DB에 반영되지 않는다.(영속성 컨텍스트랑 관계x)

 

13. Web 확장 - 페이징과 정렬

스프링 데이터가 제공하는 페이징과 정렬 기능을 스프링 MVC에서 편리하게 사용할 수 있다.

 

사용 코드

//MemberDto.java
@Data
public class MemberDto {
    private Long id;
    private String username;
    private String teamName;
    
    //엔티티에서 DTO로는 바라보지 않게하고 DTO에서 엔티티로 바라보는 것이 좋다.
    public MemberDto(Member m) {
        this.id = m.getId();
        this.username = m.getUsername();
    }
}

//MemberController.java
@RestController
@RequiredArgsConstructor
public class MemberController {
    private final MemberRepository memberRepository;

    @GetMapping("/members")
    //Member엔티티를 직접 반환하는 것이 아니라, map()을 통해 MemberDto로 받아서 뿌려줄 것.
    //외부에 엔티티를 직접 노출해서는 안된다!!!!!
    public Page<MemberDto> list(Pageable pageable) {
//        Page<Member> page = memberRepository.findAll(pageable);
//        Page<MemberDto> pageDto = page.map(m -> new MemberDto(m));
//        return pageDto;
        //위의 세줄을 인라인으로 변경하면 아래와 같다.
        return memberRepository.findAll(pageable).map(MemberDto::new);
    }

    //초기 페이지 개별 설정 옵션
    @GetMapping("/members_page")
        public Page<Member> list_page(@PageableDefault(page = 1, size = 3, sort = "username") Pageable pageable) {
        Page<Member> page = memberRepository.findAll(pageable);
        return page;
    }
}

- 파라미터로 Pageable을 받을 수 있다.

- Pageable은 인터페이스, 실제는 org.springframework.data.domain.PageRequest 객체 생성

 

요청 파라미터

  • ex)http://127.0.0.1:8080/members?page=0&size=3&sort=id,desc
  • page: 현재 페이지, 0부터 시작한다.(기본이 0)
  • size: 한 페이지에 노출할 데이터 건수
  • sort: 정렬 조건을 정의한다. ex)정렬 속성, 정렬 속성...(ASC|DESC) 기본이ASC
  • 기본 사이즈는 application.yml에서 공통적으로 설정해줄 수 있다.

글로벌 설정: 스프링 부트(application.yml)

spring.data.web.pageable.default-page-size=20   //기본 페이지 사이즈

spring.data.web.pageable.max-page-size=2000  //최대 페이지 사이즈

 

14. 스프링 데이터 JPA 분석

스프링 데이터 JPA 구현체

- 스프링 데이터 JPA가 제공하는 공통 인터페이스의 구현체

- org.springframework.data.jpa.repository.support.SimpleJpaRepository

@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> ...{
      @Transactional
      public <S extends T> S save(S entity) {
          if (entityInformation.isNew(entity)) {
              em.persist(entity);
              return entity;
          } else {
              return em.merge(entity);
     	}
     }
     ...
}

- @Repository 적용: componentScan을 포함하고 JPA 예외를 스프링이 추상화한 예외로 변환

- @Transactional 적용

  • JPA의 모든 변경은 트랜잭션 안에서 동작
  • 스프링 데이터 JPA는 변경(등록, 수정, 삭제) 메서드를 트랜잭션 처리
  • 서비스 계층에서 트랜잭션을 시작하지 않으면 리파지토리에서 트랜잭션 시작
  • 서비스 계층에서 트랜잭션을 시작하면 리파지토리는 해당 트랜잭션을 전파 받아서 사용
  • 그래서 스프링 데이터 JPA를 사용할 때 서비스딴에 트랜잭션이 없어도 데이터 등록, 변경이 가능했음(서비스 계층에 없어도 리파지토리 계층에 걸려있으므로 가능 함)

- @Transactional(readOnly = true)

   데이터를 단순히 조회만 하고 변경하지 않는 트랜잭션에서 readOnly=true 옵션을 사용하면,

   flush();를 생략해서 약간의 성능 향상을 얻을 수 있다.

   조회 기능이 많은 계층에선 readOnly=true로 두고 등록, 수정, 삭제 메서드엔 @Transactional 재정의

 

15. 새로운 엔티티를 구별하는 방법(persist, merge)  중요!!

매우 중요!!

★save() 메서드

   - 새로운 엔티티면 저장(persist)

   - 새로운 엔티티가 아니면 병합(merge)

 

새로운 엔티티를 판단하는 기본 전략

  • 식별자가 객체일 때 null로 판단
  • 식별자가 자바 기본 타입일 때 0으로 판단(기본타입은 int, long.  기본타입엔 null이 들어갈 수 없다.)
  • 엔티티에 Persistable 인터페이스를 구현해서 판단 로직으로 변경 가능

설명 코드

//ItemRepository.java
public interface ItemRepository extends JpaRepository<Item, Long> {
}

//Item.java
@Entity
@Getter
public class Item {
    @Id
    @GeneratedValue
    private Long id;
}

//테스트 코드
@SpringBootTest
class ItemRepositoryTest {
    @Autowired
    ItemRepository itemRepository;

    @Test
    public void save() {
        Item item = new Item();
        itemRepository.save(item);
    }
}

Item엔티티 id는 GeneratedValue가 걸려 있으므로 

아래의 스프링 데이터 JPA 구현체의 save() 메서드를 보면 if문으로 엔티티가 객체일 경우 null인지 확인하고 em.persist가 일어나면서 GeneratedValue로 인해 값이 들어오는 것을 볼 수 있다.

SimpleJpaRepository.java(스프링 데이터 JPA 구현체)

하지만, id값을 @GeneratedValue로 사용하지 못하고 직접 넣어야 할때

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item {
    @Id
    private String id;

    public Item(String id) {
        this.id = id;
    }
}

어쩔수 없이 @GeneratedValue를 사용하지 못하고
@Test
public void save() {
        Item item = new Item("A");
        itemRepository.save(item);
}
이런식으로 값을 넣어야 할 경우 save()시 객체가 null이 아니기 때문에
엔티티에 값이 있다고 생각하며 persist가 아닌 merge가 실행된다.

객체가 null이 아니므로 else조건을 타게된다.(이러면 안됨)

 

해결 방안(엔티티에 Persistable 구현)

@Entity
@Getter
//Auditing 사용하기 위한 어노테이션
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Item implements Persistable<String> {
    @Id
    private String id;
    public Item(String id) {
        this.id = id;
    }
    //새로운 엔티티인지 확인을 위한 날짜 필드 생성
    //해당 엔티티에 접근시 자동 생성
    @CreatedDate
    private LocalDateTime createdDate;

    @Override
    public boolean isNew() {
        return createdDate == null;
    }
}

createdDate는 트랜잭션이 종료되고 생성되므로
엔티티를 생성할때 값을 넣어 save()를 해도 createdDate가 null이기 때문에
해당 엔티티에 대한 isNew값은 true로 넘어가서 persist를 실행하게 된다.

 

16. Projections

- 엔티티 대신에 DTO를 편리하게 조회할 때 사용

- 전체 엔티티가 아니라 하나 정도의 필드만 조회하고 싶을때 유용

 

인터페이스 기반 Closed Projections 사용 코드

//UsernameOnly.java
public interface UsernameOnly {
    //조회할 엔티티의 필드를 getter 형식으로 지정하면 해당 필드만 조회
    String getUsername();
}

//MemberRepository.java
public interface MemberRepository extends JpaRepository<Member, Long> {
    //projections
    //메서드 이름은 자유, 반환 타입을 프로젝션 인터페이스로 맞춰준다.
    List<UsernameOnly> findProjectionsByUsername(String username);
}

//테스트 코드
@Test
public void projections() {
        //given
        Team team = new Team("teamA");
        em.persist(team);
        Member m1 = new Member("m1", 0, team);
        Member m2 = new Member("m2", 0, team);
        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.getUsername() = " + usernameOnly.getUsername());
        }
}

쿼리 결과는 아래와 같이 인터페이스로 만든 username만 조회해서 가져온다.(JPQL SELECT절 최적화)

조회할 엔티티 필드를 getter 형식으로 지정하면 해당 필드만 선택해서 조회(Projection)

 

인터페이스 기반 Closed Projections 사용 코드

public interface UsernameOnly {
    //아래와 같이 스프링의 SpEL 문법도 지원
    //출력하면 username, age, team.name의 문자열을 그대로 반환한다.
    @Value("#{target.username + ' ' + target.age + ' ' + target.team.name}")
    String getUsername();
}

//테스트 코드 출력 결과
usernameOnly.getUsername() = m1 0 teamA

DB에서 엔티티 필드를 다 조회한 다음에 계산하기 때문에 Close Projections보다 쿼리가 길다.
즉, JPQL SELECT절 최적화가 안됨

 

클래스 기반 Projection

위의 인터페이스 예제와 비슷하게 클래스 기반으로도 가능하다.
인터페이스는 프록시에서 가져오는 것이 차이점
//UsernameOnlyDto.java
public class UsernameOnlyDto {
    private final String username;

    public UsernameOnlyDto(String username) {
        this.username = username;
    }

    public String getUsername() {
        return username;
    }
}

//MemberRepository.java
public interface MemberRepository extends JpaRepository<Member, Long> {
    //projections
    List<UsernameOnlyDto> findProjectionsByUsername(String username);
}

위의 findProjectionsByUsername 메서드를 동적으로도 가능하다.
동적 Projections
public interface MemberRepository extends JpaRepository<Member, Long> {
    //projections
    <T> List<T> findProjectionsByUsername(String username, Class<T> type);
}

//동적 Projections 사용 테스트 코드
//when
List<UsernameOnlyDto> result = memberRepository.findProjectionsByUsername("m1", UsernameOnlyDto.class);

 

중첩 구조 처리

사용 코드

//NestedClosedProjection.java
public interface NestedClosedProjection {
    //프로젝션 대상의 root
    String getUsername();

    //내부 인터페이스에서 가져온거라 root가 아니다	
    TeamInfo getTeam();
	
    //내부 인터페이스 생성
    interface TeamInfo {
        String getName();
    }
}

//테스트 코드
//when
List<NestedClosedProjection> result = memberRepository.findProjectionsByUsername("m1", NestedClosedProjection.class);
        
//then
for (NestedClosedProjection nestedClosedProjection : result) {
    System.out.println("getUsername() = " + nestedClosedProjection.getUsername());
    System.out.println("getTeam() = " + nestedClosedProjection.getTeam());
}

//출력 결과
getUsername() = m1
getTeam() = Team(id=1, name=teamA)

인터페이스 중첩 구조는 프로젝션 대상이 root 엔티티면, JPQL SELECT절이 최적화 되어 Member 테이블에선 username만 조회하고, root가 아닌 team은 전부 가져와 진다.

아래는 위의 코드 쿼리이다.

  • 프로젝션 대상이 ROOT 엔티티면, JPQL SELECT절 최적화
  • 프로젝션 대상이 ROOT가 아니면 LEFT OUTER JOIN 처리, 모든 필드를 SELECT해서 엔티티로 조회한 다음에 계산

정 리

- 프로젝션 대상이 ROOT 엔티티면 유용하다.

- 프로젝션 대상이 ROOT 엔티티를 넘어가면 JPQL SELECT 최적화가 안된다.

- 실무의 복잡한 쿼리를 해결하기에는 한계가 있다.

- 실무에서는 단순할 때만 사용, 조금 복잡해지면 QueryDSL 사용할 것.

 

17. 네이티브 쿼리(가급적 사용x)

가급적 네이티브 쿼리는 사용하지 않는게 좋다. 어쩔 수 없을 때 사용 할 것.

 

네이티브 쿼리 제약

- Sort파라미터를 통한 정렬이 정상 동작하지 않을 수 있음(믿지 말고 직접 처리)

- JPQL처럼 애플리케이션 로딩 시점에 문법 확인 불가(3번에서 설명한 쿼리 메소드 기능들은

애플리케이션 로딩 시점에 문법 오류 확인가능)

- 동적 쿼리 불가

 

기본 네이티브 쿼리 사용 코드

//MemeberRepository.java
public interface MemberRepository extends JpaRepository<Member, Long> {
    //네이티브 쿼리
	@Query(value = "select * from member where username = ?", nativeQuery = true)
    Member findByNativeQuery(String username);
}

위와 같이 @Query안에 sql문법을 그대로 넣는 것이 네이티브 쿼리이다.

 

최근에 나온 페이징 처리가 되는 Projections 네이티브 쿼리 사용 코드

(페이징 처리가 되는 이점이 있어서 사용할만 하다.)

//MemberProjection.java (프로젝션 인터페이스 생성)
public interface MemberProjection {
    Long getId();
    String getUsername();
    String getTeamName();
}

//MemberRepository.java
public interface MemberRepository extends JpaRepository<Member, Long> {
    //프로젝션의 getter명으로 명칭을 매칭해준다.(ex. as id, as teamName)
    @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> findByNativeQueryProjection(Pageable pageable);
}

//테스트 코드
Page<MemberProjection> result = memberRepository.findByNativeQueryProjection(PageRequest.of(0, 3));

//출력된 sql문(프로젝션으로 엔티티 필드 전부가 아닌 해당 필드만 SELECT절에 사용)
//JPQL SELECT절이 최적화 된다(프로젝션 사용 장점)
select
        m.member_id as id,
        m.username,
        t.name as teamName 
    from
        member m 
    left join
        team t limit ?

 

공유하기

facebook twitter kakaoTalk kakaostory naver band
loading