본문 바로가기
BackEnd

[JPA] JPQL

by mizuiro 2024. 7. 22.

JPQL

JPQL의 사용 목적

JPA에서 기본적으로 제공하는 메서드를 이용하여 데이터베이스의 연동을 처리할 수 있었다. find(), getReference() 를 이용하여 값을 가져와 수정, 삭제 등을 할 수 있었지만 이것은 하나의 데이터에 검색을 수행하는 것이다

이제는 하나의 데이터가 아니라 여러개의 데이터를 한꺼번에 검색하여 가져올 수 있게 하기 위해 JPQL을 사용한다

JPQL은 SQL과 비슷하지만 테이블을 중심으로 검색하는 SQL과는 다르게 엔터티를 기준으로 1차 캐시에서 검색하는 것을 기본으로 하고 있다

JPQL 의 기초 사용법

  1. JPQL 을 String으로 만들어 준다
String jpql = "SELECT e FROM Employee AS e";
  1. 쿼리를 전송하여 결과값을 저장한다
    1. 쿼리를 전송하는 createQuery는 2개를 매개변수로 받는다
    2. public TypedQuery createQuery(String jpql, Class resultClass);
      1. String jpql : 전송할 jpql 문
      2. Class resultClass : 결과 값을 매핑할 엔터티 클래스 설정
TypedQuery<Employee> query = em.createQuery(jpql, Employee.class);
List<Employee> resultList = query.getResultList();
  1. 리스트로 저장한 검색 결과를 출력
System.out.println("검색된 직원 목록");
        for(Employee result : resultList) {
            System.out.println("---> " + result.toString());
        }

!!!! 중요

이때 쿼리 문이 전달되어서 일행 되는 부분은 getResultList() 부분이다

이때 select 가 실행되므로 알아놓자

또한 JPQL은 형태가 같은 SQL문을 보내는 것이 아니라 다른 SQL문을 보내는 것이라서 기존의 find 방식에 비해 속도가 느리다

결과

Hibernate: 
    select
        employee0_.id as id1_0_,
        employee0_.COMMISSION_PCT as commissi2_0_,
        employee0_.DEPT_NAME as dept_nam3_0_,
        employee0_.MAIL_ID as mail_id4_0_,
        employee0_.name as name5_0_,
        employee0_.salary as salary6_0_,
        employee0_.START_DATE as start_da7_0_,
        employee0_.title as title8_0_ 
    from
        S_EMP employee0_
검색된 직원 목록
---> Employee(id=1, name=직원 1, mailId=anti-corona1, startDate=2024-07-24 16:45:04.155, title=사원, deptName=개발부, salary=12700.0, commissionPct=15.0)
---> Employee(id=2, name=직원 2, mailId=anti-corona2, startDate=2024-07-24 16:45:04.176, title=사원, deptName=개발부, salary=25400.0, commissionPct=15.0)
---> Employee(id=3, name=직원 3, mailId=anti-corona3, startDate=2024-07-24 16:45:04.177, title=사원, deptName=개발부, salary=38100.0, commissionPct=15.0)
---> Employee(id=4, name=직원 4, mailId=anti-corona4, startDate=2024-07-24 16:45:04.178, title=사원, deptName=개발부, salary=50800.0, commissionPct=15.0)
---> Employee(id=5, name=직원 5, mailId=anti-corona5, startDate=2024-07-24 16:45:04.178, title=사원, deptName=개발부, salary=63500.0, commissionPct=15.0)
---> Employee(id=6, name=직원 6, mailId=anti-corona6, startDate=2024-07-24 16:45:04.179, title=사원, deptName=개발부, salary=76200.0, commissionPct=15.0)
---> Employee(id=7, name=직원 7, mailId=anti-corona7, startDate=2024-07-24 16:45:04.18, title=사원, deptName=개발부, salary=88900.0, commissionPct=15.0)
---> Employee(id=8, name=직원 8, mailId=anti-corona8, startDate=2024-07-24 16:45:04.18, title=사원, deptName=개발부, salary=101600.0, commissionPct=15.0)
---> Employee(id=9, name=직원 9, mailId=anti-corona9, startDate=2024-07-24 16:45:04.181, title=사원, deptName=개발부, salary=114300.0, commissionPct=15.0)
---> Employee(id=10, name=직원 10, mailId=anti-corona10, startDate=2024-07-24 16:45:04.182, title=사원, deptName=개발부, salary=127000.0, commissionPct=15.0)

