DB/JPA

[JPA] JPQL 간단 정리

꾸준함. 2021. 10. 16. 18:12

개요

JPA는 아래와 같이 다양한 쿼리 방법을 지원합니다.

  • JPQL
  • JPA Criteria
  • QueryDSL
  • Native SQL
  • JDBC API 직접 적용

이번 게시글에서는 간단하게 위 쿼리 방법들을 설명한 후 JPQL 기본 문법과 기능에 대해 알아보겠습니다.

 

1. 다양한 쿼리 방법 소개

 

1.1 JPQL

  • JPA가 테이블이 아닌 엔티티 객체를 중심으로 개발하는데, JPQL 역시 엔티티 객체를 대상으로 검색하는 쿼리 방법 (객체지향적인 것이 핵심)
  • JPQL은 SQL을 추상화한 객체 지향 쿼리 언어
    • 실제로 SQL 문법과 유사하여 ANSI 표준 키워드 전부 지원 (SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN)
    • 앞서 언급한 대로 JPQL은 엔티티 객체를 대상으로 쿼리
    • 반면, SQL은 DB 테이블을 대상으로 쿼리
  • 모든 DB 데이터를 객체로 변환해서 검색하는 것은 실질적으로 불가능
    • 따라서, 애플리케이션이 필요한 최소한의 데이터만 불러와야 함

 

1.2 Criteria

  • JPQL로 Dynamic Query 작성하기 어렵기 때문에 등장한 개념
  • 장점
    • 문자가 아닌 자바 코드로 JPQL을 작성하므로 컴파일 시점에서 에러를 잡아줌
  • 단점
    • 아래 예시를 보면 알 수 있다시피 SQL스럽지 않고 복잡하며 실용성이 없음
  • Criteria 대신 QueryDSL을 통해 동적 쿼리 작성하는 것을 추천

 

public static void main(String[] args) {
EntityManagerFactory entityManagerFactory
= Persistence.createEntityManagerFactory("hello");
EntityManager entityManager = entityManagerFactory.createEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
try {
// Criteria 사용 준비
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Member> query = criteriaBuilder.createQuery(Member.class);
Root<Member> member = query.from(Member.class);
CriteriaQuery<Member> criteriaQuery = query.select(member);
String username = "kim";
// 장점: 컴파일 시점에 에러 잡아줌
// 단점: sql 스럽지 않음
if (username != null) {
criteriaQuery = criteriaQuery.where(criteriaBuilder.equal(member.get("username"), "kim"));
}
List<Member> members = entityManager.createQuery(criteriaQuery)
.getResultList();
transaction.commit();
} catch (Exception e) {
transaction.rollback();
e.printStackTrace();
} finally {
entityManager.close();
}
entityManagerFactory.close();
}
view raw .java hosted with ❤ by GitHub

 

 

1.3 QueryDSL

  • Criteria와 마찬가지로 문자가 아닌 자바코드로 JPQL을 작성하여 컴파일 단계에서 에러를 잡을 수 있음
  • 동적 쿼리를 작성하기 쉬운 쿼리 언어
  • 단순하고 난이도가 상대적으로 쉽기 때문에 실무 사용 권장
  • 이후 별도로 공부하고 정리할 예정

 

1.4 Native SQL

  • SQL을 직접 작성해서 사용하는 기능
  • JPQL로 해결할 수 없는 특정 DB에 의존적인 기능
    • ex) ORACLE의 CONNECT BY와 같이 특정 DB만 사용하는 SQL 힌트
  • 컴파일 단계에서 에러를 잡을 수 없으므로 에러에 취약함

 

public static void main(String[] args) {
EntityManagerFactory entityManagerFactory
= Persistence.createEntityManagerFactory("hello");
EntityManager entityManager = entityManagerFactory.createEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
try {
entityManager.createNativeQuery("SELECT MEMBER_ID, USERNAME FROM MEMBER")
.getResultList();
transaction.commit();
} catch (Exception e) {
transaction.rollback();
e.printStackTrace();
} finally {
entityManager.close();
}
entityManagerFactory.close();
}
view raw .java hosted with ❤ by GitHub

 

 

