객체지향 쿼리 언어2 - 중급 문법

twocowsong·2023년 5월 2일
0

김영한_jpa

목록 보기
12/13

경로 표현식

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

select m.username -> 상태 필드
  from Member m
  join m.team t -> 단일 값 연관 필드
  join m.orders o -> 컬렉션 값 연관 필드
 where t.name = '팀A' 

경로 표현식 용어 정리

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

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

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

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

경로 표현식 특징

  • 상태 필드(state field): 경로 탐색의 끝, 탐색X
    select m.username from JPQL_MEMBER m
    m.username 은 더이상 접근연산자(점)을 통해 갈수 없습니다

  • 단일 값 연관 경로: 묵시적 내부 조인(inner join) 발생, 탐색O

List<Team> resultList = em.createQuery("select m.team from JPQL_MEMBER m")
			.getResultList();


객체에서는 접근연산자로 쉽게 접근이 가능하지만, SQL에서는 JPQL_TEAM조인이 발생하여, 묵시적 조인이 발생하게됩니다.

Collection resultList =
		em.createQuery("select t.members from JPQL_TEAM t", Collection.class)
		.getResultList();

for (Object target : resultList) {
	JpqlMember temp = (JpqlMember) target;
	System.out.println(temp.getUsername() + "/" + temp.getAge());
}

JPQL_TEAM에 Member는 컬렉션이기때문에, Collection으로 받아 사용하셔야 합니다.
여기서 중요한건!

t.members가 컬렉션이기 때문에 더이상 접근연산자로 조회가 불가능합니다.

이를 해결하기 위해서는, FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색이 가능합니다.
select m.username from JPQL_TEAM t join t.members m
이렇게 조인을 통한 접근으로 데이터를 조회할수있습니다.

묵시적 조인은 지양하며, 명시적 조인을 사용해야지 추후 쿼리 튜닝할때 도움이됩니다.
묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어렵기 때문에 명시적 조인을 추천합니다.

JPQL - 페치 조인(fetch join)

실무에서 정말정말 중요함!!!
SQL 조인 종류가 아니며, JPQL에서 성능 최적화를 위해 제공하는 기능입니다.
연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능입니다.

ex) 회원을 조회하면서 연관된 팀도 함께 조회(SQL 한 번에)

그림과 같이 회원1,2 - 팀A , 회원3 - 팀B, 회원4 - 팀X 에 포함되어있습니다.
결과론적으로는 아래와같이 데이터를 도출하고싶습니다.

이를 JPQL을 이용하여 데이터를 조회해보겠습니다.

JpqlTeam team = new JpqlTeam();
team.setName("팀1");
em.persist(team);

JpqlTeam team2 = new JpqlTeam();
team2.setName("팀2");
em.persist(team2);

JpqlMember jpqlMember = new JpqlMember();
jpqlMember.setUsername("회원1");
jpqlMember.setTeam(team);
em.persist(jpqlMember);

JpqlMember jpqlMember2 = new JpqlMember();
jpqlMember2.setUsername("회원2");
jpqlMember2.setTeam(team);
em.persist(jpqlMember2);

JpqlMember jpqlMember3 = new JpqlMember();
jpqlMember3.setUsername("회원3");
jpqlMember3.setTeam(team2);
em.persist(jpqlMember3);

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

// 지연로딩 설정으로 JPQL_TEAM정보는 가져오지않음
List<JpqlMember> resultList =
	 em.createQuery("select m from JPQL_MEMBER m", JpqlMember.class)
	.getResultList();

for (Object target : resultList) {
	JpqlMember temp = (JpqlMember) target;
    // 1, 2 지연로딩 설정으로 반복문 돌때 첫번째 팀정보를 가져옮
    // 3, 지연로딩 설정으로 반복문 돌때 두번째 팀정보를 가져옮
    // ....점점 많아질수록 쿼리가 계속 호출 됨...
	System.out.println(temp.getUsername() + "/" + temp.getTeam().getName());
}


1. 처음 em.createQuery를 통해 회원 정보를 조회
2. 반복문에서 첫번째 회원에 팀A에 대한 정보를 가져와야 하기때문에 조회
3. 반복문에서 세번째 회원에 팀B에 대한 정보를 가져와야 하기때문에 조회

이렇게, 팀이 계속 많아질수록 계속적으로 쿼리량이 증가하됩니다.
회원 100명 -> N + 1 만큼 SQL을 호출하게됩니다.
이를 해결하기 위해 join fetch를 사용합니다.

List<JpqlMember> resultList =
em.createQuery("select m from JPQL_MEMBER m join fetch m.team", JpqlMember.class).getResultList();