JPQL 이 SQL과 다른 점

  1. 테이블을 from 으로 사용하지 않고 엔터티 클래스를 사용
    1. 단, 엔터티의 이름을 지정하면 그 이름으로 사용해야 한다
    2. 354p 참조
  2. select 절을 생략하고 from 절만 사용할 수 있음
    1. 결과 : from 절에 넣은 엔터티의 구조를 가진 모든 값을 들고 온다
  3. select 절에 모든 값을 가져올때는 엔터티의 alias 이름이나, 엔터티 이름을 넣으면 된다
  4. select 절에 지정한 column의 값만 가져오려면 엔터티에서 지정한 변수 이름을 넣으면 된다
    1. 이때 결과의 column들을 하나의 엔터티 클래스 값으로 지정해서 받을 수 없기에 TypedQuery가 아닌 Query 로 받아야 한다

Query 예시 코드

// JPQL
        String jpql = "SELECT id, name, deptName, salary FROM Employee";

        // JPQL 전송
        Query query = em.createQuery(jpql);
        List<Object[]> resultList = query.getResultList();

        // 검색 결과 처리
        System.out.println("검색된 직원 목록");
        for(Object[] result : resultList) {
            System.out.println("---> " + Arrays.toString(result));
        }

결과

Hibernate: 
    select
        employee0_.id as col_0_0_,
        employee0_.name as col_1_0_,
        employee0_.DEPT_NAME as col_2_0_,
        employee0_.salary as col_3_0_ 
    from
        S_EMP employee0_
검색된 직원 목록
---> [1, 직원 1, 개발부, 12700.0]
---> [2, 직원 2, 개발부, 25400.0]
---> [3, 직원 3, 개발부, 38100.0]
---> [4, 직원 4, 개발부, 50800.0]
---> [5, 직원 5, 개발부, 63500.0]
---> [6, 직원 6, 개발부, 76200.0]
---> [7, 직원 7, 개발부, 88900.0]
---> [8, 직원 8, 개발부, 101600.0]
---> [9, 직원 9, 개발부, 114300.0]
---> [10, 직원 10, 개발부, 127000.0]

검색 결과를 특정할 수 있다 (ex. Employee) : TypedQuery

검색 결과를 특정할 수 없다 : Query

JPQL 사용법

  1. 기본 TypedQuery를 사용하여 엔터티 할당
String jpql = "SELECT e FROM Employee AS e";
TypedQuery<Employee> query = em.createQuery(jpql, Employee.class);
List<Employee> resultList = query.getResultList();
  1. Query를 사용하여 특정 column만 추출
String jpql = "SELECT id, name, deptName, salary FROM Employee";
Query query = em.createQuery(jpql);
List<Object[]> resultList = query.getResultList();
  1. 특정 column을 저장할 새로운 엔터티를 생성하고 그곳에 저장
    1. 새로운 엔터티 : EmployeeSalaryData
    2. New를 사용해서 새로운 엔터티를 지정할 때 패키지 경로가 포함된 전체 경로 지정
String jpql = "SELECT " + "NEW com.rubypaper.biz.domain.EmployeeSalaryData(id, salary, " +
                    "commissionPct) FROM Employee";
TypedQuery<EmployeeSalaryData> query = em.createQuery(jpql, EmployeeSalaryData.class);
List<EmployeeSalaryData> resultList = query.getResultList();
Hibernate: 
    select
        employee0_.id as col_0_0_,
        employee0_.salary as col_1_0_,
        employee0_.COMMISSION_PCT as col_2_0_ 
    from
        S_EMP employee0_