1.5 JDBC API 직접 적용

  • JDBC 커넥션을 직접 사용하거나 스프링 JdbcTemplate, MyBatis 등을 함께 사용
    • 단, 위와 같은 기능은 JPA와 연관이 없으므로 PersistenceContext를 강제로 플러쉬 해야 함
    • 앞서 정리한 게시글 https://jaimemin.tistory.com/1898을 보면 데이터는 persist 메서드가 아닌 commit 메서드를 호출해야 DB에 들어감 (persist를 할 경우 영속성 컨텍스트에서 관리하지만 이는 JPA를 사용할 때만 접근 가능) 
    • 따라서, JPA와 연관 없는 쿼리를 호출할 때는 수동으로 flush를 진행해서 데이터를 실제 DB에 넣어줘야 오류 방지 가능

 

2. JPQL 기본 문법과 기능

 

2.1 JPQL 문법

  • JPQL은 SQL을 추상화한 객체지향 언어이므로 기본적인 문법은 비슷
    • ex) SELECT e FROM Employee AS e WHERE e.age > 30
  • 엔티티와 속성은 위 예시처럼 대소문자 구분
    • 엔티티: Employee
    • 속성: age
  • JPQL 키워드는 대소문자 구분을 하지 않지만 키워드는 대문자로 하는 것이 가독성 측면에서 유리
  • 엔티티 중심으로 검색하므로 테이블명이 아닌 엔티티명을 사용
    • @Entity(name = "XXX")라고 가정하면 From XXX
  • 위 예시처럼 Alias는 필수 (Employee AS e)
    • AS는 생략 가능
  • 집합과 정렬을 위한 COUNT, SUM, AVG, MAX, MIN, GROUP BY, HAVING 그리고 ORDER BY 키워드 모두 지원

 

2.2 TypeQuery vs Query

  • TypeQuery는 반환 타입이 명확할 때 사용
  • Query의 경우 반환 타입이 불명확할 경우 사용

 

public static void main(String[] args) {
EntityManagerFactory entityManagerFactory
= Persistence.createEntityManagerFactory("hello");
EntityManager entityManager = entityManagerFactory.createEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
try {
Member member = new Member();
member.setUsername("member1");
member.setAge(10);
entityManager.persist(member);
// TypeQuery: 반환 타입 명확
TypedQuery<Member> query
= entityManager.createQuery("SELECT m FROM Member m", Member.class);
TypedQuery<String> query2
= entityManager.createQuery("SELECT m.username from Member m", String.class);
// Query: 반환 타입이 명확하지 않을 때
Query query3
= entityManager.createQuery("SELECT m.username, m.age from Member m");
transaction.commit();
} catch (Exception e) {
transaction.rollback();
e.printStackTrace();
} finally {
entityManager.close();
}
entityManagerFactory.close();
}
view raw .java hosted with ❤ by GitHub

 

 

2.3 getResultList() vs getSingleResult()

  • 두 메서드 모두 2,2에서 소개한 TypeQuery 혹은 Query의 메서드
  • 결과가 하나 이상일 때는 리스트 형태로 반환하는 getResultList() 메서드 사용
    • 결과가 없을 경우 빈 리스트 반환하므로 exception 발생 X
  • 결과가 정확히 하나일 경우에는 단일 객체를 반환하는 getSingleResult() 메서드 사용
    • 주의할 점은, 결과가 없을 경우나 결과가 둘 이상일 때 exception이 발생한다는 점
    • 결과가 없을 경우 -> NoResultException 예외 발생
    • 결과가 둘 이상일 경우 -> NonUniqueResultException 발생
    • Hibernate의 경우 위와 같은 NoResultException 예외가 발생할 경우 catch 해서 결과가 없더라도 예외가 발생하지 않도록 처리
  • 안전하게 처리하기 위해서는 getResultList() 메서드를 사용하는 것을 추천

 

2.4 파라미터 바인딩

  • 이름 기준 혹은 위치 기준으로 파라미터를 바인딩해주는 기능
  • 위치는 수시로 바뀔 수 있으므로 확실한 이름 기준으로 바인딩해주는 것을 추천
  • 동적 쿼리를 작성할 때 사용하는 기능

 

 

