인프런 - 자바 ORM 표준 JPA 프로그래밍 (김영한님)
객체지향 쿼리 언어 소개
jpa는 다양한 쿼리 방법을 지원
- JPQL
- JPA Criteria
- QueryDSL
- Native SQL
- JDBC API 직접 사용, Mybatis, SpringJdbcTemplate 함께 사용
JPQL 소개
- JPA를 사용하면 엔티티 객체를 중심으로 개발
- 문제는 검색 쿼리
- 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색
- 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능
- 애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함된 SQL이 필요
- WHERE, GROUP BY 등
- JPA는 SQL을 추상화환 JPQL이라는 객체 지향 쿼리 언어를 제공
- SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 지원
- JPQL은 엔티티 객체를 대상으로 쿼리
- SQL은 데이터베이스 테이블을 대상으로 쿼리
JPQL 조회 예제
List<Member> members = entityManager.createQuery(
"select m from Member m where m.username like '%kim%'",
Member.class
).getResultList();
for (Member member : members) {
System.out.println("Username = " + member.getUsername());
}
// 실행 시 아래 쿼리가 나감
Hibernate:
/* select
m
from
Member m
where
m.username like '%kim%' */
select
member0_.MEMBER_ID as MEMBER_I1_2_,
member0_.city as city2_2_,
member0_.street as street3_2_,
member0_.zipcode as zipcode4_2_,
member0_.USERNAME as USERNAME5_2_
from
Member member0_
where
member0_.USERNAME like '%kim%'
Criteria 소개
- JPQL은 단순 String으로 조회
- 동적 쿼리 생성이 매우 어려움
- Criteria는 문자가 아닌 자바코드로 작성하기 때문에 동적 쿼리 사용이 쉬운 편
- 컴파일 시 오류 잡을 수 있음
- 너무 복잡하고 유지보수 하기 어려움
- QueryDSL 사용 권장
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Member> query = builder.createQuery(Member.class);
Root<Member> member = query.from(Member.class);
CriteriaQuery<Member> memberCriteriaQuery = query.select(member).where(builder.equal(member.get("username"), "kim"));
entityManager.createQuery(memberCriteriaQuery).getResultList();
QueryDSL 소개
- 문자가 아닌 자바 코드로 작성
- 컴파일 시 문법 오류를 찾을 수 있음
- 동적 쿼리 사용 편리
- 단순하고 쉬움
- 실무 사용 권장
QMember member = QMember.member;
List<Member> result = queryFactory
.select(member)
.from(member)
.where(member.name.equal("kim"))
.fetch();
Native SQL 소개
- JPA가 제공하는 SQL을 직접 사용하는 기능
- JPQL로 해결할 수 없는 특정 DB에 의존적인 기능
- Oracle CONNECT BY 등
entityManager.createNativeQuery(
"select MEMBER_ID, city from MEMBER"
).getResultList();
JDBC 직접 사용, SpringJdbcTemplate 등
- JPA를 사용하면서 JDBC 커넥션을 직접 사용하거나, SpringJdbcTemplate, MyBatis 등을 함께 사용
- 단, 영속성 컨텍스트를 적절한 시점에 강제로 Flush 필요
- JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트에 수동 플러시 필요
JPQL(Java Persistence Query Language) - 기본 문법과 기능
JQPL 문법
// SELECT
select
from
[where]
[groupby]
[having]
[orderby]
// update
update [where]
// delete
delete [where]
- 엔티티와 속성은 대소문자 구분한다 (Member, age)
- JPQL 키워드는 대소문자 구분하지 않는다 (SELECT, FROM, WHERE ...)
- 엔티티 이름 사용 (테이블 이름 X)
- 별칭은 필수 (m) (as는 생략 가능)
집합과 정렬
select
COUNT
SUM
AVG
MAX
MIN
from
TypedQuery, Query
- TypedQuery : 반환 타입이 명확할 때 사용
- Query : 반환 타입이 명확하지 않을 때 사용
// Member 타입
TypedQuery<Member> typedQuery = entityManager.createQuery(
"select m from Member m", Member.class
).getResultList();
// username은 String, age는 int
Query query = entityManager.createQuery(
"select m.username, m.age from Member m"
);
결과 조회 API
- query.getResultList() : 결과가 하나 이상일 때 리스트 반환
- 결과가 없으면 빈 리스트 반환
- Exception 발생하지 않음
- query.getSingleResult() : 결과가 정확히 하나일 때 단일 객체 반환
- 결과가 없으면 javax.persistence.NoResultException
- 둘 이상이면 javax.persistence.NonUniqueResultException
파라미터 바인딩 - 이름 기준, 위치 기준
- :username
- ?1
Member result = entityManager.createQuery(
"select m from Member m where m.username = :username", Member.class
)
.setParameter("username", "member1")
.getSingleResult();
System.out.println(result.getUsername());
Member result = entityManager.createQuery(
"select m from Member m where m.username = ?1", Member.class
)
.setParameter(1, "member1")
.getSingleResult();
System.out.println(result.getUsername());
// member1
프로젝션(SELECT)
- SELECT 절에 조회할 대상을 지정하는 것
- 대상 : 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등 기본 데이터 타입)
- SELECT m FROM Member m : 엔티티
- 영속성 컨텍스트에 관리됨
- SELECT m.team FROM Member m : 엔티티
- SELECT m.address FROM Member m : 임베디드 타입
@Embeddable
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Address {
private String city;
private String street;
private String zipcode;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Address address = (Address) o;
return Objects.equals(city, address.city) && Objects.equals(street, address.street) && Objects.equals(zipcode, address.zipcode);
}
@Override
public int hashCode() {
return Objects.hash(city, street, zipcode);
}
}
- SELECT m.username, m.age FROM Member m : 스칼라 타입
- DISTINCT로 중복 제거
프로젝션 - 여러 값 조회
- SELECT m.username, m.age FROM Member m
1. Query 타입으로 조회
List results = em.createQuery("select m.username, m.age from Member m", Member.class).getResultList();
Object o = results.get(0);
Object[] result = (Object[]) o;
System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);
2. Object[] 타입으로 조회
List<Obejct[]> results = em.createQuery("select m.username, m.age from Member m", Member.class).getResultList();
Object[] result = results.get(0);
System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);
3. new 명령어로 조회 (DTO 사용)
List<MemberDTO> results = em.createQuery("select new MemberDTO(m.username, m.age) from Member m", MemberDTO.class).getResultList();
MemberDTO memberDTO = results.get(0);
System.out.println("username = " + memberDTO.getUsername());
System.out.println("age = " + memberDTO.getAge());
페이징
- setFirstResult(int startPosition) : 조회 시작 위치 (0부터 시작)
- setMaxResults(int maxResult) : 조회할 데이터 수
for (int i = 0; i < 100; i++) {
Member member = new Member();
member.setUsername("member" + i);
entityManager.persist(member);
}
entityManager.flush();
entityManager.clear();
List<Member> results = entityManager.createQuery("select m from Member m order by m.id desc", Member.class)
.setFirstResult(1)
.setMaxResults(10)
.getResultList();
System.out.println("results.size() = " + results.size());
for(Member members : results) {
System.out.println(members.getId() + " : " + members.getUsername());
}
transaction.commit();
//===========results===========
Hibernate:
/* select
m
from
Member m
order by
m.id desc */ select
member0_.MEMBER_ID as MEMBER_I1_2_,
member0_.city as city2_2_,
member0_.street as street3_2_,
member0_.zipcode as zipcode4_2_,
member0_.USERNAME as USERNAME5_2_
from
Member member0_
order by
member0_.MEMBER_ID desc limit ? offset ?
results.size() = 10
99 : member98
98 : member97
97 : member96
96 : member95
95 : member94
94 : member93
93 : member92
92 : member91
91 : member90
90 : member89
조인
- 내부 조인
- SELECT m FROM Member m [INNER] JOIN m.team t
// Member.java
@Entity
@Getter
@Setter
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
}
// Team.java
@Entity
@Getter
@Setter
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
// main.java
Team team = new Team();
team.setName("TeamA");
entityManager.persist(team);
Member member = new Member();
member.setUsername("Member1");
member.setTeam(team);
entityManager.persist(member);
entityManager.flush();
entityManager.clear();
String sql = "select m from Member m inner join m.team t";
List<Member> results = entityManager.createQuery(sql, Member.class)
.getResultList();
- 외부 조인
- SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
- 세타 조인
- SELECT count(m) FROM Member m, Team t WHERE m.username = t.name
- 연관 관계가 없을 때 비교
- cross join으로 쿼리가 나감
조인 - ON 절
- JPA 2.1 부터 지원
- 조인 대상 필터링
- 연관관계 없는 엔티티 외부 조인(하이버네이트 5.1부터 지원)
조인 대상 필터링
- ex) 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인
- JPQL
- SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A'
- SQL
- SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.TEAM_ID = t.id and t.name = 'A'
연관관계 없는 엔티티 외부 조인
- ex) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
- JPQL
- 아무 연관 관계가 없어도 LEFT JOIN 가능
- SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name
- SQL
- SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.username = t.name
서브쿼리
- 나이가 평균보다 많은 회원
SELECT m
FROM Member m
WHERE m.age >
(SELECT AVG(m2.age)
FROM Member m2)
*서브쿼리에서 m을 사용하지 않음
- 한 건이라도 주문한 고객
SELECT m
FROM Member m
WHERE
(SELECT COUNT(o)
FROM Order o
WHERE m = o.member) > 0
*서브쿼리에서 m을 사용 -> 성능이 별로 안나옴
서브쿼리 지원 함수
- [NOT] EXISTS : 서브쿼리에 결과가 존재하면 참
- {ALL | ANY | SOME}
- ALL 모두 만족하면 참
- ANY, SOME 조건을 하나라도 만족하면 참
- [NOT] IN : 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참
서브쿼리 - 예제
- 팀 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 서브 쿼리 한계
- WHERE, HAVING 절에서만 서브 쿼리 가능 (JPA에서 지원)
- SELECT 절도 가능 (하이버네이트에서 지원)
- FROM 절의 서브 쿼리는 현재 JPQL에서 불가능
- 조인으로 풀 수 있으면 풀어서 해결
- 쿼리를 분해해서 2번 날리거나 Application에서 결과를 가져와 조립하거나
JPQL 타입 표현과 기타식
JPQL 타입 표현
- 문자
- 'HELLO', 'She''s'
- 숫자
- 10L(Long), 10D(Double), 10F(Float)
- Boolean
- TRUE, FALSE
- ENUM
- 패키지명 포함
- e.g) where jpa-basic.MemberType.ADMIN
- Parameter로 주로 사용
- QueryDsl 사용하면 import 됨
- Entity
- TYPE(m) = Member (상속관계에서 사용)
- DiscriminatorColumn, DiscriminatorValue (DTYPE)
String sql = "select i from Item i where type(i) = Book";
JPQL 기타
- EXISTS, IN
- AND, OR, NOT
- =, >, >=, <, <=, <>
- BETWEEN, LIKE, IS NULL
조건식 (CASE 등)
조건식 - CASE 식
- 기본 CASE 식
SELECT
CASE WHEN m.age <= 10 then '학생요금'
WHEN m.age >= 60 then '경로요금'
ELSE '일반요금'
END
FROM Member m
- 단순 CASE 식
SELECT
CASE t.name
WHEN 'A' then '110%'
WHEN 'B' then '120%'
ELSE '105%'
END
FROM Team t
- COALESCE : 하나씩 조회해서 null이 아니면 반환
- m.username을 하나씩 조회해서 null(사용자 이름이 없음)이면 '이름 없는 회원'으로 return
- 사용자 이름이 없으면 이름 없는 회원을 반환
SELECT COALESCE(m.username, '이름 없는 회원') FROM Member M
- NULLIF : 두 값이 같으면 null 반환, 다르면 첫번째 값 반환
- 사용자 이름이 '관리자'면 null을 반환하고 나머지는 본인의 이름을 반환
SELECT NULLIF(m.username, '관리자') FROM Member m
JPQL 기본 함수
- CONCAT
SELECT CONCAT('a', 'b') FROM Member m
SELECT 'a' || 'b' FROM Member m
- SUBSTRING
SELECT SUBSTRING(m.username, 2, 3) FROM Member m
- TRIM
- LOWER, UPPER
- LENGTH
- LOCATE
String sql = "SELECT LOCATE('de','abcdefg') FROM Member m";
Integer result = em.createQuery(sql, Integer.class).getResultList();
// 결과
4
- ABS, SQRT, MOD
- SIZE, INDEX(JPA 용도)
사용자 정의 함수
- 하이버네이트 사용 전 DB 방언에 저장해야 함
- Dialect를 상속받아 새로 생성
public class MyH2Dialect extends H2Dialect {
public MyH2Dialect() {
registerFunction("group_concat", new StandardSQLFunction("group_concat", StandardBasicTypes.STRING));
}
}
// persistence.xml
...
<property name="hibername.dialect" value="dialect.MyH2Dialect"/>
String sql = "SELECT FUNCTION('group_concat', m.username) FROM Member m";
'STUDY > JPA' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍 - 기본편] 객체지향쿼리언어2 - 중급문법 (1) | 2023.12.10 |
---|---|
[자바 ORM 표준 JPA 프로그래밍 - 기본편] 값 타입 (0) | 2023.12.10 |
[자바 ORM 표준 JPA 프로그래밍 - 기본편] 프록시와 연관관계 관리 (0) | 2023.12.10 |
[자바 ORM 표준 JPA 프로그래밍 - 기본편] 고급 매핑 (0) | 2023.12.10 |
[자바 ORM 표준 JPA 프로그래밍 - 기본편] 다양한 연관관계 매핑 (1) | 2023.12.10 |