검색된 직원 목록
---> EmployeeSalaryData(id=1, salary=12700.0, commissionPct=15.0)
---> EmployeeSalaryData(id=2, salary=25400.0, commissionPct=15.0)
---> EmployeeSalaryData(id=3, salary=38100.0, commissionPct=15.0)
---> EmployeeSalaryData(id=4, salary=50800.0, commissionPct=15.0)
---> EmployeeSalaryData(id=5, salary=63500.0, commissionPct=15.0)
---> EmployeeSalaryData(id=6, salary=76200.0, commissionPct=15.0)
---> EmployeeSalaryData(id=7, salary=88900.0, commissionPct=15.0)
---> EmployeeSalaryData(id=8, salary=101600.0, commissionPct=15.0)
---> EmployeeSalaryData(id=9, salary=114300.0, commissionPct=15.0)
---> EmployeeSalaryData(id=10, salary=127000.0, commissionPct=15.0)
  1. JPQL 을 find 처럼 사용하기 : 테이블에서 하나의 값만 가져오기
    1. 파리미터에 인덱스 번호지정하여 바인딩
      Hibernate: 
       select
           employee0_.id as col_0_0_,
           employee0_.name as col_1_0_,
           employee0_.title as col_2_0_,
           employee0_.DEPT_NAME as col_3_0_,
           employee0_.salary as col_4_0_ 
       from
           S_EMP employee0_ 
       where
           employee0_.id=? 
           and employee0_.name=?
      1번 직원의 정보
      [1, 직원 1, 사원, 개발부, 12700.0]
      b. 파리미터에 특정 이름으로 설정하여 바인딩
    2. String jpql = "SELECT id, name, title, deptName, salary FROM Employee " + "WHERE id = :employeedId AND name = :employeeName"; Query query = em.createQuery(jpql); query.setParameter("employeedId", 1L); query.setParameter("employeeName", "직원 1");
    3. String jpql = "SELECT id, name, title, deptName, salary FROM Employee " + "WHERE id = ?1 AND name = ?2"; Query query = em.createQuery(jpql); query.setParameter(1, 1L); query.setParameter(2, "직원 1"); Object[] result = (Object[])query.getSingleResult(); System.out.println(result[0] + "번 직원의 정보"); System.out.println(Arrays.toString(result));

결과는 a,b 모두 같다

JPQL vs find()

find() : 데이터의 상세 조회를 처리 할 수 있지만 SQL의 WHERE 절 조건 형식은 할 수 가 없어서 데이터의 모든 값들을 다 검색해야 한다

검색한 데이터(엔터티)를 영속 컨테이터가 관리하는 1차 캐시에 등록하고 관리한다

⇒ 같은 데이터를 검색하면 캐시에 등록된 값을 재사용 한다

요청하는 값이 캐시에 존재하면 영속성 컨테이너는 캐시에서 엔터티 값을 들고와서 반환한다

만약 값이 없다면 db에서 가져오고, db에도 존재하지 않는다면 null 값을 반환한다

JPQL : query로 복잡한 조건을 설정하여 데이터 상세 조회가 가능하다

검색한 데이터가 1차 캐시에 존재하더라도 무조건 DB에 select를 전달하여 값을 가져온다

⇒ em.createQuery()의 파리미터에서 식별자의 값을 특정하지 않고 jpql String 문에서만 구별 가능하기에 영속컨테이너는 요청한 엔터티가 어떤 엔터티인지 알 수 가 없다

그러므로 캐시에 요청하는 엔터티가 존재하는지도 알 수 없다

그러나 find에서 수행하는 영속성 컨테이터의 행동과 같이 jpql에서도 영속성 컨테이너가 수행한다

jqpl이 db에 select문을 전송하여 값을 가져오면 그 값이 캐시에 존재하는 엔터티이면 캐시에 저장을 하고 그렇지 않으면 엔터티를 버린다 (중복 저장 방지)

또한 값이 존재하지 않을 때 getReference() 처럼 exception을 발생시킨다

javax.persistence.NoResultException

String jpql = "SELECT e FROM Employee e WHERE e.id=1L";
        TypedQuery<Employee> query = em.createQuery(jpql, Employee.class);

// 1번 직원 검색
Employee findEmp1 = query.getSingleResult();

// 1번 직원 검색
Employee findEmp2 = query.getSingleResult();

if(findEmp1 == findEmp2) {
    System.out.println("두 객체의 주소는 동일하다.");
}

결과

Hibernate: 
    select
        employee0_.id as id1_0_,
        employee0_.COMMISSION_PCT as commissi2_0_,
        employee0_.DEPT_NAME as dept_nam3_0_,
        employee0_.MAIL_ID as mail_id4_0_,
        employee0_.name as name5_0_,
        employee0_.salary as salary6_0_,
        employee0_.START_DATE as start_da7_0_,
        employee0_.title as title8_0_ 
    from
        S_EMP employee0_ 
    where
        employee0_.id=1
