인프런/자바 ORM 표준 JPA 프로그래밍 - 기본편

10) 객체지향 쿼리 언어(JPQL)

backend dev 2024. 6. 4.

JPA는 다양한 쿼리 방법을 지원

 

• JPQL

 

• JPA Criteria

 

• QueryDSL

 

• 네이티브 SQL

 

• JDBC API 직접 사용, MyBatis, SpringJdbcTemplate 함께 사용

 

 


JPQL 소개

 

 

가장 단순한 조회 방법

• EntityManager.find()

• 객체 그래프 탐색(a.getB().getC())

 

JPA를 사용하면 엔티티 객체를 중심으로 개발

 

• 문제는 검색 쿼리

 

검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색

 

• 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능

 

애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함된 SQL이 필요

 

JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어 제공

 

• SQL과 문법 유사, SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 지원

 

JPQL은 엔티티 객체를 대상으로 쿼리

 

• SQL은 데이터베이스 테이블을 대상으로 쿼리

 

 

예시)

List<Member> members = em.createQuery("select m from Member m where m.name like '%kim%'", Member.class)
                    .getResultList();

위의 JPQL로 인해 실제 생성되고 실행된 쿼리

select m from Member m은 select * from Member m 하는 SQL과 살짝 차이가있다

select m from Member m은   m이라는 멤버 엔티티를 가져오라는 뜻.

 

 

• 테이블이 아닌 객체를 대상으로 검색하는 객체 지향 쿼리

 

SQL을 추상화해서 특정 데이터베이스 SQL에 의존X

 

JPQL을 한마디로 정의하면 객체 지향 SQL

 


Criteria 소개

JPQL은 결국 문자열인 쿼리를 만들어줘야하는데

List<Member> members = em.createQuery("select m from Member m where m.name like '%kim%'", Member.class)
        .getResultList();


그러므로 동적쿼리 생성에 어려움이 있다.
[ jdbcTemplate를 사용해서 동적쿼리 짜는것과 같이 ]

 

예시)

//Criteria 사용 준비
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> query = cb.createQuery(Member.class);

//루트 클래스 (조회를 시작할 클래스)
Root<Member> m = query.from(Member.class);

//쿼리 생성
CriteriaQuery<Member> cq = query.select(m).where(cb.equal(m.get("name"), "kim"));
List<Member> resultList = em.createQuery(cq).getResultList();

 

 

문자가 아닌 자바코드로 JPQL을 작성할 수 있음

 

• JPQL 빌더 역할

 

• JPA 공식 기능

 

단점: 너무 복잡하고 실용성이 없다.

 

Criteria 대신에 QueryDSL 사용 권장

 

 


QueryDSL 소개

간단예시 

//JPQL
//select m from Member m where m.age > 18

JPAFactoryQuery query = new JPAQueryFactory(em);
 QMember m = QMember.member;
 
 List<Member> list =
 	query.selectFrom(m)
 		.where(m.age.gt(18))
 		.orderBy(m.name.desc())
 		.fetch();

 

 

• 문자가 아닌 자바코드로 JPQL을 작성할 수 있음

JPQL 빌더 역할

컴파일 시점에 문법 오류를 찾을 수 있음

동적쿼리 작성 편리함

• 단순하고 쉬움

실무 사용 권장

 


 

네이티브 SQL 소개

 

JPA가 제공하는 SQL을 직접 사용하는 기능

 

JPQL로 해결할 수 없는 특정 데이터베이스에 의존적인 기능

 

• 예) 오라클 CONNECT BY, 특정 DB만 사용하는 SQL 힌트

 

 

예시

String sql =
        "SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = ‘kim’";
List<Member> resultList =
        em.createNativeQuery(sql, Member.class).getResultList();

 


 

JDBC 직접 사용, SpringJdbcTemplate 등

 

 JPA를 사용하면서 JDBC 커넥션을 직접 사용하거나, 스프링 JdbcTemplate, 마이바티스등을 함께 사용 가능

 

단 영속성 컨텍스트를 적절한 시점에 강제로 플러시 필요

 

• 예) JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트 수동 플러시

 