페치조인을 하게되면 그림과 같이 inner join이 실행됩니다.
이때 더이상 프록시가 아닌 실제 데이터가 담기게 됩니다.
그렇기 때문에 resultList는 모든량에 데이터가 담겨있습니다.
지연로딩으로 설정하여도 페치조인이 우선시됩니다.

컬렉션 페치 조인

List<JpqlTeam> resultList = 
	em.createQuery("select t from JPQL_TEAM t join fetch t.members", JpqlTeam.class)
    .getResultList();

for (JpqlTeam temp : resultList) {
	System.out.println(temp.getName() + " / " + temp.getMembers().size());
}

일대다 조인의 경우에는 아래와같이 데이터가 출력됩니다.

팀1은 2명이 맞긴합니다, 팀1은 2명이 맞긴합니다만...
이렇게 SQL에서 일대다 조인시 데이터가 배로증가되는 문제가있습니다.

팀A에 입장상 회원이2명이기때문에 라인수가 2개가 됩니다.
객치지향과 RDB에 패러다임 특성상 어쩔수없기에..일단 DB에준 데이터를 가져옵니다.

그리고 영속성컨텍스트에는 키가 중복되기때문에 계속해서 중복해서 넣게됩니다.

그러면, 이렇게 2개 Row발생하는 문제점을 해결하기위해 DISTINCT를 사용하게됩니다.

페치 조인과 DISTINCT

SQL의 DISTINCT는 중복된 결과를 제거하는 명령입니다.
JPQL의 DISTINCT 2가지 기능 제공

    1. SQL에 DISTINCT를 추가
    1. 애플리케이션에서 엔티티 중복 제거
List<JpqlTeam> resultList =
	em.createQuery("select distinct t from JPQL_TEAM t join fetch t.members", JpqlTeam.class)
	.getResultList();

Sql에 입장상 데이터가 서로 다르기때문에 안되지만, DISTINCT가 추가로 애플리케이션에서 중복 제거시도하며 같은 식별자를 가진 Team 엔티티 제거합니다.

-> 하이버네이트6 변경 사항
하이버네이트6 부터는 DISTINCT 명령어를 사용하지 않아도 애플리케이션에서 중복 제거가 자동으로 적용됩니다.

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

일반조인 - select t from JPQL_TEAM t join t.members
-> 데이터를 조회할때, JpqlTeam객체에만 값이 있기때문에 members값을 사용시 SQL을 또 호출
-> 단지 SELECT 절에 지정한 엔티티만 조회할 뿐

페치조인 - select t from JPQL_TEAM t join fetch t.members
-> 페치조인은 이미, members값에 데이터가 있기때문에 SQL을 호출하지 않음
-> 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회(즉시 로딩)

페치 조인의 특징과 한계

페치 조인 대상에는 별칭을 줄 수 없습니다.

select t from JPQL_TEAM t join fetch t.members as m
페치조인은 나랑 연관된걸 모두 끌고오겠다는 의미입니다.

select t from JPQL_TEAM t join fetch t.members as m where m.username = ~~~
이렇게 where절에 추가적으로 붙게되면 안됩니다.
왜냐하면, 페치조인은 나랑 연관된걸 모두 끌고오게됩니다.
만약 몇개만 조건식을 붙여 들고오고싶다면, 따로 호출하는게 좋습니다.
팀과 연관된 회원이 5명일때, 만약 한명만 불러오고싶다면 4명이 누락이 된 상태로 불러지게됩니다.
이후 데이터를 이상하게 조작될 가능성이 생기게 됩니다.
이를 해결하기 위해서는, 차라리 멤버를 5명 호출하는 쿼리를 따로 호출하는것입니다.

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

일대다대다 는 조인할수없습니다.
select t from JPQL_TEAM t join fetch t.members as m join fetch t.orders
이처럼, 계속적으로 증가되는 다대다대다로직은 위험성이 있습니다.
현재, 일대다도 DISTINCT로 맞추는상황에 여러 컬렉션 페치조인은 위험합니다.

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

일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능합니다.

페치 조인 - 정리

모든 것을 페치 조인으로 해결할 수 는 없습니다.
여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, 페치 조인 보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적입니다.

엔티티 직접 사용

JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기 본 키 값을 사용합니다.
select count(m.id) from Member m //엔티티의 아이디를 사용
select count(m) from Member m //엔티티를 직접 사용
2개의 SQL문은 아래에 1개에 SQL문으로 똑같이 작동합니다.
select count(m.id) as cnt from Member m