Hibernate: 
    select
        employee0_.id as id1_0_,
        employee0_.COMMISSION_PCT as commissi2_0_,
        employee0_.DEPT_NAME as dept_nam3_0_,
        employee0_.MAIL_ID as mail_id4_0_,
        employee0_.name as name5_0_,
        employee0_.salary as salary6_0_,
        employee0_.START_DATE as start_da7_0_,
        employee0_.title as title8_0_ 
    from
        S_EMP employee0_ 
    where
        employee0_.id=1
두 객체의 주소는 동일하다.

JPQL (Join)

JPQL에서 조인의 방식은 각 테이블에 존재하는 외래키로 조인하는 것이 아니라 엔터티 즉, 객체의 연관 관계를 기반으로 조인을 수행한다

묵시적 조인 (Implicit Join) : JPQL에 join에 대한 언급이 없는데도 영속 컨테이너가 내부적으로join을 처리

명시적 조인 (Explicit Join) : JPQL에 join 관련 구문을 명시하는 것

Join을 사용하지 않고 연관관계 테이블 불러오기

부서와 직원 테이블은 직원 테이블 기준 다대일 양방향 관계를 맺고 있다

// department
@OneToMany(mappedBy = "dept", cascade = CascadeType.PERSIST)
    private List<EmployeeJoin> employeeList = new ArrayList<EmployeeJoin>();

// employee
@ManyToOne
    @JoinColumn(name = "DEPT_ID")
    private Department dept;

    public void setDept(Department department) {
        this.dept = department;
        if (department != null)
            department.getEmployeeList().add(this);
    }
  1. 조건 : 직원 7명과 부서 3개를 데이터 값을 넣는다
  2. 실행 : JPQL로 직원 엔터티 데이터베이스값들으 받아와 출력한다
  3. 코드
String jpql = "SELECT e FROM  EmployeeJoin e ";
        TypedQuery<EmployeeJoin> query = em.createQuery(jpql, EmployeeJoin.class);

        List<EmployeeJoin> resultList = query.getResultList();
        System.out.println("검색된 직원 목록");
        for (EmployeeJoin employee : resultList) {
            System.out.println(employee.getName());
        }    

결과

Hibernate: 
    select
        employeejo0_.id as id1_1_,
        employeejo0_.COMMISSION_PCT as commissi2_1_,
        employeejo0_.DEPT_ID as dept_id9_1_,
        employeejo0_.DEPT_NAME as dept_nam3_1_,
        employeejo0_.MAIL_ID as mail_id4_1_,
        employeejo0_.name as name5_1_,
        employeejo0_.salary as salary6_1_,
        employeejo0_.START_DATE as start_da7_1_,
        employeejo0_.title as title8_1_ 
    from
        S_EMP employeejo0_

Hibernate: 
    select
        department0_.DEPT_ID as dept_id1_0_0_,
        department0_.name as name2_0_0_ 
    from
        S_DEPT department0_ 
    where
        department0_.DEPT_ID=?

Hibernate: 
    select
        department0_.DEPT_ID as dept_id1_0_0_,
        department0_.name as name2_0_0_ 
    from
        S_DEPT department0_ 
    where
        department0_.DEPT_ID=?


검색된 직원 목록
개발직원 1
개발직원 2
개발직원 3
영업직원 1
영업직원 2
영업직원 3
아르바이트
영업부
  1. 결과 분석

실행 결과를 보면 3개의 select 문이 실행되었다

첫번째 select 문은 직원 목록을 가져오기 위해 실행된 쿼리 문이다

두번째와 세번째는 첫번째 쿼리 문에서 가져온 직원들의 정보와 연관된 부서 아이디 값으로 부서 정보를 가져오고 있다

여기서 문제점!!

⇒ 실행 코드는 직원의 정보만 가져오라고 하였는데 연관 관계 매핑으로 인해 자동적으로 부서의 정보들도 쿼리문으로 실행하여 저장하고 있다

⇒ 조인 쿼리로 실행되지 않고 각자 따로따로 실행되고 있기에 성능상 문제가 발생한다

묵시적 조인