jpa 문법 이후 jdbcTEmplate,마이바티스 등을 사용하려면 flush를 해주고 진행해야한다.

jpa로 진행한것들이 적용이 안되있으므로 에러가 발생함.

 

 


 

JPQL(Java Persistence Query Language)

JPQL은 객체지향 쿼리 언어다.

따라서 테이블을 대상으로 쿼리 하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.

 

JPQL은 SQL을 추상화해서 특정데이터베이스 SQL에 의존하지 않는다.

 

JPQL은 결국 SQL로 변환된다.

 

 

@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
@Entity
@Table(name = "ORDERS") // order는 예약어이므로 테이블생성이 안된다. 관례상 테이블명은 orders를 사용한다.
public class Order {
    @Id
    @GeneratedValue
    private Long id;
    private int orderAmount;

    @Embedded
    private Address address;

    @ManyToOne
    @JoinColumn(name = "PRODUCT_IT")
    private Product product;

}
@Getter
@Setter
@Embeddable
public class Address {

    private String city;
    private String street;
    private String zipcode;

}

 

Address는 임베디드 타입 ( 복합 값 타입) 이다. 

 

@Getter
@Setter
@Entity
public class Team {
    @Id
    @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<Member>();

}

 

@Getter
@Setter
@Entity
public class Product {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private int price;
    private int stockAmount;

}

 

임베디드 타입이 잘 들어간것을 확인가능, 객체상 나눠지지만 테이블상은 하나에 있다.

 

 

JPQL 문법

 

 

예시) select m from Member as m where m.age > 18

Member는 엔티티를 의미한다. 테이블이 아니다.

 

• 엔티티와 속성은 대소문자 구분O (ex ) Member, age)

 

• JPQL 키워드는 대소문자 구분X (ex )SELECT, FROM, where)

 

• 엔티티 이름 사용, 테이블 이름이 아님(Member는 테이블명이 아닌 엔티티명, 엔티티명은 기본설정으로는 클래스명 )

 

별칭은 필수(m) (as는 생략가능)

 

 

 

집합과 정렬

select
     COUNT(m), //회원수
     SUM(m.age), //나이 합
     AVG(m.age), //평균 나이
     MAX(m.age), //최대 나이
     MIN(m.age) //최소 나이
from Member m

잘동작한다.

• GROUP BY, HAVING

• ORDER BY

다 사용가능.

 

 

TypeQuery, Query

TypeQuery: 반환 타입이 명확할 때 사용

 

Query: 반환 타입이 명확하지 않을 때 사용

 

 

TypedQuery<Member> query1 =
        em.createQuery("SELECT m FROM Member m", Member.class); // 결과 타입을 확정 할 수 있을 때 사용한다.
TypedQuery<String> query2 =
        em.createQuery("SELECT m.username FROM Member m", String.class); // 결과 타입을 확정 할 수 있을 때 사용한다.

Query query3 =
        em.createQuery("SELECT m.username,m.age FROM Member m"); // 결과 타입을 확정할 수없을때 사용한다. ( name은 String이고, age는 int니까)

 

 

결과 조회 API

TypedQuery<Member> query1 =
        em.createQuery("SELECT m FROM Member m", Member.class); // 결과 타입을 확정 할 수 있을 때 사용한다.

List<Member> resultList = query1.getResultList();
for (Member member1 : resultList) {
    System.out.println("member1 = " + member1);
}

 

• query.getResultList(): 결과가 하나 이상일 때, 리스트 반환
• 결과가 없으면 빈 리스트 반환

 

TypedQuery<Member> query1 =
        em.createQuery("SELECT m FROM Member m where m.id = 1", Member.class); // 결과 타입을 확정 할 수 있을 때 사용한다.

Member member1 = query1.getSingleResult();

• query.getSingleResult(): 결과가 정확히 하나, 단일 객체 반환

• 결과가 없으면: javax.persistence.NoResultException

• 둘 이상이면: javax.persistence.NonUniqueResultException

 

하지만 결과가 없을때 예외가발생하면 예외처리의 귀찮음이 있다.

Spring Data Jpa를 사용하면 null이나 Optional로 반환해준다.

 

 