public static void main(String[] args) {
EntityManagerFactory entityManagerFactory
= Persistence.createEntityManagerFactory("hello");
EntityManager entityManager = entityManagerFactory.createEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
try {
Member member = new Member();
member.setUsername("member1");
member.setAge(10);
entityManager.persist(member);
// 파라미터 바인딩
Member result
= entityManager.createQuery("SELECT m FROM Member m WHERE m.username = :username", Member.class)
.setParameter("username", "member1")
.getSingleResult();
System.out.println("singleResult = " + result);
transaction.commit();
} catch (Exception e) {
transaction.rollback();
e.printStackTrace();
} finally {
entityManager.close();
}
entityManagerFactory.close();
}
view raw .java hosted with ❤ by GitHub

 

 

2.5 프로젝션

  • SELECT 절에 조회할 대상을 지정하는 것을 프로젝션이라고 함
  • 프로젝션은 크게 3가지로 나눌 수 있음
    • 엔티티 프로젝션
    • 임베디드 타입 프로젝션
    • 스칼라 타입 프로젝션

 

2.5.1 엔티티 프로젝션

  • 이름에서도 유추할 수 있다시피 엔티티를 반환받는 SELECT 쿼리문
  • 엔티티 프로젝션 시 조회된 모든 엔티티들은 PersistenceContext로 인해 관리가 됨
  • PersistenceContext에 대한 자세한 설명은 https://jaimemin.tistory.com/1898 참고

 

2.5.2 임베디드 타입 프로젝션

  • 앞서 https://jaimemin.tistory.com/1953에서 정리한 임베디드 값 타입을 조회하는 SELECT 쿼리문
  • 아래 예시처럼 주소를 조회할 때 유용하게 쓰임

 

2.5.3 스칼라 타입 프로젝션

  • 여러 타입의 값을 조회할 때 사용하는 SELECT 쿼리문
  • 타입이 여러 개이므로 Object 배열을 반환받는 방법이 있고
  • 혹은 스칼라 타입 프로젝션을 위한 별도 DTO를 생성하여 new 명령어를 통해 조회하는 방법이 있음
    • Object 배열로 반환받는 방법보다 가독성 좋음
    • 단, 패키지명을 포함한 전체 클래스명을 입력해야 하는 불편함이 존재하며 순서와 타입이 일치하는 생성자가 필요하다는 단점도 있음 => QueryDSL 사용 시 해결 가능


public static void main(String[] args) {
EntityManagerFactory entityManagerFactory
= Persistence.createEntityManagerFactory("hello");
EntityManager entityManager = entityManagerFactory.createEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
try {
Member member = new Member();
member.setUsername("member1");
member.setAge(10);
entityManager.persist(member);
entityManager.flush();
entityManager.clear();
// 엔티티 프로젝션 시 모든 엔티티들이 영속성 컨텍스트로 관리됨
List<Member> members
= entityManager.createQuery("SELECT m FROM Member m", Member.class)
.getResultList();
List<Team> teams = entityManager.createQuery("SELECT m.team FROM Member m")
.getResultList();
// update 되는 것을 확인할 수 있음
Member foundMember = members.get(0);
foundMember.setAge(20);
// 임베디드 타입 프로젝션
List<Address> addresses
= entityManager.createQuery("SELECT o.address FROM Order o", Address.class)
.getResultList();
// 스칼라 타입 프로젝션
List<Object[]> resultList = entityManager.createQuery("SELECT m.username, m.age FROM Member m")
.getResultList();
// 1. Object[] 타입으로 조회
Object[] result = resultList.get(0);
System.out.println("result[0] = " + result[0]);
System.out.println("result[1] = " + result[1]);
// 2. new 명령어로 조회
// 패키지명 길어지면 한계가 있는 것이 문제 -> QueryDSL로 해결 가능
List<MemberDTO> memberDTOs
= entityManager.createQuery("SELECT new com.tistory.jaimemin.jpql.MemberDTO(m.username, m.age) FROM Member m", MemberDTO.class)
.getResultList();
MemberDTO memberDTO = memberDTOs.get(0);
System.out.println("username = " + memberDTO.getUsername());
System.out.println("age = " + memberDTO.getAge());
transaction.commit();
} catch (Exception e) {
transaction.rollback();
e.printStackTrace();
} finally {
entityManager.close();
}
entityManagerFactory.close();
}
@Getter
@Setter
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
}
@Entity
@Getter
@Setter
public class Member {
@Id
@GeneratedValue
private Long id;
private String username;
private int age;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
@Getter
@Setter
public class MemberDTO {
private String username;
private int age;
public MemberDTO(String username, int age) {
this.username = username;
this.age = age;
}
}
@Entity
@Getter
@Setter
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
@Entity
@Getter
@Setter
@Table(name = "ORDERS")
public class Order {
@Id
@GeneratedValue
private Long id;
private int orderAmount;
@Embedded
private Address address;
}
view raw .java hosted with ❤ by GitHub

 

2.6 페이징 API

