JPA입문(3) - JPQL

김민지·2022년 10월 25일
0

JPA

목록 보기
15/27

JPQL

  • 엔티티 객체를 대상으로 쿼리하는 객체지향 쿼리
  • SQL을 추상화하기 때문에 DBMS에 의존적이지 않다
  • JPQL은 결국 SQL로 변환된다
  • TypeQuery : 반환타입지정가능 시
    Query : 반환타입지정 불가능 시(명확x시)

Criteria

  • JPQL을 생성하는 빌더클래스(?가뭐지)
  • 문자 대신 프로그래밍 코드로 JPQL을 작성할 수 있음
    문자면 런타임에 오류가 발생하는데 프로그래밍 코드로 작성하게 되면 컴파일시점에 오류를 잡을 수 있음

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

  • 일반조인은 연관관계를 불러오는게 아니라 그냥 단순히 그에 대한 결과도 가져오는것이다

페치조인

select t.*, m.* from Team t inner join Member m on t.team_id = m.team_id;

일반조인

select t.* from Team t inner join Member m on t.team_id = m.team_id;

  • 연관된 엔티티를 함께 조회하느냐 안하느냐의 차이
  • jpql은 결과를 반환할때 연관관계를 고려하지 않는다. 그래서 일반조인의 경우 team을조회했을때 team의 멤버변수인 members까지 조회해오지 않는것이다
  • 그리고 만약 지연로딩을 사용했다면 members에 컬렉션래퍼를 가져올 것이다
  • 즉시로딩을 사용했다면 한번에 t.*, m.*으로 조회하는게 아니라 쿼리를 한번 더 보낸다

패치조인의 특징과 한계

  • 페치조인과 지연로딩을 동시에 사용하면 페치조인이 우선으로 적용된다
  • 그래서 모든 엔티티에 지연로딩을 사용하고 필요할때 fetch 키워드를 사용하여 페치조인을 하자
  • 페치조인은 연관된 엔티티를 쿼리시점에 조회해오므로 지연로딩이 발생하지 않아서 준영속상태가 되어도 객체그래프를 탐색할 수 있다
  • 엔티티의 모습이 아닌 다른 모습으로 결과를 반환해야할때면 dto로 받아야한다
  • 컬렉션 페치조인 페이징은 사용하면 안된다

둘이상을 fetch join하고 싶다면?

select t from Team t join fetch t.members join fetch t.foods

cannot simultaneously fetch multiple bags:

  • 엔티티를 fetch하는건 괜찮은데 저런 컬렉션 둘 이상을 fetch하는게 안돼
select m from Member m join fetch m.team join fetch m.group1

대신 member, team group1이 모두 있는것만 가져오게 됨
만약 team,group1이 없어도 가져오고싶다면..? 어떻게 해야할까?

경로 표현식

. 을 찍어서 객체그래프 탐색하는 것

  • 상태필드
  • 단일 값 연관필드
    ㄴ단일 값 연관필드로 경로탐색을 하면 sql에서 내부조인이 일어난다(묵시적조인)
    select o.member from Order o
  • 컬렉션 값 연관필드

묵시적조인 vs 명시적조인

묵시적 조인: 경로표현식에 의해 묵시적으로 조인이 일어나는 것
명시적 조인: join을 적어주는 조인

주의) 컬렉션 값 연관 경로 탐색

select t.members from Team t -> 성공
select t.members.name from Team t -> 실패

컬렉션에서 경로 탐색을 하려면?

select m.username from Team t join t.members m

서브쿼리

  • where, having에만 사용할 수 있고 select, from절에서는 사용할 수 없다
  1. 나이가 평균보다 많은 회원
