BackEnd/JPA

[JPA] 객체지향 쿼리 언어 1 (기본 문법)

샤아이인 2022. 4. 8.

내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.

 

1. 소개

● JPQL 소개

가장 단순한 조회 방법은 다음과 같다.

EntityManager.find()
 

또는 객체 그래프 탐색을 하면서 조회할수도 있다. => a.getB().getC()

만약 나이가 18살 이상인 회원을 모두 검색하고 싶다면? 특정 조건을 기준으로 검색을 하고싶다면 어떻게 해야할까?

 

● JPQL 필요성

JPA를 사용하면 엔티티 객체를 중심으로 개발할수 있다.

 

문제는 검색 쿼리이다!

 

애당초 모든 DB데이터를 객체로 변환해서 검색하는 것은 불가능하다.

검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색할수 있는 방법이 필요하다.

애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색조건이 포함된 SQL이 필요하다.

 

이러한 문제를 해결하기 위해 JPA에서는 SQL을 추상화 한 JPQL이라는 객체지향 쿼리 언어를 제공한다.

 

● JPQL 특징

- JPQL은 일반 SQL과 비슷하지만 엔티티를 대상으로 한다는점이 가장큰 차이점이다.

- 일반 SQL은 데이터베이스 테이블을 대상으로 한다.

- JPQL은 SQL을 추상화 한것이기 때문에 특정 데이터베이스 SQL에 의존하지 않는다.

 

예를 들면 다음과 같다.

List resultList = entityManager.createQuery("select m From Member m where m.username like '%kim%'")
        .getResultList();

for (Object member : resultList) {
    System.out.println("member = " + member);
}
 

결과 쿼리는 다음과 같다.

파란 박스 안이 우리가 만든 Entity를 대상으로 하는 JPQL이고, 이를 적절한 SQL로 번역하여 쿼리가 나가게 된다.

 

● Criteria - 소개 => 실무에서 안쓴다! => QueryDSL 사용을 권장.

- 문자가 아닌 자바 코드로 JPQL을 작성할수가 있다.

- JPQL의 빌더역할을 해준다.

- JPA의 표준 스펙이다.

 

● QueryDSL (오픈소스)

QueryDSL은 다음과 같은 방식으로 동적 쿼리를 작성할수가 있다.

//JPQL
//select m from Member m where m.age > 18
JPAFactoryQuery queryFactory = 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 등등

String sql = "SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = ‘kim’";
List<Member> resultList = em.createNativeQuery(sql, Member.class).getResultList();
 

● JDBC 직접 사용, SpringJdbcTemplate 등

실무에서 직접 네이티브한 SQL을 작성하기 보다는 스프링이 제공하는 JDBC를 더 자주 사용한다고 하셨다.

 

JPA를 사용하면서 JDBC 커넥션을 직접 사용하거나, 스프링 JdbcTemplate, 마이바티스등 함께 사용가능 가능하다.

 

단, 영속성 컨텍스트를 적절한 시점에 강제로 플러시 필요가 있다.

예를 들어 다음 코드를 살펴보자.

Member member = new Member();
member.setUsername("test"); // 아직 DB에 반영되지 않음

// JDBC로 쿼리 생성
conn.createQuery("select * from Member where username = 'test'");
// DB상에는 데이터가 없음
em.commit();
 

member는 Jdbc가 쿼리를 수행하는 시점에서 영속성 컨텍스트에만 있고 db에 아직 저장되지 않았기 때문에 조회결과가 없다. 따라서 쿼리 수행 전 수동으로 플러시를 해서 DB와 동기화를 해야한다.

 

2. 기본 문법과 쿼리 API

우선 설명을 위한 예시 구조부터 하나 만들고 시작하자.

출처 - 인프런 김영한 JPA

이를 코드로 구현하면 다음과 같다.

@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<Member>();
    public Long getId() {
        return id;
    }
}

@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;
}

@Entity
public class Product {
    @Id @GeneratedValue
    private Long id;
    private String name;
    private int price;
    private int stockAmound;
}

