Development/JPA

[2] 자바 ORM 표준 JPA 프로그래밍 - 기본편(객체지향 쿼리) [2/2]

루루지 2021. 5. 20. 20:47
반응형

JPQL(Java Persistence Query Language)

- JPA를 사용하면 엔티티 객체를 중심으로 개발

- 문제는 검색 쿼리

- 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색

- JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어 제공

- SQL과 문법 유사, SELECT, FROM ,WHERE ,GROUP BY, HAVING, JOIN 지원

- JPQL은 엔티티 객체를 대상으로 쿼리

- SQL은 데이터베이스 테이블을 대상으로 쿼리

- 테이블이 아닌 객체를 대상으로 검색하는 객체 지향 쿼리

- SQL을 추상화해서 특정 데이터베이스 SQL에 의존X

- JPQL을 한마디로 정의하면 객체 지향 SQL

사용코드

List<Member> result = em.createQuery("select m from Member m where m.username like '%kim%")
                    .getResultList();

또는

QueryDSL 소개

- 문자가 아닌 자바코드로 JPQL을 작성할 수 있음

- JPQL 빌더 역할

- 컴파일 시점에 문법 오류를 찾을 수 있음

- 동적쿼리 작성 편리함

- 단순하고 쉬움

- 실무 사용 권장

사용 코드

 

JPQL 문법

- select m from Member as m where m.age > 18

- 엔티티와 속성은 대소문자 구분O (Member, age)

- JPQL 키워드는 대소문자 구분X (SELECT, FROM, where)

- 엔티티 이름 사용, 테이블 이름이 아님(Member)

- 별칭은 필수(m) (as는 생략가능)

 

집합 사용 가능

파라미터 바인딩

 

프로젝션 - 여러 값 조회

3번의 new 명령어로 조회 사용 코드

MemberDTO.java 생성 후 username, age로 생성자를 만들어준다.

//MemberDTO.java
public class MemberDTO {
    private String username;
    private int age;
    
	// new 명령어로 조회 하기 위한 생성자 추가
    public MemberDTO(String username, int age) {
        this.username = username;
        this.age = age;
    }
    //getter, setter
}
///////////////////////////////////////////////////

//JpaMain.java
Member member = new Member();
member.setUsername("member1");
member.setAge(10);
em.persist(member);

List<MemberDTO> resultList = em.createQuery("select new jpql.MemberDTO(m.username, m.age) from Member m",
                              MemberDTO.class).getResultList();

MemberDTO memberDTO = resultList.get(0);
System.out.println("username = " + memberDTO.getUsername());
System.out.println("age = " + memberDTO.getAge());

 

페이징 API

- JPA는 페이징을 다음 두 API로 추상화

- setFirstResult(int startPosition) : 조회 시작 위치(0부터 시작)

- setMaxResults(int maxResult) : 조회할 데이터 수

 

관계형DB 방언에 맞게 페이징 처리 SQL문이 만들어 진다.

 

사용 코드

for(int i=0; i<100; i++) {
	Member member = new Member();
	member.setUsername("member"+i);
	member.setAge(i);
	em.persist(member);
}

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

List<Member> result = em.createQuery("select m from Member m order by m.age desc", Member.class)
                   .setFirstResult(1) // 1번부터 시작
                   .setMaxResults(10) // 한페이지에 10개씩
                   .getResultList();

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

결과(H2DB 방언 기준)

 

조인

 

서브 쿼리

 

JPA 서브 쿼리 한계

- JPA는 WHERE, HAVING 절에서만 서브 쿼리 사용 가능

- SELECT 절도 가능(하이버네이트에서 지원)

- FROM 절의 서브 쿼리는 현제 JPQL에서 불가능

   조인으로 풀 수 있으면 풀어서 해결

 

경로 표현식

.(점)을 찍어 객체 그래프를 탐색하는 것