String jpql = “select m from Member m where m = :member”;
List resultList = em.createQuery(jpql).setParameter("member", member).getResultList();

String jpql = “select m from Member m where m.id = :memberId”;
List resultList = em.createQuery(jpql).setParameter("memberId", memberId).getResultList();

2개에 방법 다 똑같은 결과가 도출됩니다.
setParameter로 member를 해도 기본 식별자로 바인딩이 됩니다.

Team team = em.find(Team.class, 1L);

String qlString = “select m from Member m where m.team = :team”;
List resultList = em.createQuery(qlString).setParameter("team", team).getResultList();

String qlString = “select m from Member m where m.team.id = :teamId”;
List resultList = em.createQuery(qlString).setParameter("teamId", teamId).getResultList();

외래키를 사용하실때도, joinColumn으로 설정한 값으로 외래키 설정이 가능합니다.

Named 쿼리

@Entity
@NamedQuery(
 	name = "Member.findByUsername",
 	query="select m from Member m where m.username = :username")
	public class Member {
 	...
}

엔티티 상단 부분에 미리 만들어 놓고 아래처럼 호출이 가능합니다.

List<Member> resultList =
	em.createNamedQuery("Member.findByUsername", Member.class)
    .setParameter("username", "회원1")
    .getResultList();

미리 정의해서 이름을 부여해두고 사용하는 JPQL입니다. 단, 정적 쿼리만 가능합니다.

  • 어노테이션, XML에 정의
  • 애플리케이션 로딩 시점에 초기화 후 재사용
    -> 미리 만들어놓아둔 쿼리를, 로딩시점에 캐시로 가지고있어 호출비용이 없습니다.
  • 애플리케이션 로딩 시점에 쿼리를 검증
    -> 정적 쿼리이기 때문에, 오류시 컴파일에서 잡을수있습니다.
    -> 영환님 왈 - 양심이있으면 적어도 한번은 컴파일 해보고 커밋하겠지

JPQL - 벌크 연산

ex) 변경된 데이터가 100건이라면 100번의 UPDATE SQL 실행

  • 쿼리 한 번으로 여러 테이블 로우 변경(엔티티)
  • executeUpdate()의 결과는 영향받은 엔티티 수 반환
// 벌크연산 전 flush됨
int resultCnt = em.createQuery("update JPQL_MEMBER m set m.age = 20")
	.executeUpdate();
System.out.println(resultCnt); // 수정된 로우 갯수    

벌크 연산 주의

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

  • 벌크 연산을 먼저 실행하기
  • 벌크 연산 수행(flush가 됨) 후 영속성 컨텍스트 초기화
JpqlMember jpqlMember = new JpqlMember();
jpqlMember.setUsername("회원1");
jpqlMember.setAge(0);
jpqlMember.setTeam(team);
em.persist(jpqlMember);

JpqlMember jpqlMember2 = new JpqlMember();
jpqlMember2.setUsername("회원2");
jpqlMember2.setAge(0);
jpqlMember2.setTeam(team);
em.persist(jpqlMember2);

JpqlMember jpqlMember3 = new JpqlMember();
jpqlMember3.setUsername("회원3");
jpqlMember3.setAge(0);
jpqlMember3.setTeam(team2);
em.persist(jpqlMember3);

// flsuh
int resultCnt = em.createQuery("update JPQL_MEMBER m set m.age = 20")
				.executeUpdate();

System.out.println("jpqlMember 1 : " + jpqlMember.getAge());
System.out.println("jpqlMember 2 : " + jpqlMember2.getAge());
System.out.println("jpqlMember 3 : " + jpqlMember3.getAge());

벌크연산으로 UPDATE가 실행되었다고해도, 1차캐시에서 가져온 jpqlMember.getAge(), jpqlMember2.getAge(), jpqlMember3.getAge()에 값들은 모두 0으로 출력됩니다.
flush가 되었다고해서 사라진게 아니게되죠.

잘못하면, 데이터 정확성이 떨어지게됩니다.
이를 해결하기위해 벌크 연산 후 em.clear();를 호출해줍니다.
그 다음 새롭게 데이터를 find를 통해 가져오면 정상적인 데이터를 얻을수 있습니다.

int resultCnt = em.createQuery("update JPQL_MEMBER m set m.age = 20")
				.executeUpdate();
em.clear();
JpqlMember jpqlMember1 = em.find(JpqlMember.class, jpqlMember.getId());
System.out.println(jpqlMember1.getAge());

이렇게 작업하시면 벌크연산 수행후에도 정상적인 데이터를 가져올수 있습니다.

profile
생각하는 개발자

0개의 댓글