@Embeddable
public class Address {
    private String city;
    private String street;
    private String zipcode;
}
 

● JPQL 문법

출처 - 인프런 김영한 JPA

- select m from Member as m where m.age > 18

- 엔티티와 속성은 대소문자 구분O (Member, age)

- JPQL 키워드는 대소문자 구분X (SELECT, FROM, where)

- 엔티티 이름 사용, 테이블 이름이 아님(Member)

별칭은 필수(m) (as는 생략가능)

 

- 집합과 정렬기능 전부 사용가능

count(m), sum(m.age), avg(m.age), max(m.age), min(m.age)

group by, having

 

● TypeQuery, Query

- TypeQuery : 반환 타입이 명확할때 사용

TypedQuery<Member> query1 = em.createQuery("select m from Member m", Member.class);
 

위 코드를 보면 Member.class를 반환받을 것을 명시했다. 따라서 TypeQuery를 사용하면 된다.

 

- Query : 반환 타입이 명확하지 않을때 사용

Query query2 = em.createQuery("select m.username, m.age from Member m");
 

위 코드의 경우 m.username(String) 과 m.age(int) 두종류를 반환한다. 따라서 타입을 명시할수가 없다.

이때는 Query 를 사용한다.

 

● 결과 조회 API

- query.getResultList() : 결과가 하나 이상일 때, 리스트 반환

=> 결과가 없다면 빈 리스트를 반환한다.

 

- query.getSingleResult() : 결과가 정확히 하나일때

=> 결과가 하나만 나오는 것 외의 모든 상황에서 에러가 나오기 때문에 사용에 주의가 필요하다.

결과가 없으면: javax.persistence.NoResultException

결과가 둘 이상이면: javax.persistence.NonUniqueResultException

 

● 파라미터 바인딩 - 이름 기준, 위치 기준

- 이름 기준

우선 이름 기준의 파라미터 바인딩 부터 살펴보자. 다음과 같이 사용한다.

SELECT m FROM Member m where m.username = :username
query.setParameter("username", usernameParam);
 

이름 기준을 보면 :username 에 전달할 파라미터를 지정하고 있다. 다음 예시코드를 살펴보자.

Member member = new Member();
member.setAge(10);
member.setUsername("member1");
em.persist(member);

Member singleResult = em.createQuery("select m from Member m where m.username = :username", Member.class)
        .setParameter("username", "member1")
        .getSingleResult();

System.out.println("singleResult = " + singleResult.getUsername());

transaction.commit();
 

실행 결과는 다음과 같다.

 

- 위치 기준 (사용 비추천)

SELECT m FROM Member m where m.username = ?1
query.setParameter(1, usernameParam);
 

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.setAge(10);
member.setUsername("member1");
em.persist(member);

em.flush();
em.clear();

Member singleResult = em.createQuery("select m from Member m", Member.class)
        .getSingleResult();

singleResult.setAge(20);

transaction.commit();
 

em.flush() 이후, em.clear()를 호출하여 영속성 컨텍스트를 전부 비웠다.

이후 singleResult의 age를 변경해주는 코드를 작성하였는데, 만약 singleResult가 영속성 컨텍스트의 관리를 받는다면 age값이 10에서 20으로 바뀌어 있을것이다.

 

왜냐하면 singleResult가 처음 select 되어 영속성 컨텍스트에 들어왔을때 스넵샷과 함께 1차 캐시에 저장이 된다.

이후 필드값을 변경해 주면 commit 시점에 dirty checking을 하여 변경사항을 적용할 쿼리를 만들어 지연 저장소에 저장하고,

이후 DB에 반영하기 때문이다.

 

실행해보면 update 쿼리가 나감과 동시에 DB에서 조회 해보면 20으로 값이 바뀐것을 확인할수가 있다.

● 묵시적 조인, 명시적 조인

- 묵시적 조인

List<Team> result = em.createQuery("select m.team from Member m", Team.class).getResultList();
 

실행 결과는 다음과 같다.

inner join을 하는것을 확인할수가 있다. 문제는 JPQL 만 보면 join을 하고있는지 한눈에 파악이 되지 않는다는 점 이다.