파라미터 바인딩 - 이름 기준, 위치 기준

TypedQuery<Member> query1 =
        em.createQuery("SELECT m FROM Member m where m.username = :username", Member.class);
query1.setParameter("username", "member1");
Member member1 = query1.getSingleResult();

 

SQL을 짤때 :이름을 이용해서 파라미터로 넣어줄자리를 설정하고

setParameter()를 이용해서 해당 이름에 값을 넣어준다.  [ 이름 기준]

 

 

그리고 체인을 이용하여 깔끔하게 코드를 구성할 수 있다.

Member member1 = em.createQuery("SELECT m FROM Member m where m.username = :username", Member.class)
        .setParameter("username", "member1")
        .getSingleResult();

 

 

Member member1 = em.createQuery("SELECT m FROM Member m where m.username=?1", Member.class)
        .setParameter(1, usernameParam)
        .getSingleResult();

? 를 이용하여 위치기준으로 넣어줄수도있지만

1,2,3있다고 치고 2와 3사이에 값을 추가하려고 하면 다 순서를 바꿔줘야하는 귀찮음이 있어서 사용 X

 

결론 : 이름기준을 쓴다.

 

 

프로젝션

SELECT 절에 조회할 대상을 지정하는 것

 

프로젝션 대상: 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자등 기본 데이터 타입)

 

• SELECT m FROM Member m -> 엔티티 프로젝션

 

• SELECT m.team FROM Member m -> 엔티티 프로젝션

 

• SELECT m.address FROM Member m -> 임베디드 타입 프로젝션

 

• SELECT m.username, m.age FROM Member m -> 스칼라 타입 프로젝션

 

• DISTINCT로 중복 제거

 

 

 

Member member = new Member();
member.setUsername("member1");
em.persist(member);

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

List<Member> result = em.createQuery("SELECT m FROM Member m", Member.class)
        .getResultList();

Member member1 = result.get(0);
member1.setUsername("member2");

tx.commit();

 

JPQL로 가져온 엔티티들도 영속성 컨테이너에서 관리된다.

 

[ 영속성 컨테이너에서 관리되므로 변경감지로인해 SET만해도 UPDATE 쿼리가 생성되어 실행되는것을 확인가능]

 

List<Team> result = em.createQuery("select m.team from Member m", Team.class)
        .getResultList();

이렇게해도

조인한 쿼리가 생성된다.

 

하지만 JPQL 도 실제 SQL과 최대한 비슷하게 작성해야한다.

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

 

이런식으로 작성해야한다. ( join) [ 나중에 또 설명 ]

 

 

프로젝션 - 여러 값 조회

 

SELECT m.username, m.age FROM Member m

다음과 같이 여러값을 조회해야할때

 

 

 

1. Query 타입으로 조회

List resultList = em.createQuery("select m.username,m.age from Member m")
        .getResultList();
Object o = resultList.get(0); // 이런경우 오브젝트 배열이 반환된다.
Object[] result = (Object[]) o;
System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);

 

오브젝트 배열을 반환되고 그 배열안에 각각 값이 들어있다.

 

 

2. Object[] 타입으로 조회

List<Object[]> resultList = em.createQuery("select m.username,m.age from Member m")
        .getResultList();

Object[] result =  resultList.get(0);
System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);

결과 List의 타입을 설정하면된다.

 

결과 : 위와같이 성공 

 

 

3. new 명령어로 조회

 

단순 값을 DTO로 바로 조회

SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m

 

패키지 명을 포함한 전체 클래스 명 입력

• 순서와 타입이 일치하는 생성자 필요

@Getter
@Setter
@AllArgsConstructor // 생성자 필요
public class MemberDTO {
    private String username;
    private int age;
}

 

 

List<MemberDTO> result = em.createQuery("select new com.example.jpa2.MemberDTO(m.username,m.age)  from Member m", MemberDTO.class) // 엔티티가 아닌것을 조회하려고할떄는 생성자를 사용해야한다.
        .getResultList();

MemberDTO m = result.get(0);
System.out.println("username = " + m.getUsername());
System.out.println("age = " + m.getAge());

 