select m from Member m where m.age > (select avg(m.age) from Member m
  1. 한건이라도 주문한 고객
select m from Member m where (select count(o) from Order o where o.member = m) > 0
  1. 팀 A소속인 회원
select m from Member m where exists (select t from m.team t where t.name = '팀A'
  1. 전체 상품 각각의 재고보다 주문량이 많은 주문들
select o from Order o where o.orderAmount < ALL (select p.productAmount from Product p)
  1. 어떤팀이든 좋으니 팀에 소속된 회원
select m from Member m where m.team = ANY (select t from Team t)
  1. 20세 이상을 보유한 팀
select t from Team t where t IN (select m.team from Member m where m.age >= 20)
select t from Team t where t IN (select t2 From Team t2 join t2.members m2 where m2.age >= 20)

-> 결과 같게 나오는데 같은 쿼리인게 맞을까?
-> 근데 이게 서브쿼리의 결과는 다른데 in을썼기때문에 같은결과가 나오는거일까요?
-> select m from Member m where m In (서브쿼리)
이런 쿼리면 항상 결과는 중복이 없나요?(id값이 같은 member가 여러건 조회)

enum 사용하기

select m.username from Member m where m.type = jpql.MemberType.ADMIN
  • 파라미터 바인딩
em.createQuery("select m.username from Member m where m.type =:type")
			.setParameter("type", MemberType.ADMIN).getResultMap();

entity타입 사용하기

select p from Person p where Type(p) = Student
@Test
	@Transactional
	public void 상속(){
		//given
		Student student = new Student(12, "학생1");
		Teacher teacher = new Teacher("교감", "김교감");
		em.persist(student);
		em.persist(teacher);
		em.flush();
		em.clear();
		//when
		List<Person> personList = em.createQuery("select p from Person p where Type(p) = Student", Person.class).getResultList();
		//then
		for (Person p: personList) {
			System.out.println(p.getName() + " " + ((Student) p).getAge());
		}
	}

NULL비교

  • = 으로 비교하면 안되고 반드시 IS NULL을 사용해야한다

    컴파일 에러 뜸

컬렉션 비교

@Test
	@Transactional
	public void 출력안되는컬렉션식of(){
		//given
		biSetting();
		Member member1 = new Member();
		member1.setName("mem1");
		em.persist(member1);//member1도 영속화되어있어야 되는듯
		//when
		List<Team> list = em.createQuery("select t from Team t where :member1 member of t.members", Team.class).setParameter("member1", member1).getResultList();//주솟값이 다르니 출력이 안됨
		//then
		for (Team team:list) {
			System.out.println(team.getName());
		}
	}

	@Test
	@Transactional
	public void 출력되는컬렉션식of(){
		//given
		Member member1 = new Member();
		member1.setName("mem1");
		Team team1 = new Team();
		team1.setName("team1");
		team1.addMember(member1);
		em.persist(member1);//member1도 영속화되어있어야 되는듯
		em.persist(team1);
		//when
		List<Team> list = em.createQuery("select t from Team t where :member1 member of t.members", Team.class).setParameter("member1", member1).getResultList();
		//then
		for (Team team:list) {
			System.out.println(team.getName());
		}
	}
  • 주솟값까지 같아야 출력이 된다
  • member of 자체가 키워드이다

스칼라식

  • 문자,숫자,날짜,엔티티 같은 기본적인 타입에 사용하는 식

coalesce

  • m.username이 null이 아닌 경우에 '이름없는회원'을 반환한다
select coalesce(m.username, '이름없는회원') from Member m

NULLIF

  • 두값이 같으면 NULL, 다르면 첫번째인자 반환
  • 사용자 이름이 관리자인경우에만 NULL반환하고 나머지는 본인의 이름을 반환
select nullif(m.username, '관리자') from Member m

상속관계에서의 조회

  • jpql로 부모엔티티를 조회하면 그 자식엔티티도 같이 조회된다
select p from Parent p
  • 단일 테이블 전략: 부모 조회 쿼리 한번만 나간다
  • 조인전략: 부모, 모든자식을 조인해서 쿼리 한번이 나간다

상속 예제

1) item중에 book, movie를 조회해보자

select i from Item i where type(i) in (Book, Movie)

실제 나가는 sql

select i from Item i where i.dtype in ('B','M')

2) 부모타입을 타입캐스팅해서 사용하기

select i from Item where treat(i as Book).author = '김'

사용자 정의 함수

  • 사용자가 직접 정의하는 함수인줄 알았다. 그런데 구글링해보니 거의 직접 정의하는 부분은 나와있지 않았다. 방언에 있는 함수를 쓰는것만으로도 충분해서 그 함수를 등록하는 일만 주로 하고 내가 함수를 직접등록하는일은 별로 없나보다

기타 정리

  • 임베디드 타입은 비교를 지원하지 않는다
  • 왜되는걸까?

엔티티 직접 사용

  • JPQL에서 엔티티 객체를 직접사용하면(ID값 사용안하고 직접사용하면) SQL에서는 해당엔티티의 기본키값을 사용한다
select count(m.id) from Member m //id사용
select count(m) from Member m//직접사용

실제 실행되는 쿼리

select count(m.id) from Member m

엔티티를 파라미터로 받아보자

select m from Member m where m = :member

실행된 sql

select m from Member m where m.id =:member.id
  • 그러니까 member의 id값만 제대로 저장되어있으면 조회가 가능할것이라는 얘기
    (다른값이 비어있어도)

외래키 값

  • 특정 팀에 소속된 회원을 찾아보자
select m from Member m where m.team = :team
  • 실제 sql
select m from Member m where m.team_id = :team_id
  • team을 사용하든 team.id를 사용하든 생성되는 sql은 같다
  • 이때 묵시적 조인이 일어날까? 아니다. member가 team_id를 가지고 있으니까 조인은 일어나지 않는다. 대신 id를 제외한 필드에 참조하면 묵시적조인이 일어날 것이다.