조인을 설정하지 않고 JPQL 문에 연관 관계에 있는 테이블의 값을 모두 검색하도록 조건을 걸어 조인이 자동적으로 일어나게 하는 것

String jpql = "SELECT e, e.dept FROM  EmployeeJoin e ";
        TypedQuery<Object[]> query = em.createQuery(jpql, Object[].class);

        List<Object[]> resultList = query.getResultList();
        System.out.println("검색된 직원 목록");
        for (Object[] result : resultList) {
            EmployeeJoin employee = (EmployeeJoin) result[0];
            Department department = (Department) result[1];
            System.out.println(employee.getName() + "의 부서 " + department.getName());
        }

결과

Hibernate: 
    select
        employeejo0_.id as id1_1_0_,
        department1_.DEPT_ID as dept_id1_0_1_,
        employeejo0_.COMMISSION_PCT as commissi2_1_0_,
        employeejo0_.DEPT_ID as dept_id9_1_0_,
        employeejo0_.DEPT_NAME as dept_nam3_1_0_,
        employeejo0_.MAIL_ID as mail_id4_1_0_,
        employeejo0_.name as name5_1_0_,
        employeejo0_.salary as salary6_1_0_,
        employeejo0_.START_DATE as start_da7_1_0_,
        employeejo0_.title as title8_1_0_,
        department1_.name as name2_0_1_ 
    from
        S_EMP employeejo0_ 
    inner join
        S_DEPT department1_ 
            on employeejo0_.DEPT_ID=department1_.DEPT_ID
검색된 직원 목록
개발직원 1의 부서 개발부
개발직원 2의 부서 개발부
개발직원 3의 부서 개발부
영업직원 1의 부서 영업부
영업직원 2의 부서 영업부
영업직원 3의 부서 영업부

명시적 조인

Inner Join

묵시적 조인에서 썼던 JPQL 문에서 명시적으로 INNER JOIN을 표현하면 된다

String jpql = "SELECT e, d FROM  EmployeeJoin e INNER JOIN e.dept d";

Outer Join

두 연관관계 엔터티에서 다른 엔터티의 외래값을 가지고 있지 않는 값들을 함께 검색하고 싶을 때 사용한다 단, inner join 보다는 성능이 떨어진다

String jpql = "SELECT e, d FROM  EmployeeJoin e LEFT OUTER JOIN e.dept d";

결과

Hibernate: 
    select
        employeejo0_.id as id1_1_0_,
        department1_.DEPT_ID as dept_id1_0_1_,
        employeejo0_.COMMISSION_PCT as commissi2_1_0_,
        employeejo0_.DEPT_ID as dept_id9_1_0_,
        employeejo0_.DEPT_NAME as dept_nam3_1_0_,
        employeejo0_.MAIL_ID as mail_id4_1_0_,
        employeejo0_.name as name5_1_0_,
        employeejo0_.salary as salary6_1_0_,
        employeejo0_.START_DATE as start_da7_1_0_,
        employeejo0_.title as title8_1_0_,
        department1_.name as name2_0_1_ 
    from
        S_EMP employeejo0_ 
    left outer join
        S_DEPT department1_ 
            on employeejo0_.DEPT_ID=department1_.DEPT_ID
검색된 직원 목록
개발직원 1의 부서 개발부
개발직원 2의 부서 개발부
개발직원 3의 부서 개발부
영업직원 1의 부서 영업부
영업직원 2의 부서 영업부
영업직원 3의 부서 영업부
아르바이트는 대기중...
영업부는 대기중...

세타 조인

서로 연관관계가 없지만 값은 같고 관계는 없는 column 을 가지고 조인을 하는 방법

String jpql = "SELECT e, d FROM  EmployeeJoin e, Department d "
                        + "WHERE e.name = d.name";

직원의 이름과 부서명은 아무런 관계가 없지만 같은 값을 가진 것을 찾아내기 위해 조인이 일어난다

결과

