BackEnd/JPA

[JPA] 객체지향 쿼리 언어 2 (기본 문법)

샤아이인 2022. 4. 9.

내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.

 

5. 조인

● 내부 조인

SELECT m FROM Member m [INNER] JOIN m.team t
 

● 외부 조인

SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
 

● 세타 조인

select count(m) from Member m, Team t where m.username = t.name
 

(설명에서 세타 조인의 의미가 내가 알던것과 달라 질문글을 찾아봤는데, 다음과 같이 설명해 주셨다.

=> 세타조인은 동등조인이면서 동시에 sql에서 join구문 없이 사용하는 것으로 이해하시면됩니다.

내 의견을 좀 추가하면 카티션 곱한 결과 릴레이션에다가 =(동등조건) 을 추가해준 것 같다?)

 

(내 의견: 세타 조인은 join을 하되 >, <, =  등 equal조건을 포함하는 더 커다란 집합의 조인 아닌가?? 음... 이건 좀 뭔가 이상하다..)

 

예시 코드를 하나 살펴보자.

Team team = new Team();
team.setName("teamA");
em.persist(team);

Member member = new Member();
member.setAge(10);
member.setUsername("member");
member.setTeam(team);
em.persist(member);

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

String query = "select m from Member m inner join m.team t";
List<Member> result = em.createQuery(query, Member.class)
        .getResultList();

transaction.commit();
 

실행 결과는 다음과 같다.

첫 JOIN문이 나오는것은 이해할수가 있다. 하지만 2번째 select 쿼리는 왜 나온것 일까? 우리는 join한후 조회를 한적이 없다!

=> Member의 @ManyToOne의 fetch를 lazy로 설정해줘야 한다.

@Entity
public class Member {
    ...
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    ...
}

 

아직 한가지 의문이 더 남아있다!

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

 위 쿼리 에서는 "즉시 로딩"이라 할지라도 이미  "조인한 쿼리"(member 하고 team 을 INNER JOIN 한)가 나오는데, 왜 해당되는 Team에 다시 select 쿼리를 나오는건가요?

정리해 보면,

이미 조인해서 Member 하고 Team에 연관관계된 모든 데이터가 1차캐시에 저장되어서 Team 관련된 select 쿼리는 안오는게 아닌가?

 

영한님 답변 : 

JPA는 em.find() 같은 단건 조회는 즉시 로딩으로 되어 있으면 최적화를 해서 JOIN도 하고, select 절에 team도 추가해서 SQL 한번에 조회 하지만, JPQL을 실행하는 경우는 다릅니다. JPQL은 SQL로 그대로 번역됩니다.

String query = "select m from Member m inner join m.team t";
이 JPQL을 실행한 결과를 잘 보시면, 조인한 쿼리라 할지라도 SQL의 select 절을 보시면, member 와 관련된 데이터만 DB에서 조회합니다! (SQL에서 TEAM에 대한 데이터를 조회하지 않습니다.)

그래서 최초 JPQL 실행후에 team에 대한 데이터는 없는 것이지요. 이런 문제를 해결하려면 fetch join을 학습해야 합니다.
 

 

● 조인 ON절 활용하기

1. 조인 대상 필터링

예를 들어 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인 한다고 해보자.

우선 팀 전체에서 팀A로 줄인 후 에 Member와 join 시켜보자!

JPQL: SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A'

SQL: 
SELECT m.*, t.* 
FROM Member m LEFT JOIN Team t 
ON m.TEAM_ID = t.id and t.name ='A'
 

2. 연관관계 없는 엔티티 외부 조인

예를 들어 회원의 이름과 팀의 이름이 같은 대상 외부조인하는 경우

JPQL: SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name

SQL: 
SELECT m.*, t.* 
FROM Member m LEFT JOIN Team t 
ON m.username = t.username
 

하지만 난 설명만 들었을때 잘 이해가 가지 않았다.

Member에는 team에 대한 참조값이 있고, Team에도 members 라는 참조가 있는데? 연관관계가 있는것 아닌가?

각각 @ManyToOne, @OneToMany 라는 에노테이션도 추가되어 있지 않은가?

 

<답변>

회원과 팀이 연관관계가 있는 것이 맞습니다.

 

그런데 이 연관관계가 있다는 것을 좀 더 자세히 설명하면, 회원은 팀의 참조값을 가지고 있고, 반대로 팀도 회원의 참조값을 가지고 있습니다. 그래서 이 참조값을 통해서 연관관계가 이루어집니다.

 

이렇게 참조값을 가지고 있는 필드로 조인을 하는 것이 정상적인 조인 방법입니다.

예를 들어 다음과 같은 코드에서는 m.team 이라는 Member의 참조값을 사용하니, 정상적인 Join 방법 입니다.

SELECT m FROM Member m [INNER] JOIN m.team t
 