동적쿼리 vs 정적쿼리

  • 동적쿼리: em.createQuery
  • 정적쿼리: Named쿼리

정적쿼리의 장점

  • 애플리케이션 로딩시점에 JPQL 문법을 체크하고 미리 파싱 -> 오류빨리 확인 가능
    사용하는 시점에는 파싱된 결과를 재사용해서 성능상 이점도 있다

정적쿼리를 만들어보자

  1. 어노테이션에 정의
  • 사용이되는 엔티티에 쿼리를 정의하고 em.createQuery에 엔티티명.(쿼리명) 으로 접근하면된다
  1. XML문서에 작성
  • 이게 더 편리

Criteria

  • JPQL을 생성하는 빌더클래스(?가뭐지)
  • 문자 대신 프로그래밍 코드로 JPQL을 작성할 수 있음
    문자면 런타임에 오류가 발생하는데 프로그래밍 코드로 작성하게 되면 컴파일시점에 오류를 잡을 수 있음
  • 문자 기반의 JPQL보다 동적쿼리를 안전하게 생성할 수 있음
  • 결국 CRITERIA는 JPQL생성을 돕는 클래스 모음이다
  • 너무 복잡하고 어렵다

쿼리DSL

https://data-make.tistory.com/728

  • 쿼리dsl에서 사용하는 쿼리타입(Q)는 기본적인 별칭을 가지고있다. 그래서 서브쿼리같은것을 지정할때 메인에서나 서브에서 둘다 같은 별칭을써버릴수 있다. 그러니 그때는 별칭을 직접 지정하면 된다

https://github.com/Youngerjesus/Querydsl/blob/master/docs/basic.md
잘정리되어있어서 참고했다

정리

오늘은 jpql에 대해서 설명
sql과 비슷한데 sql은 테이블을 대상으로 쿼리를 날리는 언어였다면
jpql은 entity(객체)를 대상으로 쿼리를 날리는 객체지향언어야
jpql을 작성하면 jpa가 알아서 해석해서sql을 dbms로 날려준다. 그래서 dbms에 의존적이지 않다

jpql작성을 도와주는 빌더클래스
빌더클래스란 무엇일까?

  • 빌더패턴으로 쿼리를 작성할수있는것

페치조인: 연관된 엔티티들을 함께 한번에 조회해오는 조인
조회해와서 이들모두 영속화시킨다(n+1문제도 해결됨)
연관된 애를 한번에 조회해오니까 n+1문제가 해결되지. n+1문제는 연관된 애까지 한번에 조회를 못해왔으니까 쿼리를 더 날리는 상황이니깐

n+1문제란?
team 2가 있고 member가 몇개 있어 그냥 team 을 select해오는건데 team에 연관된 member도 조회해오느라고
지연로딩이든 즉시로딩이든 select member from m.id = ? 을 team개수만큼 실행시키는거지 그래서 n+1문제.
findall을 하면 이 문제가 발생함 근데 findbyid를 하면 연관된 엔티티들을 모두 조인해서 가져온다

findbyid 와 findall의 차이는?
findAll: em.createQuey(select * from Team); -> jpql사용
findById: em.find(Team.class, ${id}); -> jpql을 사용하지 않음
그니까 n+1라는것은 jpql의 문제인것임

em.find(Team.class, id)와 -> 연관된 엔티티는 조인으로 가져옴 -> jpql아님
em.createQuery(select t from Team t where t.id :=id) -> jpql
의 차이는 바로 디비로 조회하러 가느냐 영속성컨텍스트를 들르느냐의 차이

em.find가 jpql이 아닌 이유
em.find는 영컨에서 식별자로 엔티티를 찾는거임 디비에 쿼리를 날리긴하지만 그 쿼리를 우리가 작성하진 않으니
jpql이 아닌 것이다. (이해가 잘 가진 않지만 그런댄다 메서드를 호출하는 목적을 생각해봐라.)
jpql은 쿼리로 엔티티를 가져오는 것

페치조인의 한계

  • 두개이상의 컬렉션을 fetch해오는게 안돼. 그니까 team이라는 엔티티에 members도 있고 roles뭐 이런것들도 있으면 둘다는 못불러온다는얘기

동적쿼리와 정적쿼리
em.createQuery -> 동적쿼리 -> 보통 jpql그대로 사용하지않고 동적쿼리는 쿼리dsl사용
정적쿼리 -> named쿼리
@Entity
@NamedQuery(
name = "Member.findMemberByName",
query = "select m from Member m where m.name = :username"
)
public class Member extends DateMarkable{

profile
안녕하세요!

0개의 댓글