  • JPA는 페이징을 아래의 두 API로 추상화
    • setFirstResult(int startPosition): 조회 시작 위치
    • setMaxResults(int maxResult): 조회할 데이터 수
  • ORACLE의 페이징 처리에 비해 훨씬 간단함
  • MySQL 혹은 PostgreSQL의 offset, limit과 유사


public static void main(String[] args) {
EntityManagerFactory entityManagerFactory
= Persistence.createEntityManagerFactory("hello");
EntityManager entityManager = entityManagerFactory.createEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
try {
for (int i = 0; i < 100; i++) {
Member member = new Member();
member.setUsername("member" + i);
member.setAge(i);
entityManager.persist(member);
}
entityManager.flush();
entityManager.clear();
/**
* SELECT
* m
* FROM
* Member m
* ORDER BY
* m.age DESC
*
* select
* member0_.id as id1_0_,
* *member0_.age as age2_0_,
* *member0_.TEAM_ID as team_id4_0_,
* *member0_.username as username3_0_
* from
* Member member0_
* order by
* member0_.age DESC limit ?offset ?
*/
List<Member> members
= entityManager.createQuery("SELECT m FROM Member m ORDER BY m.age DESC", Member.class)
.setFirstResult(0)
.setMaxResults(10)
.getResultList();
System.out.println("members.size = " + members.size());
for (Member m : members) {
System.out.println("m.getUsername() = " + m.getUsername());
System.out.println("m.getAge() = " + m.getAge());
System.out.println();
}
transaction.commit();
} catch (Exception e) {
transaction.rollback();
e.printStackTrace();
} finally {
entityManager.close();
}
entityManagerFactory.close();
}
view raw .gist hosted with ❤ by GitHub

 

2.7 Join

  • INNER JOIN, OUTER JOIN, CROSS JOIN 모두 지원
  • JPA 2.1부터 아래의 ON 절을 활용한 JOIN 또한 지원
    • 조인 대상 필터링
    • 연관관계없는 엔티티 OUTER JOIN (Hibernate 5.1 ~)


public static void main(String[] args) {
EntityManagerFactory entityManagerFactory
= Persistence.createEntityManagerFactory("hello");
EntityManager entityManager = entityManagerFactory.createEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
try {
Team team = new Team();
team.setName("team");
entityManager.persist(team);
Member member = new Member();
member.setUsername("member");
member.setAge(10);
member.setTeam(team);
entityManager.persist(member);
entityManager.flush();
entityManager.clear();
String query = "SELECT m FROM Member m INNER JOIN m.team t";
List<Member> members = entityManager.createQuery(query, Member.class)
.getResultList();
String query2 = "SELECT m FROM Member m LEFT OUTER JOIN m.team t";
List<Member> leftMembers = entityManager.createQuery(query2, Member.class)
.getResultList();
// cross join
String query3 = "SELECT m FROM Member m, Team t WHERE m.username = t.name";
List<Member> thetaMembers = entityManager.createQuery(query3, Member.class)
.getResultList();
// 조인 대상 필터링
String query4 = "SELECT m FROM Member m LEFT JOIN m.team t on t.name = 'teamA'";
List<Member> filteringMembers = entityManager.createQuery(query4, Member.class)
.getResultList();
// 연관관계가 없는 엔티티 외부 조인
String query5 = "SELECT m FROM Member m LEFT JOIN Team t on m.username = t.name";
List<Member> notRelatedMembers = entityManager.createQuery(query5, Member.class)
.getResultList();
transaction.commit();
} catch (Exception e) {
transaction.rollback();
e.printStackTrace();
} finally {
entityManager.close();
}
entityManagerFactory.close();
}
view raw .java hosted with ❤ by GitHub

 

2.8 SubQuery