상태 필드(state field) : 단순히 값을 저장하기 위한 필드

                              (ex: m.username)

       =>특징 : 경로 탐색의 끝, 탐색X

연관 필드(association field) : 연관관계를 위한 필드

  - 단일 값 연관 필드 : @ManyToOne, @OneToOne, 대상이 엔티티(ex : m.team)

      =>특징 : 묵시적 내부 조인(inner join)발생, 탐색O

  - 컬렉션 값 연관 필드 : @OneToMany, @ManyToMany, 대상이 컬렉션(ex : m.orders)

      =>특징 : 묵시적 내부 조인 발생, 탐색X

 

아래와 같이 연관 필드로 묵시적 조인을 하게 되면 조인이 발생하는 것을 추적하기가 힘들다.

String query = "select m.team from Member m";

List<Team> result = em.createQuery(query, Team.class)
	.getResultList();

 

따라서, 아래와 같이 묵시적 조인을 사용하지 말고 명시적 조인을 사용 할 것.

String query = "select t from Member m join m.team t";

List<Team> result = em.createQuery(query, Team.class)
	.getResultList();

 

페치 조인(fetch join)  실무에서 정말 중요!!

- SQL 조인 종류 X

- JPQL에서 성능 최적화를 위해 제공하는 기능

- 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능

- join fetch 명령어 사용

- 페치 조인 ::= [LEFT[OUTER] | INNER] JOIN FETCH 조인경로

   ex)  [JPQL] select m from Member m join fetch m.team

         [SQL] SELECT M.*, T.* FROM MEMBER M 

                 INNER JOIN TEAM T ON M.TEAM_ID = T.ID

 

사용 예제

위와 같이 팀A를 참조하는 회원2, 팀B를 탐조하는 회원1명이 있을 경우

//  JpaMain.java
Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);
Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);

Member member1 = new Member();
member1.setUsername("회원1");
member1.changeTeam(teamA);
em.persist(member1);

Member member2 = new Member();
member2.setUsername("회원2");
member2.changeTeam(teamA);
em.persist(member2);

Member member3 = new Member();
member3.setUsername("회원3");
member3.changeTeam(teamB);
em.persist(member3);

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

String query = "select m from Member m";
List<Member> result = em.createQuery(query, Member.class)
	.getResultList();
    
// 아래에서 member객체 안의 team을 호출할 때마다 team 개수마다 select 쿼리가 날라간다(성능 취약)    
for (Member member : result) {
	System.out.println("member = " + member.getUsername() + ", "+ member.getTeam().getName());
    //회원1, 팀A(SQL)
	//회원2, 팀A(1차캐시)  why? 위에서 같은 팀A를 이미 호출해서 캐시에 담겨있음
	//회원3, 팀B(SQL)
}            
tx.commit();


// Member.java
// team에 FetchType.LAZY가 걸려 있으므로
// 이를 호출할 때만 조인이 된다.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;

실행 결과 SQL 

(새로운 팀이 있을때 마다 select 쿼리 생성된다.  100개의 팀이 있을 경우 select문 100번)

 

위와 같은 성능을 최적하 하기 위해 fetch join으로 미리 team을 호출한다.

위의 JpaMain.java 소스 코드에서 아래 부분만 변경 해준다.

// join fetch를 사용하여 team을 SQL에서 미리 가져온다.
String query = "select m from Member m join fetch m.team t";
List<Member> result = em.createQuery(query, Member.class)
                    .getResultList();
for (Member member : result) {
	System.out.println("member = " + member.getUsername() + ", "+ member.getTeam().getName());
}

실행 결과 SQL 

 

페치 조인과 일반 조인의 차이

- 페치 조인을 사용할 때만 연관된 엔티티도 함게 조회(즉시 로딩)

- 페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념

 

페치 조인의 특징과 한계

- 페치 조인 대상에는 별칭을 줄 수 없다.(전부 가져오는 개념이라 별칭을 줄 수 없다.)

