공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.
소개
JPQL 소개
- 가장 단순한 조회 방법이다.
- EntityManager.fin()
- 객체 그래프 탐색(a.getB(). getC())
- JPA를 사용하면 엔티티 객체를 중심으로 개발하게 된다.
- 그러나 검색 쿼리를 작성할 때 문제가 발생한다.
- 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색해야 한다.
- 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능하다.
- 애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함된 SQL이 필요하다.
- JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어를 제공한다.
- JPQL은 SQL과 문법이 유사하며, SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 등의 기능을 지원한다.
- JPQL은 엔티티 객체를 대상으로 쿼리를 수행한다.
- SQL은 데이터베이스 테이블을 대상으로 쿼리를 수행한다.
예시코드
List<Member> result = em.createQuery(
"select m From Member m where m.username like '%kim%'",
Member.class
).getResultList();
for (Member member : result) {
System.out.println("member = " + member);
}
- 주석 부분의 JPQL이 실제 SQL로 변환 후 실행된다.
Criteria
- JPQL을 작성할 때 문자가 아닌 자바 코드로 작성할 수 있다.
- 이는 JPQL 빌더 역할을 한다.
- JPA의 공식 기능이다.
- 하지만 단점으로는 너무 복잡하고 실용성이 떨어진다.
- Criteria 대신에 QueryDSL 사용을 권장한다.
//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("username"), "kim"));
em.createQuery(cq).getResultList();
- 지금은 단순한 쿼리라서 쉬워 보이지만 복잡해지면 끝도 없이 복잡해진다. 실용성이 없으니 사용하지 말자.
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 기능이나 특정 데이터베이스에서만 사용하는 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을 실행하기 직전에 영속성 컨텍스트를 수동으로 플러시해야 한다.
기본 문법과 쿼리 API
JPQL 실습을 위한 엔티티 구조다.
코드로 구현해 보자
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String username;
private int age;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
...
}
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
...
}
@Entity
@Table(name = "ORDERS")
public class Order {
@Id @GeneratedValue
private Long id;
private int orderAmount;
@Embedded
private Address address;
@ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product product;
}
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
...
}
@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, age)
- JPQL 키워드는 대소문자를 구분하지 않는다. (예: SELECT, FROM, where)
- JPQL에서는 테이블 이름이 아닌 엔티티 이름을 사용한다. (예: Member)
- 별칭 사용은 필수다. (예: m) as 키워드는 생략 가능하다.
집합과 정렬
- GROUP BY, HAVING, ORDER BY 사용가능
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);
- TypeQuery : 반환 타입이 명확할 때 사용한다.
- Member를 Type으로 지정하거나, name을 넘기기 위해 String으로 지정할 수 있다.
Query query3 = em.createQuery("select m.username, m.age from Member m");
- Query : 반환 타입이 명확하지 않을 때 사용한다.
- name과 age는 String과 int로 타입이 명확하지 않다. 이럴 때 Query를 사용한다.
결과 조회 API
TypedQuery<Member> query = em.createQuery("select m from Member m", Member.class);
List<Member> resultList = query.getResultList();
for (Member member1 : resultList) {
System.out.println("member1 = " + member1);
}
- query.getResultList(): 결과가 하나 이상일 때, 리스트를 반환한다. 결과가 없으면 빈 리스트를 반환한다.
TypedQuery<Member> query = em.createQuery("select m from Member m where m.id = 10", Member.class);
Member result = query.getSingleResult();
System.out.println("result = " + result);
- query.getSingleResult(): 결과가 정확히 하나여야 한다(단일 객체 반환)
- 결과가 없으면: javax.persistence.NoResultException
- 둘 이상이면: javax.persistence.NonUniqueResultException
파라미터 바인딩
이름기준
TypedQuery<Member> query = em.createQuery("select m from Member m where m.username =:username", Member.class);
query.setParameter("username", "member1");
Member singleResult = query.getSingleResult();
System.out.println("singleResult = " + singleResult);
----> 메서드 체이닝
Member result = em.createQuery("select m from Member m where m.username =:username", Member.class)
.setParameter("username", "member1")
.getSingleResult();
System.out.println("singleResult = " + result.getUsername());
위치 기준
SELECT m FROM Member m where m.username=?1
query.setParameter(1, usernameParam);
- 예를 들어 1, 2, 3 이 있을 때 중간에 숫자를 삽입하면 순서가 밀려서 장애가 발생한다. 따라서 사용을 추천하지 않는다.
프로젝션(SELECT)
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");
member.setAge(10);
em.persist(member);
em.flush();
em.clear();
List<Member> result = em.createQuery("select m from Member m", Member.class)
.getResultList();
Member findMember = result.get(0);
findMember.setAge(20);
tx.commit();
- 이 코드에서 age값이 10 -> 20으로 변경되면 영속성 컨텍스트에서 관리되는 거고 변경되지 않으면 아닌 것이다.
- 20으로 변경되었다.
엔티티 프로젝션으로 조회하는 대상은 전부 영속성 컨텍스트에서 관리된다.
묵시적 조인, 명시적 조인
List<Team> result = em.createQuery("select m.team from Member m", Team.class).getResultList();
- JPQL만 보고 INNER조인을 할 것이라는 것을 한눈에 파악하기 힘들다.
List<Team> result = em.createQuery("select t from Member m join m.team t", Team.class).getResultList();
- 이렇게 JPQL을 보고 JOIN을 할 것이라는 게 예상이 되도록 쿼리를 작성해야 한다.
임베디드 타입 프로젝션
em.createQuery("select o.address from Order o", Address.class).getResultList();
- Order 안의 값 타입인 Address를 조회하는 것이기 때문에 조인이 필요 없다.
- From절에 Address를 적을 수는 없다. 값 타입은 어딘가에 소속되어 있기 때문에 어디 소속인지 엔티티를 명시해주어야 한다.
스칼라 타입 프로젝션
em.createQuery("select distinct m.username, m.age from Member m").getResultList();
- 일반 SQL 프로젝션이랑 거의 똑같다.
여러 값 조회
Query 타입으로 조회
List resultList = em.createQuery("select distinct 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]);
- Query는 반환타입이 명확하지 않을 때 사용한다.
- Query는 일반적으로 Object 배열이나 List를 반환한다.
- 내용을 출력하기 위해 Object 배열로 타입 캐스팅 한다.
Object [] 타입으로 조회
List<Object[]> resultList = em.createQuery("select distinct 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 <Object []>로 지정하면 위의 타입 캐스팅 과정을 생략할 수 있다.
new 명령어로 조회
MemberDTO 생성
public class MemberDTO {
private String username;
private int age;
public MemberDTO() {
}
public MemberDTO(String username, int age) {
this.username = username;
this.age = age;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
List<MemberDTO> result = em.createQuery("select new jpql.MemberDTO(m.username, m.age) from Member m", MemberDTO.class).getResultList();
MemberDTO memberDTO = result.get(0);
System.out.println("memberDTO = " + memberDTO.getUsername());
System.out.println("memberDTO = " + memberDTO.getAge());
- 단순 값을 DTO로 바로 조회할 수 있다.
- 생성자를 호출하듯이 new를 써주고 패키지명을 포함한 클래스명을 써준다.
페이징
JPA는 페이징을 다음 두 API로 추상화한다.
- setFirstResult(int startPosition) : 조회 시작 위치(0부터 시작)
- setMaxResults(int maxResult) : 조회할 데이터 수
페이징 API 예시코드
@Entity
public class Member {
...
@Override
public String toString() {
return "Member{" +
"id=" + id +
", username='" + username + '\'' +
", age=" + age +
'}';
}
}
- 결과를 보기 위해 toString()을 생성한다. 여기서 team은 제거한다.(무한루프)
for (int i = 0; i < 100; i++) {
Member member = new Member();
member.setUsername("member" + i);
member.setAge(i);
em.persist(member);
}
em.flush();
em.clear();
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);
}
tx.commit();
- 총 10개의 결과를 확인할 수 있다.
- 오라클을 사용하면 페이징 쿼리가 기본 deth 3까지 나오는 긴 쿼리가 발생한다.
- 따라서 추상적인 설계만 하고 구체적인 라이브러리는 프레임워크에게 위임해서 코드를 짜면 된다.
- setFirstResult와 setMaxResults 두 API로 간단하게 페이징 쿼리를 처리할 수 있다.
'BackEnd > JPA' 카테고리의 다른 글
[JPA] 객체지향 쿼리 언어2 - 중급 문법(1) (0) | 2024.07.21 |
---|---|
[JPA] 객체지향 쿼리 언어1 - 기본 문법(2) (0) | 2024.07.20 |
[JPA] 값 타입(2) (0) | 2024.07.18 |
[JPA] 값 타입(1) (0) | 2024.07.17 |
[JPA] 영속성 전이와 고아 객체 (0) | 2024.07.16 |