이를 해결하기 위해 명시적으로 join을 사용하기를 권장한다고 해주셨다.

 

- 명시적 조인

List<Team> result = em.createQuery("select t from Member m join m.team t", Team.class).getResultList();
 

실행되는 SQL은 동일하지만 명시적으로 JPQL에 적어줬기에 가독성이 높아지고 JOIN 쿼리가 날아가겠다고 예측이 가능하다.

 

● 임베디드 타입 프로젝션

em.createQuery("select o.address from Order o", Address.class).getRresultList();
 

Order 안에 있는 임베디드 타입인 address를 조회하기 때문에 따로 join이 필요하지 않다.

다만, from절에 Order가 아닌 Address를 적으면 에러가난다. 엔티티로부터 시작되야 한다.

 

● 스칼라 타입 프로젝션 / 여러 값 조회

다음 쿼리는 member의 address 와 age를 찾아온다. 이를 어떤타입으로 받아야 할까?

em.createQuery("select distinct m.address, m.age from Member m").getRresultList();
 

- Query 타입으로 조회

Query query = em.createQuery("select distinct m.username, m.age from Member m");
List resultList = query.getResultList();

Object member1 = resultList.get(0);
Object[] result = (Object[]) member1;
System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);

찾아온 resultList를 그림으로 보면 다음과 같을 것 이다.

Object[] 를 원소로 가지고 있는 list 이다.

결과는 다음과 같다.

 

- Object[] 타입으로 조회

TypedQuery<Object[]> query = em.createQuery("select distinct m.username, m.age from Member m", Object[].class);
List<Object[]> resultList = query.getResultList();

Object[] result= resultList.get(0);
System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);
 

- new 명령어로 조회

우선 데이터를 담을 DTO를 준비한다.

public class MemberDTO {

    private String username;
    private int age;

    public MemberDTO(String username, int age) {
        this.username = username;
        this.age = age;
    }
}
 

이를 활용하여 new 명령어로 받아오는 코드는 다음과 같다.

TypedQuery<MemberDTO> query = em.createQuery("select new jpql.MemberDTO(m.username, m.age) from Member m", MemberDTO.class);
List<MemberDTO> resultList = query.getResultList();

MemberDTO result = resultList.get(0);
System.out.println("username = " + result.getUsername());
System.out.println("age = " + result.getAge());
 

단순값을 DTO를 활용하여 값을 얻어오고 있다.

패키지 명을 포함한 전체 클래스 명을 적어줘야 한다.

순사와 타입이 일치하는 생성자 필요.

 

4. 페이징

● JPA는 페이징을 다음 두 API로 추상화 했다.

1) setFirstResult(int startPosition) : 조회 시작 위치 (0부터 시작)

2) setMaxResults(int maxResult) : 조회할 데이터 수

 

예시를 살펴보자.

for(int i = 0; i < 100; i++){
    Member member = new Member();
    member.setAge(i);
    member.setUsername("member" + 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(0)
        .setMaxResults(10)
        .getResultList();

System.out.println("result.size() = " + result.size());
for (Member m : result) {
    System.out.println("member = " + m);
}

transaction.commit();
 

결과는 다음과 같다.

API 2개로 매우 간단하게 페이징 처리를 할수있게 되었다.

Oracle만 해도 페이징 처리를 하려면 쿼리가 depth 3까지 들어가는 어마무시한 쿼리가 나온다.

이렇게 구현부분은 라이브러리에게 위임하고, 추상적인 부분에서만 사용자는 사용하면 된다.

 

'BackEnd > JPA' 카테고리의 다른 글

[JPA] 객체지향 쿼리 언어 3 (중급 문법)  (0) 2022.04.10
[JPA] 객체지향 쿼리 언어 2 (기본 문법)  (0) 2022.04.09
[JPA] 값 타입 - 2  (0) 2022.04.07
[JPA] 값 타입 - 1  (0) 2022.04.07
[JPA] 영속성 전이와 고아 객체  (0) 2022.04.06

댓글