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();
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);
}
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 방언을 상속받고, 사용자 정의 함수를 등록한다.
'인프런 > 자바 ORM 표준 JPA 프로그래밍 - 기본편' 카테고리의 다른 글
11] 객체지향 쿼리 언어 - 중급 문법 (0) | 2024.06.04 |
---|---|
9) [ 미완 ]값 타입 [임베디드 타입,값 타입과 불변 객체 (1) | 2024.05.31 |
8) 프록시,즉시 로딩, 지연 로딩,영속성 전이, 고아객체 (0) | 2024.05.31 |
7) 고급 매핑 [ 상속관계 매핑 -조인,단일,각각] , MappedSuperclass (0) | 2024.05.29 |
6) 다양한 연관관계 매핑 (0) | 2024.05.28 |
댓글