자바 ORM 표준 JPA 프로그래밍 - 기본편을 공부하며 정리한 내용입니다.
자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런
JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다. 초급 웹 개발 프로그�
www.inflearn.com
목차
- JPQL 소개
- 프로젝션(SELECT)
- 페이징
- 조인
- 서브쿼리
- JPQL 타입 표현과 기타식
- 조건식(CASE 등등)
- JPQL 함수
🗒 JPQL 소개
- JPQL은 객체지향 쿼리 언어이다. 따라서 테이블을 대상으로 쿼리하는 것이 아니라 엔티티 객체를 대상으로 쿼리한다.
- JPQL은 SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
- JPQL은 결국 SQL로 변환된다.
JPQL 문법
- select m from Member as m where m.age > 18
- 엔티티와 속성은 대소문자 구분o (Member, age)
- JPQL 키워드는 대소문자 구분x (SELECT, FROM, where)
- 엔티티 이름 사용, 테이블 이름 아님!
- 별칭은 필수(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 : 반환 타입이 명확할 때 사용
/* 타입이 명확할 때 */
TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class);
- Query : 반환 타입이 명확하지 않을 때 사용
/* 명확하지 않을 때 */
Query query = em.createQuery("select m.username, m.age from Member m");
String(username), int(age) 두개의 타입이 섞여 있어서 타입 정보를 받아 올 수 없음.
결과 조회 API
- query.getResultList()
- 결과가 하나 이상일 때, 리스트 반환 (결과가 없을 땐 비어있는 리스트 반환)
- query.getSingleResult()
- 결과가 정확히 하나, 단일 객체 반환
- 둘 다 exception 터지니까 정말정말정말 조심해서 사용!
- 결과가 없으면: javax.persistence.NoResultException
- 둘 이상이면: javax.persistence.NonUniqueResultException
파라미터 바인딩 - 이름 기준, 위치 기준
이름 기준
select m from Member m where m.username=:username
query.setParameter("username", usernameParam); // username == usernameParam인 데이터 조회
위치 기준
(만약 중간에 데이터를 추가한다고 가정했을 때, 인덱스가 1씩 밀리기 때문에, 왠만해서는 사용하지 않는것이 좋다.)
select m from Member m where m.username=?1
query.setParameter(1, usernameParam);
메소드 체인 방식처럼 사용
TypeQuery<Member> query = em.createQuery("select m from Member m where m.username=:username", Member.class);
query.setParameter("username", "member1");
Member singleResult = query.getSingleResult();
/* 메소드 체인 방식 사용 */
Member result = em.createQuery("select m from Member m where m.username=:username", Member.class)
.setParameter("username", "member1")
.getSingleResult();
🗒 프로젝션
- 정의 : 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로 중복 제거 */
SELECT로 조회한 모든 member들은 전부 다 영속성 컨텍스트에 관리 된다.
List<Team> resultList = em.createQuery("select t from Member m join m.team t", Team.class)
.getResultList();
join은 명시적으로 표현 해주는 것이 좋다!
프로젝션 - 여러 값 조회
SQL
- SELECT m.username, m.age FROM Member m
List resultList = em.createQuery("select m.age, m.username from Member m")
.getResultList();
Query 타입으로 조회
Object객체를 Object[] 타입 캐스팅해서 사용하기.
Object o = resultList.get(0);
Object[] result = (Object[]) o; // object배열에 [m.age, m.username] 이런 식으로 들어있다.
System.out.println("age = " + result[0]); // age
System.out.println("username = " + result[1]); // username
Object[] 타입으로 조회
제네릭에 Object[] 타입 선언.
List<Object[]> resultList = em.createQuery("select m.age, m.username from Member m")
.getResultList();
Object[] result1 = resultList.get(0);
System.out.println("age = " + result1[0]); // age
System.out.println("username = " + result1[1]); // username
new 명령어로 조회
간단한 DTO 생성해서 DTO타입으로 뽑는 방법
List<MemberDTO> resultList = em.createQuery(
"select new package명.MemberDTO(m.age, m.username) from Member m", MemberDTO.class)
.getResultList(); // 마치 MemberDTO의 생성자를 호출하는 것 처럼!
MemberDTO memberDTO = resultList.get(0);
System.out.println("memberDTO.age = " + memberDTO.getAge()); // age
System.out.println("memberDTO.username = " + memberDTO.getUsername()); // username
SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m
- 패키지 명을 포함한 전체 클래스 명 입력
- 순서와 타입이 일치하는 생성자 필요!
🗒 페이징 API
- JPA는 페이징을 다음 두 API로 추상화
- setFirstResult(int startPosition) : 조회 시작 위치(0부터 시작)
- setMaxResults(int maxResult) : 조회할 데이터 수
// Member 100개 미리 persist
List<Member> resultList = em.createQuery("select m from Member m order by m.age desc", Member.class)
.setFirstResult(0) // 페이징 : 조회 시작 위치(0부터 시작)
.setMaxResults(10) // 페이징 : 조회할 데이터 수(10개)
.getResultList(); // 결과값을 List로
for(Member member : resultList) {
System.out.println("member = " + member); // member.toString() 오버라이딩 했음
}
실행 쿼리
order by .. desc limit ? 인 이유는 setFirstResult(0)으로 줬기 때문이다.
setFirstResult(1)로 주면?
offset이 설정됨.
🗒 조인(JOIN)
내부조인 (inner join)
- inner 생략 가능.
String qlString = "select m from Member m inner join m.team t";
List<Member> resultList = em.createQuery(qlString, Member.class);
.getResultList();
응? team을 조회한건 알겠는데, team을 사용하지 않았는데 왜 쿼리가 나갈까?
이게 바로 @ManyToOne의 fetch 기본 값이 EAGER이기 때문! (LAZY로 바꿔주도록 하자)
외부 조인(outer join)
- outer 생략 가능
String qlString = "select m from Member m left outer join m.team t";
List<Member> resultList = em.createQuery(qlString, Member.class);
.getResultList();
🗒 on 절
조인 대상 필터링
String qlString = "select m from Member m left join m.team t on t.name = 'teamA'";
List<Member> resultList = em.createQuery(qlString, Member.class)
.getResultList();
member.id 와 team.id를 비교해서 and해서 추가로 member.username 과 team.name이 같은 데이터를 가져오는 것이다.
+ where로 조회 결과
String qlString = "select m from Member m join m.team t where t.name = 'teamA'";
List<Member> resultList = em.createQuery(qlString, Member.class)
.getResultList();
연관관계 없는 엔티티 외부 조인
String qlString = "select m from Member m left join Team t on m.username = t.name";
List<Member> resultList = em.createQuery(qlString, Member.class)
.getResultList();
연관관계가 없는 필드(id가 아닌 username이라던지 등등..)를 조인할 때, id값을 가져오지 않고 name끼리만 비교하여 값을 가지고 온다.
+ where로 조회 결과
String qlString = "select m from Member m join m.team t where m.username = t.name";
List<Member> resultList = em.createQuery(qlString, Member.class)
.getResultList();
Member, Team은 연관관계가 있는데 왜 연관관계가 없다고 말할까?
회원과 팀이 연관관계가 있는 건 맞지만, 회원의 이름과 팀 이름은 서로 아무런 연관관계가 없는 필드이다.
이러한 연관관계가 없는 필드로 조인하는 방법을 세타 조인이라고 하고, 예를 들어서 세타 조인을 사용하면
회원과 팀이 아니라, 팀 이름과 회원 이름이 같은 필드로도 조인이 가능하다고 생각하면 된다.
🗒 서브 쿼리
서브 쿼리
- 쿼리 안에 또 다른 쿼리
서브 쿼리 지원 함수
- EXISTS : 서브 쿼리에 결과가 존재하면 참
- ALL, ANY, SOME : ALL은 모두 만족하면 참, ANY, SOME은 조건을 하나라도 만족하면 참
- IN : 서브 쿼리의 결과 중 하나라도 같은 것이 있으면 참
예제 1. 나이가 평균보다 많은 회원
SELECT m FROM Member m WHERE m.age > (SELECT avg(m2.age) FROM Member m2)
서브 쿼리를 보면 기존의 Member m을 사용하지 않고, 새로운 m2를 만들어서 조회 했다. (이렇게 해야 성능이 잘 나오게 된다.)
예제 2. 한 건이라도 주문한 고객
SELECT m FROM Member m WHERE (SELECT count(o) FROM Order o WHERE m = o.member) > 0
메인 쿼리에서 조회한 member와 일치한 Order.member의 주문 건수가 0 보다 클 때 조회 해 온다.
서브 쿼리 사용
SELECT m FROM Member m WHERE exists (SELECT t FROM m.team t WHERE t.name = 'teamA')
exists 함수는 결과가 존재하는 경우 true를, 없는 경우 false를 리턴한다.
위에 코드는 member가 'teamA' 소속인지 확인 하는 sql이다.
SELECT o FROM Order o WHERE o.orderAmount > ALL (SELECT p.stockAmount FROM Product p)
전체 상품 각각의 재고보다 주문량이 많은 주문들을 조회해 오는 sql이다.
SELECT m FROM Member m WHERE m.team = ANY(select t from Team t)
어떤 팀이든 팀에 소속된 회원을 조회해 오는 sql이다.
🗒 JPQL 타입 표현과 기타식
- 문자 : 'HELLO' , 'She''s' (중간에 ' 넣기 위해서)
- 숫자 : 10L(Long), 10D(Double), 10F(Float)
- Boolean : True, False (대소문자 구분 x)
- Enum : 패키지명 포함
- 엔티티 타입 : Type(m) = Member (상속 관계에서 사용)
문자, boolean, Enum 타입
// enum 타입은 항상 FQCN(Fully Qualified Class Name)을 적어줘야 한다.
String sql = "select m.username, 'HELLO', true From Member m " +
"where m.type = ch10.MemberType.ADMIN";
List<Object[]> result = em.createQuery(sql)
.getResultList();
for (Object[] objects : result) {
System.out.println(objects[0]);
System.out.println(objects[1]);
System.out.println(objects[2]);
}
쿼리를 보면, MemberType.ADMIN으로 조회 하고 있다.
DTYPE
String sql = "select i From Item i" +
"where type(i) = Book"; // DTYPE = 'BOOK' 으로 쿼리가 나간다.
🗒 조건식(CASE 등등)
기본 CASE 식 (띄어쓰기 주의)
String query = "select " +
"case when m.age <= 10 then '학생요금'" +
"when m.age >= 60 then '경로요금'" +
"else '일반요금'" +
"end " +
"from Member m";
형태는 java의 switch문이랑 비슷하다.
member의 나이가 10이하이면 "학생요금", 60이상이면 "경로요금", 나머지 나이는 "일반요금"으로 출력된다.
단순 CASE 식
String query = "select " +
"case m.team.name " +
"when 'teamA' then '팀A'" +
"when 'teamB' then '팀B'" +
"else '그 외 팀'" +
"end " +
"from Member m";
기본 CASE문과 살짝 다르다. case문 옆에 값을 지정해 놓고, 그 값이 when 'A' 일때 -> then ... 또 when 'B'일때는 -> then ... 이런 식으로 작성한다.
COALESCE : 하나씩 조회해서 null이 아니면 반환
- 사용자 이름이 null(없으면)이면 이름 없는 회원을 반환
member.setUsername(null);
..
String query = "select coalesce(m.username, '이름 없는 회원') from Member m";
NULLIF : 두 값이 같으면 null 반환, 다르면 첫번째 값 반환
- 사용자 이름이 '관리자'이면 null을 반환, 아니면 자기 자신의 이름 반환
member.setUsername("관리자");
..
String query = "select nullif(m.username, '관리자') from Member m";
🗒 JPQL 기본 함수, 사용자 정의 함수
JPQL 기본 함수
사용자 정의 함수
- 하이버네이트는 사용전에 방언에 추가해야 사용 가능하다.
- 사용하는 DB 방언을 상속받고, 사용자 정의 함수를 등록한다.
등록 후엔 아래와 같이 사용할 수 있다.
select function('group_concat' , i.name) from Item i
다행히도 웬만하면, DB에 이미 다 등록이 되어있긴 하다.
- concat (문자열 더하기)
String query = "select concat('a', 'b') from Member m";
// 실행 결과 : ab
- SUBSTRING (문자열 자르기)
String query = "select substring('abcdef', 2, 3) from Member m";
// 실행 결과 : bcd
- TRIM (공백 제거)
- LTRIM (시작 문자열 공백 제거)
- RTRIM (끝 문자열 공백 제거)
String query = "select trim(' abcdef ') from Member m"; // "abcdef"
String query = "select Ltrim(' abcdef ') from Member m"; // "abcdef "
String query = "select Rtrim(' abcdef ') from Member m"; // " abcdef"
- LOCATE (해당 문자 위치)
- return Integer 타입
String query = "select locate('cd', 'abcdef') from Member m"; // 결과 값 : 3
SIZE (크기)
String query = "select size(t.members) from Team t"; // team에 있는 멤버를 저장하는 컬렉션의 사이즈를 조회
사용자 정의 함수 만들기
1. 패키지 생성 - MyH2Dialect 클래스 생성
지금은 H2를 사용하고 있기 때문에, H2Dialect를 상속 받아서 작성한다.
package dialect;
import org.hibernate.dialect.H2Dialect;
import org.hibernate.dialect.function.StandardSQLFunction;
import org.hibernate.type.StandardBasicTypes;
/**
* H2Dialect 를 상속받아서 내가 사용하고 싶은 함수를 등록 시킬 수 있다.
* <p>
* 어떻게 생성하는지는 H2Dialect 클래스 들어가서 registerFunction 메소드를 보고 참조하면 된다.
*/
public class MyH2Dialect extends H2Dialect {
public MyH2Dialect() {
registerFunction("group_concat", new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
}
}
정의 함수를 만들때 H2Dialect 클래스의 등록된 메소드를 살펴보고 참조하자.
persistence.xml 파일에 기존에 등록되어있던 H2Dialect를 현재 생성한 MyH2Dialect로 바꿔준다.
// <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
<property name="hibernate.dialect" value="dialect.MyH2Dialect"/>
group_concat 은 데이터를 한줄로 만들어주는 함수이다.
String query = "select function('group_concat', m.username) from Member m";
참고로, 하이버네이트 사용중일 때는 아래 방법도 가능하다.
String query = "select group_concat(m.username) from Member m"; // member1, member2