DTO의 패키지위치를 다 적어야한다는 단점이있지만

queryDSL을 사용하면 그 단점을 극복가능하다.

 

 

 

 

 

페이징 API

 

• JPA는 페이징을 다음 두 API로 추상화

setFirstResult(int startPosition)

조회 시작 위치 (0부터 시작)

 setMaxResults(int maxResult)

조회할 데이터 수

 

@Entity
@Getter
@Setter
@ToString(exclude = "team") // Member의 toString 때문에 team을 참조하고 team의 toString 때문에 Member를 참조하여 무한으로 양방향 참조가 발생할수 있으므로 해당 필드를 toSTring에서 제외한다.
public class Member {
    @Id
    @GeneratedValue
    private Long id;
    private String username;
    private int age;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
}

 

 

10명 출력

List<Member> result = em.createQuery("select m from Member m order by m.age asc", Member.class)
        .setFirstResult(0)
        .setMaxResults(10)
        .getResultList();

System.out.println("result.size() = " + result.size());
for (Member member1 : result) {
    System.out.println("member1 = " + member1);
}

 

 

99번째부터 10명

List<Member> result = em.createQuery("select m from Member m order by m.age desc", Member.class)
        .setFirstResult(1)
        .setMaxResults(10)
        .getResultList();

System.out.println("result.size() = " + result.size());
for (Member member1 : result) {
    System.out.println("member1 = " + member1);
}

 

h2 데이터베이스 기준 페이징쿼리

 

SELECT
 M.ID AS ID,
 M.AGE AS AGE,
 M.TEAM_ID AS TEAM_ID,
 M.NAME AS NAME
FROM
 MEMBER M
ORDER BY
 M.NAME DESC LIMIT ?, ?

mysql 기준 페이징쿼리 예시

SELECT * FROM
 ( SELECT ROW_.*, ROWNUM ROWNUM_
 FROM
 ( SELECT
 M.ID AS ID,
 M.AGE AS AGE,
 M.TEAM_ID AS TEAM_ID,
 M.NAME AS NAME
 FROM MEMBER M
 ORDER BY M.NAME
 ) ROW_
 WHERE ROWNUM <= ?
 )
WHERE ROWNUM_ > ?

Oracle 기준 페이징쿼리 예시

 

 


 

조인

 

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

 

내부 조인은 Inner 생략가능 , 외부조인은 outer 생략 가능

 

세타조인은

from 절에 테이블을 여러개 넣으면 

카테시안곱 ( cross 조인 )이 진행된다.

즉  멤버테이블행의수 * 팀테이블 행의수의 결과만큼 결과행이 나온다. [ where가 없다면 ]

 

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

System.out.println("result.size() = " + result.size());
for (Member member1 : result) {
    System.out.println("member1 = " + member1);
}

join 이후 on~ 절을 작성하지않았지만.

id값을 이용해서 쿼리를 생성해주는 모습을 확인가능.

+ inner는 생략이 가능하다.

팀의 내용을 가져오는 두번째 쿼리가 바로 실행되는 이유는

@ManyToOne은 즉시로딩이 기본값이기 때문이다.

