kiwi

객체지향 쿼리 언어 JPQL

by 키위먹고싶다

jpa

JPA는 다양한 쿼리 방법을 지원한다.

 

  • - JPQL
  • - JPA Criteria
  • - QueryDSL
  • - 네이티브 SQL
  • - JDBC API직접 사용, MyBatis, SpringJdbcTemplate 함께 사용

 

JPQL 소개

 

가장 단순한 조회 방법은 EntityManager.find(), 객체 그래프 탐색 a.getB(). getC())였다. 

그런데 나이가 18살 이상인 회원을 모두 검색하고 싶다면?? 

 

JPA를 사용하면 엔티티 객체를 중심으로 개발한다. 문제는 검색 쿼리인데 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색한다. 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능하다. 애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함된 SQL이 필요하다. 

 

JPA는 SQL을 추상화한 JPQL이라는 개체 지향 쿼리 언어를 제공한다. SQL과 문법이 유사하다. SELECT , FROM , WHERE , GROUP BY, HAVING, JOIN을 지원한다. JPQL은 엔티티 객체를 대상으로 쿼리 하지만 SQL은 데이터베이스 테이블을 대상으로 쿼리 한다.  

 

public static void main(String[] args) {

    EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

    EntityManager em = emf.createEntityManager();

    EntityTransaction tx = em.getTransaction();
    tx.begin();

    try {

        List<Member> result = em.createQuery(
                "select m From Member m where m.name like '%kim%'",
                Member.class
        ).getResultList();

        for (Member member : result) {
            System.out.println(member);
        }

        tx.commit();

    } catch (Exception e) {

        tx.rollback();

    } finally {

        em.close();

    }

    emf.close();
}

 

테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리이며 SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다. 한마디로 객체지향SQL이다.

 

 


Criteria소개 

 

//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"));

List<Member> resultList = em.createQuery(cq).getResultList();

문자가 아닌 자바 코드로 JPQL을 작성할수 있지만 너무 복잡하고 실용성이 없다. 

 


QueryDSL 소개

try {

    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();

    tx.commit();

문자가 아닌 자바코드로 JPQL을 작성할 수 있으며 빌더 역할을 한다. 컴파일 시점에 문법 오류를 찾을 수 있고 동적 쿼리 작성이 편리하며 단순하고 쉽다. 

 

네이티브 SQL 소개

 

List resultList = em.createNativeQuery("select MEMBER_ID, city, street, zipcode from MEMBER").getResultList();

 

JDBC 직접 사용, SpringJdbcTemplate 등

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

단 영속성 콘텍스트를 적절한 시점에 강제로 플러시가 필요하다. JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트 수동 플러시

 


JPQL 문법

 

프로젝션 - 여러 값 조회

 

em.createQuery("select m.username, m.age from Member m")
        .getResultList();

String과 int 타입 중 어드 타입으로 받아야 할까? 

 

TypeQuery는 반환 타입이 명확할때 사용하고 Query는 반환타입이 명확하지 않을 때 사용하는데 이 경우는 반환타입이 명확하지 않으므로 Query로 받을 수 있다. 

 

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]);

첫 번째 방법은 우선 배열 리스트로 받는 것이다. 어느 타입인지 모르니까 Object로 받아두는데 이때는 Object배열이 사용된다. 

 

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

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

두번째는 단순값을 DTO로 바로 조회 하는데 방법이다. 패키지명을 포함한 전체 클래스 명을 적어주어야 한다. 그리고 순서와 타입이 일치하는 생성자가 필요하다. 

 

 

페이징 API

 

JPA는 페이징을 setFirstResult(조회 시작 위치, 0부터 시작), setMaxResults(조회 할 데이터 수) 두 가지 API로 추상화 했다. 

 

List<Member> resultList = em.createQuery("select m from Member m order by m.age desc", Member.class)
        .setFirstResult(0)
        .setMaxResults(10)
        .getResultList();

 

JPQL 타입 표현

String query = "select m.username,  'HEELO', true from Member m where m.type = jpql.MemberType.USER";

List<Object[]> result = em.createQuery(query).getResultList();

enum타입을 사용할때는 패키지명을 다 적어줘야 한다. 

 

 


★★★페치 조인(fetch join) ★★★

 

JPQL에서 성능최적화를 위해 제공하는 기능이다. 연관된 엔티티나 컬렉션을 SQL 한번에 조회 하는 기능이다. 

 

@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;
    private String username;
    private int age;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;

member에서 team은 지연로딩이다. 그렇다면 member를 조회 해보자

 

Team teamA = new Team();
teamA.setName("팀A");
em.persist(teamA);

Team teamB = new Team();
teamB.setName("팀B");
em.persist(teamB);

Member member1 = new Member();
member1.setUsername("회원1");
member1.setTeam(teamA);
em.persist(member1);

Member member2 = new Member();
member2.setUsername("회원2");
member2.setTeam(teamA);
em.persist(member2);

Member member3 = new Member();
member3.setUsername("회원3");
member3.setTeam(teamB);
em.persist(member3);

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

String query = "select m from Member m";

List<Member> result = em.createQuery(query, Member.class).getResultList();

for (Member member : result) {
    System.out.println("member = " + member.getUsername() + ", " + member.getTeam().getName());
}

팀A와 팀B를 저장하고 회원1,2,3을 만들었다. [팀A - 회원1,2] [팀B - 회원3]

 

그리고 member를 조회해서 조회된 멤버의 팀의 이름을 출력했다. 

 

결과를 보자. 

 

먼저 첫번째 select문을 보면 member가 조회됐다. 두번째 select문은 영속성 컨텍스트에 team의 정보가 없으므로 for문안에 있는 member.getTeam.getName()을 가져오기 위해 프록시가 실제 쿼리를 DB에 요청한다. 그래서 팀A의 정보가 영속성 컨텍스트에 담기게 된다. 회원2의 팀 정보를 가져오기 위해 select를 실행할 필요가 없는게 이미 회원 2의 팀A정보가 영속성 컨텍스트에 담겨있기 때문에 회원1과 회원 2의 결과가 같이 나왔다. 그리고 회원3의 팀 정보를 가져오기 위해 또 프록시가 select를 DB에 요청하고 팀B의 정보를 가져오게 된다. 

 

그런데 만약 팀의 회원이 100명이고 팀의 정보가 모두 다르다고 생각해보자. 그럼 select쿼리는 팀의 수 많큼 더 나가게 된다. (N+1) 이럴 경우 패치 조인을 사용한다.

 

String query = "select m from Member m join fetch m.team";

결과를 보면 join을 해서 가져오므로 select문이 1번만 실행된다. 이때는 프록시를 사용하지 않는다. 왜냐? 이미 조인을 통해 DB에서 결과를 가져오기 때문이다. Team이 FetchType.LAZY로 설정되어있어도 패치조인이 먼저 적용된다.

 

 

결론 : 페치조인을 사용할때만 연관된 엔티티도 함께 조회된다. (즉시로딩)   

'jpa' 카테고리의 다른 글

[Transaction Isolation]과 트랜잭션 범위의 영속성 컨텍스트  (0) 2022.04.02
프록시와 연관관계 관리  (0) 2022.03.27
엔티티 매핑  (0) 2022.03.19
JPA소개와 작동 매커니즘  (0) 2022.03.17

블로그의 정보

kiwi

키위먹고싶다

활동하기