그런데 회원의 이름과 팀의 이름은 서로 아무런 연관관계가 없는 필드입니다.

위 코드를 보면 on절이 다음과 같습니다.

m.username = t.name
 

이렇게 아무런 연관관계가 없는 필드로 조인하는 방법을 세타 조인이라 합니다.

 

6. 서브 쿼리

- 나이가 평균보다 많은 회원 조회

select m from Member m 
where m.age > (select avg(m2.age) from Member m2)
 

일반 SQL과 똑같이 subquery 절의 Member를 m2 라고 별칭을 주며, FROM절 member와는 별도의 메모리에 담아둬야 성능상 이점이 있다.

 

- 한 건 이라도 주문한 고객

select m from Member m
where (select count(o) from Order o where m = o.member) > 0
 

위 코드는 서브쿼리 에서의 m이 From 절에서 별칭을 준 대상이기 때문에 성능상 좋지 못하다.

 

● 서브쿼리 지원 함수

1. [NOT] EXISTS(subquery): 서브쿼리에 결과가 존재하면 참

예를 들어 '팀A' 소속인 회원을 구하는 쿼리는 다음과 같다.

select m from Member m
where exists (select t from m.team t where t.name = ‘팀A')
 

 

2. {ALL | ANY | SOME} (subquery)

=> ALL 모두 만족하면 참

=> ANY, SOME: 같은 의미, 조건을 하나라도 만족하면 참

예를 들어 전체 상품 각각의 재고보다 주문량이 많은 주문들

select o from Order o 
where o.orderAmount > ALL (select p.stockAmount from Product p)

 


어떤 팀이든 팀에 소속된 회원

select m from Member m 
where m.team = ANY (select t from Team t)

 

3. [NOT] IN(subquery): 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참

 

● JPA 서브쿼리의 한계

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

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

- FROM 절의 서브 쿼리는 현재 JPQL에서 불가능 => 조인으로 풀 수 있으면 풀어서 해결한다.

 

 

7. JPQL 타입 표현과 기타식

● 문자: ‘HELLO’, ‘She’’s’

● 숫자: 10L(Long), 10D(Double), 10F(Float)

● Boolean: TRUE, FALSE

● ENUM: jpabook.MemberType.Admin (패키지명 포함)

select m.username, 'HELLO', true from Member m 
where m.type = jpql.MemberType.ADMIN
 

● 엔티티 타입: TYPE(m) = Member (상속 관계에서 사용)

em.createQuery("select i from Item i where type(i) = Book", Item.class);
 

● 기타

SQL과 문법이 같은 식

EXISTS, IN

AND, OR, NOT

=, >, ≥, <, ≤, <>

BETWEEN, LIKE, IS NULL

 

8. 조건식(CASE식)

● 기본 CASE 식

select
    case when m.age <= 10 then '학생요금'
        when m.age >= 60 then '경로요금'
        else '일반요금'
    end
from Member m
 

● 단순 CASE 식

select 
    case t.name
	    when '팀A' then '인센티브 110%'
	    when '팀B' then '인센티브 120%'
	    else '인센티브 105%'
    end
from Team t
 

● COALESCE: 하나씩 조회해서 null이 아니면 반환

select coalesce(m.username, '이름 없는 회원') from Member m;
 

● NULLIF: 두 값이 같으면 null 반환, 다르면 첫번째 값 반환

select NULLIF(m.username, '관리자') from Member ;

 

 

9. JPQL 함수

//- CONCAT
select concat('a','b'); //ab

//- SUBSTRING: firstParam의 값을 secondParam위치부터 thirdParam갯수만큼 잘라서 반환
select substring('abcd', 2, 3) // bc

//- TRIM
select trim(' zbqm gldjfh ')//'zbqm gldjfh'

//- LOWER, UPPER
select LOWER('zbqmgldjfh');//zbqmgldjfh
select UPPER('zbqmgldjfh');//ZBQMGLDJFH

//- LENGTH
select LENGTH('zbqmgldjfh'); // 10

//- LOCATE
select LOCATE('de', 'abcdefg');//4

//- ABS, SQRT, MOD
select ABS(-10);// 10
select SQRT(4);//2
select MOD(4,2);//0

//- SIZE, INDEX(JPA용도)
select SIZE(t.members) from Team t // 0 
 

● 사용자 정의 함수 호출

하이버네이트는 사용전에 방언을 추가해야 한다.

- 사용하는 DB 방언을 상속받고, 사용자 정의 함수를 등록한 다.

public class MyH2Dialect extends H2Dialect {
    public MyH2Dialect() {
        registerFunction("group_concat", new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
    }
}
 

이후 persistence.xml에서 방언을 변경해주면 된다.

<property name="hibernate.dialect" value="dialect.MyH2Dialect"/>

댓글