김영한 님의 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 보고 작성한 내용입니다.
JPA 를 사용하면 엔티티 객체를 중심으로 개발을 하기 때문에, 검색할 때 테이블이 아닌 엔티티 객체를 대상으로 검색해야 합니다.
그래서 JPA 는 SQL 을 추상화한 JPQL 이라는 객체 지향 쿼리 언어를 지원하는데 SQL을 추상화했기 때문에 특정 데이터베이스 SQL 에 의존하지 않습니다.
또 JPQL 을 작성하면 SQL 로 번역되어 실행되는데 ANSI 표준 SQL 이 지원하는 모든 문법을 지원하며 엔티티 객체를 대상으로 쿼리합니다. ( SQL 은 테이블 대상 )
JPQL 은 문자로 작성해야 하기 때문에 동적 쿼리를 작성하기 어렵고, 쿼리가 실행되는 런타임에 오류가 발생할 수도 있습니다.
Criteria 는 쿼리를 코드로 작성할 수 있어 컴파일 시점에 오류를 파악할 수 있고, JPQL 보다 동적쿼리를 작성하기 편리합니다. 하지만 코드가 복잡하고 가독성이 좋지 않습니다.
Criteria 와 동일하게 JPQL 빌더 역할을 하며, 코드로 작성할 수 있어 컴파일 시점에 오류를 파악할 수 있습니다. 무엇보다 단순하고 쉬우며 동적 쿼리 작성도 편리합니다.
JPA 에서 SQL 을 직접 사용하는 기능인데 createNativeQuery()
를 사용하며, JPQL 로 해결할 수 없는 특정 데이터베이스에 의존적인 기능이 필요할 때 사용합니다.
JPA 를 사용하면서 JDBC API 를 직접 사용하거나, Spring JdbcTemplate, MyBatis 를 함께 사용할 수 있습니다.
이때 SQL 을 실행하기 직전에 영속성 컨텍스트를 수동으로 플러시하는 것처럼 영속성 컨텍스트를 적절한 시점에 강제로 플러시 하는게 필요합니다.
select m from Member m where m.age > 20
select COUNT(m), SUM(m.age), MAX(m.age) from Member m
JPQL 에서 select, from 과 같은 키워드는 대소문자를 구분하지 않지만 엔티티를 표현할 때는 대문자를 사용하고, 속성을 표현할 때는 소문자를 사용합니다.
select 절에 위와 같은 집계 함수 사용이 가능합니다.
from 절에는 엔티티 이름을 사용하며, 별칭은 필수이나 as
는 생략할 수 있습니다. 엔티티 이름이란 클래스명이 아닌 @Entity
에 지정된 이름이며, 기본은 클래스명과 동일합니다.
createQuery()
를 사용했을 때 반환되는 타입은 TypedQuery 와 Query 가 있습니다.
TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class);
TypedQuery<String> str = em.createQuery("select m.name from Member m", String.class);
createQuery()
의 두 번째 파라미터로 응답 클래스에 대한 타입 정보를 넘겨줄 수 있는데 이때 반환되는 것이 TypedQuery 이며 반환 타입이 명확할 때 사용됩니다.
첫 번째 예시는 Member 엔티티를 반환하기 때문에 Member 를 가지고, 두 번째 m.name
은 String 이기 때문에 String 타입을 주었고 제네릭에 String 이 있는 것을 확인할 수 있습니다.
Query query = em.createQuery("select m from Member m");
Query query2 = em.createQuery("select m.name, m.age from Member m");
반대로 Query 는 두 번째 파라미터로 엔티티 정보를 넘겨주지 않았을 때, 반환 타입이 명확하지 않을 때 사용됩니다.
두 번째 예시를 보면 m.name
과 m.age
를 사용했는데 name 은 String 이고, age 는 int 입니다. 두 개의 타입이 다르기 때문에 타입 정보를 넘겨줄 수 없고, 이때 Query 가 반환됩니다.
TypedQuery<Member> query = em.createQuery("select m from Member m",Member.class);
List<Member> members = query.getResultList();
Member resultMember = query.getSingleResult();
getResultList()
는 결과가 하나 이상일 때 사용하며, 리스트를 반환합니다. 결과가 없으면 빈 리스트가 반환됩니다.
getSingleResult()
는 결과가 정확히 하나일 때 사용하며, 단일 객체를 반환합니다. 만약 결과가 없거나 둘 이상이면 예외가 발생합니다.
em.createQuery("select m from Member m where m.name = :username" ,Member.class)
.setParameter("username", "kim");
em.createQuery("select m from Member m where m.username = ?1", Member.class)
.setParameter(1, "kim");
파라미터 바인딩하는 첫 번째 방법은 이름을 기준으로 하는 방법입니다. 파라미터 이름 앞에 :
를 붙여서 나타내며 setParameter()
를 통해 지정할 수 있습니다.
두 번째 방법은 위치를 기준으로 하는 방법인데 ?위치
로 JPQL 을 작성하면 되는데 이 방법은 추천하지 않습니다.
프로젝션이란 select 절에 조회할 대상을 지정하는 것인데 엔티티, 연관관계 엔티티, 임베디드 타입 숫자와 문자 같은 스칼라 타입을 지정할 수 있습니다.
em.createQuery("select m from Member m", Member.class);
em.createQuery("select m.team from Member m", Team.class);
em.createQuery("select t from Member m join m.team t", Team.class);
엔티티 프로젝션의 경우, 조회되는 데이터 모두 영속성 컨텍스트에서 관리됩니다.
m.team
을 조회하는 경우, createQuery()
의 두 번째 파라미터로 Team.class
를 넘겨주어야 합니다. 그렇게 되면 Member 와 Team 이 조인되는 쿼리가 실행됩니다.
참고로 두 번째 예시와 세 번째 예시는 동일한 SQL 이 실행되는데, 두 번쨰의 경우 JOIN 예측이 안되기 때문에 세 번째처럼 사용하는 것이 좋습니다.
em.createQuery("select o.address from Order o", Address.class); // 임베디드
em.createQuery("select a from Address a", Address.class); // 불가능한 예시
em.createQuery("select m.name from Member m", Member.class); // 스칼라
Order 내부에 임베디드 타입을 조회하려면 응답 타입을 임베디드 타입으로 지정해야 합니다. 임베디드 타입은 하나의 테이블에 컬럼이 있는 형태이기 때문에 문제가 없습니다.
임베디드 타입은 특정 테이블에 소속되어 있기 때문에 두 번째 예시처럼 사용할 수 없습니다.
여러 개의 타입을 조회해야 하는 경우가 있는데 이때 3가지 방법이 존재합니다.
List resultList = em.createQuery("select m.username, m.age from Member m")
.getResultList();
Object obj = resultList.get(0);
Object[] result = (Object[]) obj;
System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);
첫 번째는 Query 를 사용하는 방법입니다. 위의 List 안에는 Object
가 들어 있습니다. 조회 결과로 반환되는 것은 두 개이기 때문에 이를 Object[]
로 캐스팅하고, 배열 내부에서 조회 결과를 가져올 수 있습니다.
List<Object[]> resultList = em.createQuery(
"select m.username, m.age from Member m")
.getResultList();
Object[] result = resultList.get(0);
System.out.println("username = " + objects[0]);
System.out.println("age = " + objects[1]);
두 번째는 Object[]
를 사용하는 방법입니다. 반환되는 List 의 제네릭을 Object[]
로 지정하면 캐스팅 작업 없이 바로 Object[]
를 받을 수 있습니다.
List<MemberDTO> dto = em.createQuery(
"SELECT new jpabook.jpql.MemberDTO(m.username, m.age) " +
"FROM Member m", MemberDTO.class)
.getResultList();
엔티티가 아닌 다른 타입으로 조회를 하는 경우 반드시 new
키워드를 사용해서 생성해야 합니다. 생성할 때는 패키지명을 포함한 전체 클래스명을 입력해야 하며, 순서와 타입이 일치하는 생성자가 필수로 필요합니다.
em.createQuery("select m from Member m order by m.age desc", Member.class)
.setFirstResult(10)
.setMaxResults(20)
.getResultRest();
setFirstResult()
로 조회 시작 위치를 지정할 수 있으며, 0 부터 시작합니다.
setMaxResults()
로 조회할 데이터의 수를 지정할 수 있습니다.
위의 함수를 작성하면 JPA 가 각 데이터베이스 방언에 맞게 쿼리를 작성해서 실행하게 됩니다.
JPQL 은 엔티티를 중심으로 동작하기 때문에 객체 스타일로 조인 문법을 작성해야 합니다.
// 내부조인
String jpql = "select m from Member m inner join m.team t";
em.createQuery(jpql, Member.class);
// 외부조인
String jpql = "select m from Member m left outer join m.team t";
em.createQuery(jpql, Member.class);
inner 와 outer 는 생략할 수 있습니다.
세타조인은 전혀 연관관계가 없는 엔티티를 조인하는 것입니다. 아래 예시들은 Member 가 4명, Team 이 2개가 저장되어 있고, 둘의 연관관계는 없는 상태입니다.
String jpql = "select m, t from Member m, Team t";
List resultList = em.createQuery(jpql).getResultList();
System.out.println("resultList.size() = " + resultList.size()); // 결과 : 8
위의 예시에서는 조인 조건이 없으므로 카테시안 곱으로 인한 모든 데이터가 함께 나오게 되기 때문에 8이 출력됩니다.
String jpql = "select m, t from Member m, Team t where m.username = t.name";
List resultList = em.createQuery(jpql).getResultList();
System.out.println("resultList.size() = " + resultList.size()); // 결과 : 2
만약 여기서 조건을 추가해서 실행하면 2가 출력됩니다. 왜냐하면 세타조인은 내부조인만 가능하기 때문입니다. 전체 8개 중에 Member 와 Team 이 동일한 것은 최대 Team 의 데이터 수만큼 가능하기 때문에 결과가 2가 나오게 됩니다.
String jpql = "select m, t from Member m left join m.team t on t.name = 'A'";
JPA 는 조인할 때 조인 대상을 필터링 할 수 있는 ON 절을 지원합니다.
위의 예시는 회원과 팀을 조인할 때 팀의 이름이 A 인 팀만 조인하는 JPQL 입니다.
String jpql = "select m, t from Member m left join m.team t on m.name = t.name";
위의 세타조인에서는 내부조인만 가능했는데 연관관계 없는 엔티티 외부 조인도 가능합니다.
[NOT] EXISTS : 서브쿼리에 결과가 존재하면 참
[NOT] IN : 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참
{ ALL | ANY | SOME }
ALL 은 조건을 모두 만족해야 참
ANY 와 SOME 은 조건을 하나라도 만족하면 참
원래는 select 절, where 절, having 절에서만 서브쿼리를 사용할 수 있었는데 하이버네이트 6 부터는 FROM 절 서브쿼리도 지원합니다.
// JPQL 에 직접 사용
String jpql = "select m from Member m where m.type = jpabook.MemberType.ADMIN";
// 파라미터 바인딩 사용
String jpql = "select m from Member m where m.type = :userType";
em.createQuery(jpql, Member.class)
.setParameter("userType", MemberType.ADMIN);
jpql 에 ENUM 타입을 사용할 수 있는데 JPQL 자체에 작성할 때는 패키지명을 포함해서 적어주어야 합니다. 파라미터 바인딩을 사용해서 ENUM 타입을 사용할 수 있습니다.
String jpql = "select i from Item i where type(i) = Book";
em.createQuery(jpql, Item.class);
이전 예시에서 Item 를 상속 받는 엔티티가 Book, Album, Movie 가 있었는데 그 중에 Book 만 가지고 오고 싶을 때 위처럼 type()
을 사용할 수 있습니다.
Item 에는 자식들을 구분하기 위한 DTYPE 컬럼이 추가되는데 위의 쿼리가 실행되면 where 절에 item.DTYPE = 'BOOK'
이 추가됩니다.
String jpql =
"select " +
"case when m.age < 8 then '어린이' " +
" when m.age < 20 then '학생' " +
" else '성인' " +
"end " +
"from Member m";
List<String> resultList = em.createQuery(jpql, String.class).getResultList();
String jpql =
"select " +
"case t.name " +
" when 'teamA' then '인센티브110%' " +
" when 'teamB' then '인센티브120%' " +
" else '인센티브105%' " +
"end " +
"from Team t";
List<String> resultList = em.createQuery(jpql, String.class).getResultList();
select coalesce(m.username,'이름 없는 회원') from Member m
COALESCE 는 하나씩 조회해서 NULL 이 아니면 반환합니다. 위의 예시는 사용자 이름이 없으면 "이름 없는 회원" 을 반환합니다.
select NULLIF(m.username, '관리자') from Member m
NULLIF 는 두 값이 같으면 null 을 반환하고, 다르면 첫 번째 값을 반환합니다. 위의 예시는 사용자 이름이 "관리자"면 null 을 반환하고, 나머지는 본인의 이름을 반환합니다.
IN
, AND
, OR
, NOT
, BETWEEN
, LIKE
, IS NULL
, CONCAT
, SUBSTRING
, TRIM
, LOWER
, UPPER
, LENGTH
, LOCATE
, ABS
, SQRT
, MOD
, SIZE
, INDEX
와 같은 기본 표현과 함수들도 다 사용할 수 있습니다.
DB 내부에 존재하는 사용자 정의 함수를 사용할 때는 함수 등록이 필요한데 Hibernate 6 버전 부터는 방식이 변경되었다고 한다 ( 참고 )