  • JPA 또한 SQL처럼 서브 쿼리를 지원하며 아래의 키워드를 제공
    • [NOT] EXISTS (subquery)
    • {ALL | ANY | SOME} (subquery)
    • [NOT] IN (subquery)
  • 하지만, SQL과 달리 JPQL은 서브 쿼리 한계가 존재
    • JPA는 WHERE, HAVING 절에서만 서브 쿼리를 사용 가능
    • Hibernate를 이용할 경우 SELECT 절에서도 서브 쿼리 사용 가능
    • 하지만, FROM 절의 서브 쿼리는 현재 JPQL에서 사용 불가능
      • Native Query를 사용하거나
      • 쿼리를 두 번 호출하거나
      • 혹은 쿼리를 호출한 뒤 Application 단에서 제어를 하는 방식으로 우회해야 함

 

2.9 JPQL Type

  • 엔티티를 제외하고도 문자, 숫자, Boolean 타입 지원
  • Enum 타입도 지원하지만 이를 위해서는 스칼라 타입 프로젝션처럼 전체 패키지명을 명시해야 하는 불편한 점이 있음
    • 파라미터 바인딩을 통해 위 문제점을 해결 가능
  • 엔티티 타입도 지원하는데 이는 @Discriminator 어노테이션을 통해 명시한 DTYPE을 의미

 

2.10 사용자 정의 함수

  • 각 DB 방언(Dialect)마다 사용하는 함수가 다름
  • 따라서 특정 함수를 사용하기 위해서는 사용하는 DB 방언을 상속받은 뒤, 아래와 같이 사용자 정의 함수를 등록해야 함
  • 아래의 예시는 H2Dialect에 group_concat을 추가한 예시
    • CustomH2Dialect 클래스를 선언하고 H2Dialect를 상속받은 뒤 group_concat 함수를 등록
    • persistence.xml에 CustomH2Dialect 등록
    • 실제로 group_concat 함수를 호출

 

CustomH2Dialect

 

public class CustomH2Dialect extends H2Dialect {

    public CustomH2Dialect() {
        registerFunction("group_concat", new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
    }
}

 

persistence.xml

 

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
             xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
    <persistence-unit name="hello">
        <properties>
            <!-- 필수 속성 -->
            <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
            <property name="javax.persistence.jdbc.user" value="sa"/>
            <property name="javax.persistence.jdbc.password" value=""/>
            <property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test"/>
            <property name="hibernate.dialect" value="com.tistory.jaimemin.dialect.CustomH2Dialect"/>
            <!-- 옵션 -->
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.use_sql_comments" value="true"/>
            <property name="hibernate.hbm2ddl.auto" value="create" />
        </properties>
    </persistence-unit>
</persistence>

 

group_concat 함수 호출하는 에시


public static void main(String[] args) {
EntityManagerFactory entityManagerFactory
= Persistence.createEntityManagerFactory("hello");
EntityManager entityManager = entityManagerFactory.createEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin();
try {
Employee employee = new Employee();
member1.setPosition("사원");
entityManager.persist(employee);
Employee employee2 = new Employee();
member2.setPosition("과장");
entityManager.persist(employee2);
entityManager.flush();
entityManager.clear();
String query = "SELECT group_concat(e.position) FROM Employee e";
List<String> result = entityManager.createQuery(query, String.class)
.getResultList();
// position = 사원, 과장
for (String position : result) {
System.out.println("position = " + poistion);
}
transaction.commit();
} catch (Exception e) {
transaction.rollback();
e.printStackTrace();
} finally {
entityManager.close();
}
entityManagerFactory.close();
}
view raw .java hosted with ❤ by GitHub

 

출처

자바 ORM 표준 JPA 프로그래밍 - 기본편 (김영한 강사님)

반응형

'DB > JPA' 카테고리의 다른 글

[JPA] 준영속(Detached) 상태 엔티티 수정하는 방법  (0) 2023.05.07
[JPA] JPQL 추가 정리  (0) 2021.10.18
[JPA] 값 타입 정리  (0) 2021.09.29
[JPA] 프록시와 연관관계 관리 정리  (0) 2021.09.14
[JPA] @MappedSuperclass  (0) 2021.09.07