본문 바로가기
BackEnd/JPA

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

by 개발 Blog 2024. 7. 20.

공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.

 

조인

//내부 조인
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

 

내부 조인 예시코드

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

Member member = new Member();
member.setUsername("member");
member.setAge(10);

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();

tx.commit();

- join 쿼리 이후 select 쿼리가 한번 더 발생했다.

- JPQL은 JPA와 다르게 SQL 그대로 해석이 된다. select에서 가져오는 값은 Member 뿐이다. 

- 따라서 이후에 Team을 조회하기 위해 쿼리가 한번 더 발생하는 것이다.

- 이는 Member에 지연로딩을 설정하면 해결 된다.

 

1:N 관계에서는 항상 지연로딩으로 설정하자.

 

다시 돌려보면

의도한 쿼리만 나가는 것을 확인할 수 있다.

 

외부 조인 예시코드

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

Member member = new Member();
member.setUsername("member");
member.setAge(10);

member.setTeam(team);

em.persist(member);

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

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

tx.commit();

 

세타 조인 예시코드

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

Member member = new Member();
member.setUsername("member");
member.setAge(10);

member.setTeam(team);

em.persist(member);

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

String query = "select m from Member m, Team t where m.username = t.name";
List<Member> result = em.createQuery(query, Member.class).getResultList();

tx.commit();

 

조인 - ON 절

1. 조인 대상 필터링 

ex) 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인

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. 연관관계없는 엔티티 외부 조인(하이버네이트 5.1부터)

ex) 회원의 이름과 팀의 이름이 같은 대상 외부 조인 

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.name

 

서브쿼리

나이가 평균보다 많은 회원

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

- 서브쿼리에서 Member를 새로 만들어서 사용했다. 즉 메인쿼리와 서브쿼리는 전혀 관계가 없다.

 

한 건이라도 주문한 고객 

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

- 서브쿼리에서 메인쿼리의 Member를 사용했다. 이러면 성능이 잘 안 나온다.

 

서브 쿼리 지원 함수

 

팀 A 소속인 회원 ([NOT] EXISTS (subquery): 서브쿼리에 결과가 존재하면 참)

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

 

전체 상품 각각의 재고보다 주문량이 많은 주문들 (ALL 모두 만족하면 참)

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

 

어떤 팀이든 팀에 소속된 회원 (ANY, SOME: 같은 의미, 조건을 하나라도 만족하면 참)

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

 

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

 

JPA 서브 쿼리 한계

- JPA는 WHERE 절과 HAVING 절에서 서브쿼리를 사용할 수 있다.
- 일부 구현체, 예를 들어 하이버네이트 6 이상에서는 SELECT 절뿐만 아니라 FROM 절에서도 서브쿼리를 지원한다.

- JPA 표준 JPQL에서는 FROM 절에서 서브쿼리를 사용할 수 없다.

- FROM 절의 서브쿼리를 사용해야 하는 경우, JPA 표준에서는 조인으로 문제를 해결할 수 있다.

 

 

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");

 

JPQL 기타

- SQL 문법이 같은

- EXISTS, IN

- AND, OR, NOT

- =, >, >=, <, <=, <>

- BETWEEN, LIKE, IS NULL

 

조건식(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

 

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

// 사용자 이름이 없으면 이름 없는 회원을 반환
select coalesce(m.username,'이름 없는 회원') from Member m

 

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

// 사용자 이름이 '관리자'면 null을 반환하고 나머지는 본인의 이름을 반환
select NULLIF(m.username, '관리자') from Member m

 

JPQL 함수

CONCAT

select concat('a', 'b') From Member m //ab

 

SUBSTRING

select substring("관리자2", 2, 3) // -> 리자2

 

TRIM

select TRIM(" 유저 ") // "유저"

 

LOWER, UPPER

select LOWER('small'), UPPER('BIG') //SMALL, big

 

LEGNTH

select LENGTH('asdasdasd') //9

 

LOCATE

select locate('de', 'abcdegf') //4

 

ABS, SORT, MOD

select ABS(-10) // 10 (절대값)
select SQRT(16) //4 (제곱근)
select MOD(4, 2) // 0 (나머지)

 

SIZE, INDEX(JPA 용도, 웬만하면 쓰지 말 것)

select size(t.members) From Team t; //0

 

사용자 정의 함수 호출

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

 

사용자 정의 함수를 만든다.

package custom;

import org.hibernate.boot.model.FunctionContributions;
import org.hibernate.boot.model.FunctionContributor;
import org.hibernate.dialect.function.StandardSQLFunction;
import org.hibernate.type.StandardBasicTypes;

public class CustomFunctionContributor implements FunctionContributor {
    @Override
    public void contributeFunctions(FunctionContributions functionContributions) {
        functionContributions.getFunctionRegistry()
                .register("group_concat", new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
    }
}

META-INF -> services  밑에 파일을 생성했던 사용자 정의 함수 명으로 만든다.

<!--            <property name="hibernate.custom" value="org.hibernate.custom.H2Dialect"/>-->
<property name="hibernate.custom" value="custom.MyH2Dialect"/>

- xml에 MyH2Dialect 속성을 추가한다.

 

이제 사용해 보자.

Team team1 = new Team();
team1.setName("TeamA");
em.persist(team1);

Member member1 = new Member();
member1.setUsername("관리자1");
member1.setTeam(team1);
team1.getMembers().add(member1);
em.persist(member1);

Member member2 = new Member();
member2.setUsername("관리자2");
member2.setTeam(team1);
team1.getMembers().add(member2);
em.persist(member2);

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

String query = "select function('group_concat', m.username) From Member m";
List<String> result = em.createQuery(query, String.class)
        .getResultList();

for (String s : result) {
    System.out.println("s = " + s);
}

tx.commit();

- group_concat 함수는 여러 행의 값을 하나의 문자열로 결합해준다.