- 둘 이상의 컬렉션은 페치 조인 할 수 없다.

- 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.

- 연관된 엔티티들을 SQL 한 번으로 조회 (성능 최적화)

- 실무에서 글로벌 로딩 전략은 모두 지연 로딩

  ex)@ManyToOne(fetch=FetchType.LAZY)   //글로벌 로딩전략

- 최적화가 필요한 곳은 페치 조인 적용

 

JPQL - 엔티티 직접 사용

사용 코드

// 엔티티를 조건에 넣으면 키 값을 비교하기 때문에
// 아래의 query 둘다 같은 결과를 반환한다.

//String query = "select m from Member m where m = :member";
String query = "select m from Member m where m.id = :memberId";

Member findMember = em.createQuery(query, Member.class)
	.setParameter("memberId", member1.getId())
	.getSingleResult();

 

JPQL - Named 쿼리(정적 쿼리)

- 미리 정의해서 이름을 부여해두고 사용하는 JPQL

- 정적쿼리

- 어노테이션, XML에 정의

- 애플리케이션 로딩 시점에 초기화 후 재사용

- 애플리케이션 로딩 시점에 쿼리를 검증(쿼리 문법 오류시 런타임 에러 발생)

 

사용 코드

// Member.java
@Entity
// 정적쿼리인 NamedQuery 생성
@NamedQuery(name = "Member.findByUsername",
            query = "select m from Member m where m.username = :username")           
public class Member {
    @Id
    @GeneratedValue
    private Long id;
    private String username;
    private int age;
}

// JpaMain.java
// createNameQuery에 NamedQuery 네임을 가져온다
List<Member> memberList = em.createNamedQuery("Member.findByUsername", Member.class)
                    .setParameter("username", "회원1")
                    .getResultList();

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

위와 같은 방법으로 사용 하지만, 추후에 spring data jpa를 배우면 더 간단하게 사용 가능하다.

사용 예시

 

JPQL - 벌크 연산

- 쿼리 한 번으로 여러 테이블 로우 변경(엔티티)

- executeUpdate()의 결과는 영향받은 엔티티 수 반환

- UPDATE, DELETE 지원

- INSERT(insert into .. select, 하이버네이트 지원)

 

벌크 연산 주의

- 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리

- 벌크 연산을 먼저 실행(방법1)

- 벌크 연산 수행 후 영속성 컨텍스트 초기화(방법2)

 

사용 코드

// JpaMain.java
//executeUpdate()시 FLUSH가 실행 된다.
int resultCount = em.createQuery("update Member m set m.age = 20")
                    .executeUpdate();

System.out.println("resultCount = " + resultCount);
// DB에서 업데이트된 정보를 가져오는 것이 아니라 영속성 컨텍스트에 있는 기존 age값을 가져온다
Member findMember = em.find(Member.class, member1.getId());
// 결과 값은 update한 20이 아닌, 영속성 컨텍스트에 있는 기존 age를 가져옴
System.out.println("findMember = " + findMember.getAge());

////////////////방법1 사용/////////////////
위의 예제로는 데이터를 넣고 update하는 식으로 벌크연산을 먼저 수행할 수 없다.
벌크연산을 수행하려면 기존에 데이터가 있는 상태여야함

///////////////방법2 사용/////////////////
//FLUSH가 실행 된다.
int resultCount = em.createQuery("update Member m set m.age = 20")
                    .executeUpdate();
// 위에서 벌크연산 후 영속성 컨텍스트를 초기화 해줌으로써
// 아래의 em.find()는 DB에서 데이터를 가져온다.
em.clear();

System.out.println("resultCount = " + resultCount);
// 벌크연산 후 영속성 컨텍스트 초기화로 DB에서 직접 가져온다.
Member findMember = em.find(Member.class, member1.getId());
// 결과 값은 DB에서 가져온 값인 20 출력
System.out.println("findMember = " + findMember.getAge());