JPQL 기본 문법
- JPA
- 2021. 12. 18. 18:40
지금까지 JPA를 학습하면서
어떻게 쿼리를 이용할 수 있는지에 대해 학습했다.
근데 의문이 생겼다. 의문의 정체는 바로 조인이다.
물론 JPA자체에서도 조인을 제공하지 않는것은 아니다. 엔티티로만으로도 조인만을 사용하는 것은 가능하다.
하지만 조건문이 들어간 조인도 가능할까?
가능할 수 도 있겠지만 내가 알기로는 불가능한걸로 알고 있다.
예를들어
다음과 같이 학교와 학생이 존재한다고 가정해보자.
@Entity
public class School {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "school", fetch = FetchType.LAZY)
private List<Student> students = new ArrayList<>();
}
@Entity
public class Student {
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn
private School school;
private String name;
}
JPA의 조인은 대충 @XTOX가 알아서 해줄 것이다.
이게 LAZY로 되어있어서 학생을 출력한다고 해서 학교도 함께 조인해서 데이터를 가져 오지는 않는다.
select
student0_.id as id1_1_0_,
student0_.name as name2_1_0_,
student0_.school_id as school_i3_1_0_
from
Student student0_
where
student0_.id=?
뭐 LAZY가 아니라 eagear를 사용하면 당연히 조인해서 데이터를 가져온다.
저 위 쿼리는 학생을 구했지만, 학교를 구하면 어떨까?
select
school0_.id as id1_0_0_,
school0_.name as name2_0_0_
from
School school0_
where
school0_.id=?
하나만 가져온다.
근데 이것을 eagear로 바꾼다면?
다음과 같은 쿼리가 나타난다.
select
school0_.id as id1_0_0_,
school0_.name as name2_0_0_,
students1_.school_id as school_i3_1_1_,
students1_.id as id1_1_1_,
students1_.id as id1_1_2_,
students1_.name as name2_1_2_,
students1_.school_id as school_i3_1_2_
from
School school0_
left outer join
Student students1_
on school0_.id=students1_.school_id
where
school0_.id=?
근데 쿼리를 살펴보면 나는 학생을 구하고 싶은 생각이 전혀 없다.
깔끔하게 어떤 학교인지 확인하고 싶은데 eagear를 사용하면 이렇게 조인해서 데이터를 가져온다.
이래서 eager보다 LAZY를 사용해야 되는 이유도 그 때문이다.
물론 N+1이라는 심각한 문제도 있지만 재현을 하려고 했지만 어떤 이유인지 확실하지 않지만 실패했다.
그래서 lazy로 바꾸면 조인으로 나가지는 않는다.
근데 학생의 정보도 구해야 한다면? eager로 바꿔야 해야 할까?
결론부터 말하면 절대로 eager로 하면 안된다.
하지만 조인을 사용해야 할려면 대안이 있다.
그것이 바로 JPQL이다.
JPQL을 간단히 표현하면 SQL과 굉장히 유사하다.
대충 이렇게 생겼는데 천천히 알아보면 될것 같다.
TypedQuery<Student> query = em.createQuery("select s from Student s join s.school",
Student.class);
List<Student> resultList = query.getResultList();
새롭게 안 사실은 getResultList()처럼 그 쿼리를 실행시켜야 쿼리문을 나온다는 사실을 알게 되었다.
작성해보니 굉장히 당연한 행위라 생각이 든다.
현재 타입을 자세히 보면 타입이 선언이 되어있다.
이것을 제거해도 같을까?
Query query = em.createQuery("select s from Student s join s.school");
inner join => join으로 작성 가능
left outer join => left join으로 작성 가능
연관관계가 없는 엔티티도 join이 가능하다 이때는 ON을 사용해야한다.
이렇게 작성해도 쿼리는 작성이 된다. 웬만하면 타입쿼리를 사용하겠지만 다음과 같은 경우에 사용이 된다고 한다.
쿼리가 좀 애매해서 조금만 수정하고 다시 설명하지 그 쿼리는 아닌것 같아.
private Long id;
private String server;
private String name;
이렇게 변경했는데 예제니까 생각이 안나 ㅜㅜ
이런 쿼리인 경우
Query query = em.createQuery("select s.name, s.server from School s");
클래스를 뭐라고 해야 할까?
만약 클래스를 지정하게 되면 select쿼리가 나오지 않는다.
그러면 어쩔 수 없이 unclass타입쿼리를 사용해야 된다는 것인데 여기서 약간의 문제가 발생한다.
기능상의 문제는 아니고
List resultList = query.getResultList();
요런식으로 코드가 나오는데
문제는 List가 어떤 값인지 모른다는것이 가장 큰 문제다.
만약에 이름만 출력하고 싶다면?
어떻게 해야 할까?
여기에는 총 3가지 방법으로 이것을 풀어낼 수 있다.
1. Object[]타입으로 캐스팅
for (Object o : resultList) {
Object[] objects = (Object[]) o;
System.out.println(objects[0]);
System.out.println(objects[1]);
}
요런식으로 출력하면 원하는 결과를 얻을 수 있다.
근데 Object를 Objcet[]로 캐스팅한다는게 게인적으로 마음에 들지는 않는다.
2. Object[]타입으로 ..
TypedQuery<Object[]> query = em.createQuery("select s.name, s.server from School s",
Object[].class);
List<Object[]> resultList = query.getResultList();
근데 이것도 별로 좋아 보이지 않는다.
3. 다른 클래스로 ..
TypedQuery<SchoolDto> query = em.createQuery("select new helloJava.SchoolDto(s.name, s.server) from School s",
SchoolDto.class);
List<SchoolDto> resultList = query.getResultList();
for (SchoolDto schoolDto : resultList) {
System.out.println(schoolDto.getName());
System.out.println(schoolDto.getServer());
}
이 방법이 가장 좋을 것 같은데 이것도 별로 인게 패키지가 길어지면 전부 적어야 한다.
근데 갑자기 궁금한게 생겼는데 빌더도 될까?
에이 안되네 컴파일 에러네 무조건 이렇게 해야 한다.
파라미터 바인딩
지금까지 보여준 쿼리들은 전부 string에 담겨져있는 데이터였다. 그래서 동적인 쿼리를 작성할 수 없다.
뭐 이것도 동적 쿼리를 만드는데는 부족하지만
적어도 데이터를 직접 검색하고 싶을 때도 있을 것이다.
방법은 총 2가지로
위치기반과 이름 기반이 존재한다.
뭐 이름 기반이 훨씬더 좋다고 한다 왜 그럴까? 간단히 설명할 예정이니 뭐
List<Student> resultList = em.createQuery("select s from Student s where s.name =:name",
Student.class)
.setParameter("name", "학생1").getResultList();
System.out.println(resultList);
이게 이름 기반이고
List<Student> resultList = em.createQuery("select s from Student s where s.name =?1",
Student.class)
.setParameter(1, "학생1").getResultList();
System.out.println(resultList);
이게 위치 기반이다.
근데 확인해보니 0번을 작성하지 않아도 되는데 1번부터는 아무거나 작성해도 된다 심지어 2번을 먼저 작성해도 된다.
말이 위치 기반이지 그냥 숫자 기반인 것 같다. 하지만 숫자의 가장 큰 치명적인 단점은 기억하기 어렵다는 것이다.
그래서 위치 기반보다는 이름기반을 더 추천하는 것도 그 이유 때문이다.
그렇다면 페이징은 어떻게 해야 할까?
되게 간단하게 작성할 수 있다. 근데 이건 좀 데이터가 많이 필요할 것 같아서 데이터를 엄청나게 추가하고 다시 하자.
List<Student> resultList = em.createQuery("select s from Student s",
Student.class)
.setFirstResult(1)
.setMaxResults(10).getResultList();
요런식으로 추가하는데 문제는 시작값만 있어서 이걸 어떻게 페이지을 해야 할지 고민이다.
결국은 최대 갯수를 계속 더하면서 첫번째 값의 위치를 정해 줘야 할 것 같다.
List<Student> resultList = em.createQuery("select s from Student s",
Student.class)
.setFirstResult(10)
.setMaxResults(10).getResultList();
요런식으로 해야 할것 같다.
h2를 사용하면 다양한 sql를 확인이 가능하다는데 아쉽게도 나는
PostgresPlusDialect
요걸 사용하고 있어서 아쉽게 되었다.
근데 리턴 값이 계속 List로 하고 있는데 단일은 불가능한 걸까?
Student resultList = em.createQuery("select s from Student s",
Student.class).getSingleResult();
요렇게 작성하면 되는데 문제는 무조건 하나일때만 가능하다고 한다.
Spring에서는 이걸 또 추상화해서 익셉션이 나오지 않게 변경했다고 한다.
이밖에도 서브쿼리(물론 from절은 안되지만), 각종 함수들도 된다고 한다.
이 참고로 동적 쿼리를 만들기 위해서는 쿼리 DSL를 사용해야 된다고 하는데 이마저도 불가능한 경우도 종종있다고 한다.
이때는 어쩔 수 없이 네이티브 sql이나 마이바티스 같은것을 사용해야된다고 한다.
하지만 이들은 JPA가 아니기 떄문에 관리를 해주지 않는다.
em.flush();
em.clear();
그래서 요걸 사용하면 된다고 한다.
손톱이 길이서 너무 타자치키 어렵다. 이제 곧 종료 한다. ㅎ;
'JPA' 카테고리의 다른 글
JPA 스터디 2주차 (0) | 2022.01.20 |
---|---|
JPQL 중급 문법 (0) | 2021.12.26 |
값 타입 작성 방법 (0) | 2021.12.12 |
Proxy? (0) | 2021.12.04 |
상속하는 방법? (0) | 2021.11.27 |