Hibernate: 
    select
        employeejo0_.id as id1_1_0_,
        department1_.DEPT_ID as dept_id1_0_1_,
        employeejo0_.COMMISSION_PCT as commissi2_1_0_,
        employeejo0_.DEPT_ID as dept_id9_1_0_,
        employeejo0_.DEPT_NAME as dept_nam3_1_0_,
        employeejo0_.MAIL_ID as mail_id4_1_0_,
        employeejo0_.name as name5_1_0_,
        employeejo0_.salary as salary6_1_0_,
        employeejo0_.START_DATE as start_da7_1_0_,
        employeejo0_.title as title8_1_0_,
        department1_.name as name2_0_1_ 
    from
        S_EMP employeejo0_ cross 
    join
        S_DEPT department1_ 
    where
        employeejo0_.name=department1_.name
검색된 직원 목록
영업부의 부서 영업부

Join fetch(조인 페치)

제일 처음 실행한 Join을 사용하지 않고 연관관계 테이블을 불러오는 방법을 실행하였을 때, 직원 테이블만 조회했음에도 부서 테이블을 같이 조회하는 것을 볼 수 있었다

이 방법이 심각하게 성능을 떨어트리게 때문에 이 방법을 사용하는 것보다 조인을 사용하게 하는 것이 더 좋다고 하였다

그래도 만약 직원 테이블만 조회를 하려고 하는 경우가 생긴다면 조인 페치를 사용해 처음부터 조인 쿼리를 이용하게 강제하도록 하여 검색 능력을 높일 수 있다

쿼리문(inner join)

String jpql = "SELECT e FROM  EmployeeJoin e JOIN FETCH e.dept ";

⇒ 일반적인 JOIN 문과 다른 점은 select 조건에서 직원 테이블만 조회하기 때문이다

JOIN 문은 JOIN 하기 위해 직원, 부서 테이블 모두를 조회하는데 join fetch 는 검색 조건에 직원만 있어도 된다

결과

Hibernate: 
    select
        employeejo0_.id as id1_1_0_,
        department1_.DEPT_ID as dept_id1_0_1_,
        employeejo0_.COMMISSION_PCT as commissi2_1_0_,
        employeejo0_.DEPT_ID as dept_id9_1_0_,
        employeejo0_.DEPT_NAME as dept_nam3_1_0_,
        employeejo0_.MAIL_ID as mail_id4_1_0_,
        employeejo0_.name as name5_1_0_,
        employeejo0_.salary as salary6_1_0_,
        employeejo0_.START_DATE as start_da7_1_0_,
        employeejo0_.title as title8_1_0_,
        department1_.name as name2_0_1_ 
    from
        S_EMP employeejo0_ 
    inner join
        S_DEPT department1_ 
            on employeejo0_.DEPT_ID=department1_.DEPT_ID
검색된 직원 목록
개발직원 1
개발직원 2
개발직원 3
영업직원 1
영업직원 2
영업직원 3

outer join을 조건으로 조인을 하고 싶다면 외부 조인을 결합하면 된다

쿼리문(outer join)

String jpql = "SELECT e FROM  EmployeeJoin e LEFT JOIN FETCH e.dept ";

결과

Hibernate: 
    select
        employeejo0_.id as id1_1_0_,
        department1_.DEPT_ID as dept_id1_0_1_,
        employeejo0_.COMMISSION_PCT as commissi2_1_0_,
        employeejo0_.DEPT_ID as dept_id9_1_0_,
        employeejo0_.DEPT_NAME as dept_nam3_1_0_,
        employeejo0_.MAIL_ID as mail_id4_1_0_,
        employeejo0_.name as name5_1_0_,
        employeejo0_.salary as salary6_1_0_,
        employeejo0_.START_DATE as start_da7_1_0_,
        employeejo0_.title as title8_1_0_,
        department1_.name as name2_0_1_ 
    from
        S_EMP employeejo0_ 
    left outer join
        S_DEPT department1_ 
            on employeejo0_.DEPT_ID=department1_.DEPT_ID
검색된 직원 목록
개발직원 1
개발직원 2
개발직원 3
영업직원 1
영업직원 2
영업직원 3
아르바이트
영업부

'BackEnd' 카테고리의 다른 글

[JPA] Spring과 JPA-1  (0) 2024.07.29
[JPA] JPQL 연산자와 함수  (0) 2024.07.25
[JPA] groupping과 subquery  (0) 2024.07.24
[JPA] 연관관계 매핑( 다대다)  (0) 2024.07.18
[JPA] 연관관계 매핑(4장 다대일, 일대다)  (0) 2024.07.14