@ManyToOne (fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;

지연로딩으로 설정해준다.

 

 

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

System.out.println("result.size() = " + result.size());
for (Member member1 : result) {
    System.out.println("member1 = " + member1);
    System.out.println("member1.getTeam().getClass().getName() = " + member1.getTeam().getClass().getName());
}

쿼리는 한번만 실행되고, 팀에는 프록시객체가 들어간것을 확인가능.

 

 

left outer 조인 테스트

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

 

outer join 쿼리가 생성되지않는 이유 : 

지연로딩이므로, Team의 정보가 필요없으므로 현재는 join을 진행할 필요가없다.

나중에 Team의 값을 사용할때 left outer join을 해서 가져온다.

 

 

세타조인 테스트

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

 

 

 

조인 - ON 절

• ON절을 활용한 조인(JPA 2.1부터 지원)

• 1. 조인 대상 필터링

• 2. 연관관계 없는 엔티티 외부 조인(하이버네이트 5.1부터)

 

 

1. 조인 대상 필터링

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

 

JPQL 버전

"select m,t from Member m left join m.team t on t.name = 'A'"

 

 

SQL 버전

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

 

 AND t.name='A'는 조인에 추가적인 조건을 부여한 부분

즉, Team 테이블의 name이 'A'인 경우에만 조인이 이루어진다.

[outer가 생략된 left outer join 쿼리]

 

 

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

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

 

JPQL버전

"select m,t from Member m left join Team t on m.username=t.name"

Member가 Team과 연관관계가 있어서

필드로 Team을 가지고있다면 이전처럼 left join m.team 으로 join할수있는데

이번에는 연관관계가 없다는 가정이므로 left join Team t 를 해준다.

그리고 연관관계가 없으므로 자동으로 id를 이용하여 연결을 해줄 수 없다.

그래서 on을 이용하여 어떻게 조인할지 정의해줘야한다.

 

SQL버전

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

 

 

서브 쿼리

• 나이가 평균보다 많은 회원

JQPL

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

서브쿼리에서 m을 안쓰고 m2로 쓰는 이유는 메인 쿼리랑 서브쿼리랑 관계가 없게해야 성능이 좋다.

[gpt ->

  • 서브쿼리를 메인 쿼리와 독립적으로 실행하도록 만들면 데이터베이스 엔진이 서브쿼리를 한 번만 계산하고 그 결과를 재사용할 수 있습니다.
  • 반면, 메인 쿼리와 서브쿼리가 서로 연관되어 있으면, 메인 쿼리의 각 행에 대해 서브쿼리가 반복 실행될 수 있습니다. 이렇게 되면 성능이 저하될 수 있습니다.

]

  한 건이라도 주문한 고객

 

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

 

서브 쿼리 지원 함수

 

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

• {ALL | ANY | SOME} (subquery)

• ALL 모두 만족하면 참

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

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

 

 

 

서브 쿼리 - 예제

 

• 팀A 소속인 회원

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

 

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

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)

 

JPA 서브 쿼리 한계

 

• JPA는 WHERE, HAVING 절에서만 서브 쿼리 사용 가능 [ 표준스펙이 그렇다는거고, 기본적으로 구현체를 사용한다.]

 

• SELECT 절도 가능(하이버네이트[==jpa 구현체]에서 지원)

 

  [하이버네이트6 부터는 FROM 절의 서브쿼리를 지원]

 

 

JPQL 타입 표현

 

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

[' 를 추가하고싶다면 '' 처럼 2번사용]

 

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

 

• Boolean: TRUE, FALSE

 

• ENUM:  예시 ) jpabook.MemberType.Admin (패키지명 포함)

[ 해당 필드에 @Enumerated(EnumType.STRING)을 붙여줘야한다.]

[ 패키지 명까지 붙이기 귀찮으니까 일반적으로는 파라미터 바인딩으로 바꿔서 사용한다.]

 

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

ex) 아이템클래스를 상속한 책클래스가 있는 상태에서

select i from Item i where type(i) = Book     

 

JPQL 기타

SQL과 문법이 같은 식

 

• EXISTS, IN

• AND, OR, NOT

• =, >, >=, <, <=, <>

• BETWEEN, LIKE, IS NULL

 

 

 

 

 

 

 

조건식 - CASE 식

 

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

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

 

 

사용자 이름이 없으면 이름 없는 회원을 반환

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

 

사용자 이름이 ‘관리자’면 null을 반환하고 나머지는 본인의 이름을 반환

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

 

 

JPQL 기본 함수

• CONCAT

• SUBSTRING

• TRIM

• LOWER, UPPER

• LENGTH

• LOCATE

• ABS, SQRT, MOD

• SIZE, INDEX(JPA 용도)

 

사용자 정의 함수 호출

 

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

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

 

 

[hibernate 6] custom 함수 등록 방법 공유 - 인프런

Hibernate 6에서는 강의에서 처럼 Dialect를 통한 함수 등록이 불가능합니다.https://start.spring.io/로 Spring Boot 3버전으로 만드신 분들은 문제를 겪으실 거라고 생각합니다.등록법FunctionContributer의 구현체

www.inflearn.com

 

 

댓글