포스트

자바 ORM 표준 JPA4

자바 ORM 표준 JPA 프로그래밍4

김영한 지음

JPA 에서 교과서로 유명한 책을 읽고 일부 부분을 메모해봤습니다.

10 장 객체지향 쿼리 언어

객체지향 쿼리 소개

JPQL 을 한마디로 정의하면 객체지향 SQL 이다. JPA 가 공식 지원하는 기능은 다음과 같다.

  • JPQL
  • Criteira 쿼리 : JPQL 을 편하게 작성하도록 도와주는 API 로, 빌더 클래스 모음이다.
  • 네이티브 SQL : jpa 에서 JPQL 대신 직접 SQL 을 사용할 수 있다.

다음은 JPA 가 공식 지원하는 기능은 아니지만 알아둘 가치가 있다.

  • QueryDSL : criteria 쿼리처럼 JPQL 을 편하게 작성하도록 도와주는 빌더클래스 모음.
  • JDBC 직접 사용, MyBatis 와 같은 SQL 매퍼 프레임워크 사용

JPQL 소개

JPQL 은 엔티티 객체를 조회하는 객체지향 쿼리다. JPQL 은 SQL 을 추상화했기에 특정 db 에 의존하지 않는다.

Criteria 쿼리 소개

Criteria 는 JPQL 을 생성하는 빌더 클래스다. Criteria 의 장점은 문자가 아닌 프로그래밍 코드로 JPQL 을 작성할 수 있다는 점이다. 문자로 작성한 JPQL 보다 코드로 작성한 Criteria 의 장점은 다음과 같다.

  • 컴파일 시점에 오류를 발견할 수 있다.
  • IDE 를 사용하면 코드 자동완성을 지원한다.
  • 동적 쿼리를 작성하기 편하다.

QueryDSL 소개

QueryDSL 도 Criteria 처럼 JPQL 빌더 역할을 한다. 코드 기반이면서 단순하고 사용하기 쉽다. QueryDSL 도 어노테이션 프로세서를 사용해서 쿼리 전용 클래스를 만들어야 한다.

네이티브 SQL 소개

JPA 는 SQL 을 직접 사용할 수 있는 기능을 지원하는데 이것을 네이티브 SQL 이라고 한다. 예를 들어 오라클 데이터베이스에서만 사용하는 Connect By 기능이다 특정 db 에서만 동작하는 SQL 힌트 같은 것이다.

1
2
3
4
String sql="SELECT ID, AGE From MEMBER WHERE ANME = 'kim'";
  em.createNativeQuery(sql,Member.class).getResultList();

JPQL

별칭은 필수이다. 별칭이 없으면 잘못된 문법이라는 오류가 발생한다.
TypeQuery 를 사용해서 반환 타입을 지정할 수 있다.

1
2
3
4
TypedQWuey<Member> query=em.createQuery("SELECT m from Member m",Member.class);
  List<Member> memberList=query.getResultList();

파라미터 바인딩은 이름 기준과 위치 기준 둘 다 지원한다. 위치 기준 파라미터 방식보다는 이름 기준 파라미터 바인딩 방식을 사용하는 것이 더 명확하다.

1
2
3
4
5
TypedQWuey<Member> query=em.createQuery("SELECT m from Member m where m.username =:username",Member.class);
  query.setParameter("username",usernameParam);
  List<Member> memberList=query.getResultList();

페이징 API

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

  • setFirstResult(int startPosition) : 조회 시작 위치
  • setMaxResults(int maxResults) : 조회할 데이터 수
1
2
3
4
5
query.setFirstResult(10);
  query.setMaxResults(20);
  query.getResultList();

11번째 데이터부터 총 20건의 데이터를 조회한다.

Named 쿼리 : 정적 쿼리

  • 동적 쿼리 : em.createQuery 처럼 JQPL 을 문자로 완성해서 직접 넘기는 것이다. 런타임에 특정 조건에 따라 JPQL 을 동적으로 구성할 수 있다.
  • 정적 쿼리 : 미리 정의한 쿼리에 이름을 부여해서 필요할 때 사용할 수 있는데 이것을 Named 쿼리라 한다.

Named 쿼리는 애플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱해준다. 변화지 않는 정적 SQL 이 생성되므로 데이터베이스 조회 서능 최적화에도 유리하다.

1
2
3
4
5
6
7
8
9
10
@Entity
@NamedQuery(
  name = "Member.findByUsername"
  query= "select m from Member m where m.username = :username"
)
public class Member {

}

NamedQuery 에 이름을 줄 때, Member.findByUsername 으로 설정한 것은 엔티티 이름을 앞에 둬서 관리를 용이하게 하기 위함이다.

Criteria

Criteria 쿼리는 JPQL 을 자바 코드로 작성하도록 도와주는 빌더 클래스 API 다.

1
2
3
4
5
6
7
8
9
10
11
CriteriaBuilder cb=em.getCriteriaBuilder();

  CriteriaQuery<Member> cq=cb.createQuery(Member.class);

  Root<Member> m=cq.from(Member.class);
  cq.select(m);

  TypedQuery<Member> query=em.createQuery(cq);
  List<Member> members=query.getResultList();

동적 쿼리

다양한 검색 조건에 따라 실행 시점에 쿼리를 생성하는 것을 동적 쿼리라 한다. 동적 쿼리는 코드 기반인 Criteria 로 작성하는 것이 더 편리하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
CriteriaBuilder cb=em.getCriteriaBuilder();

  CriteriaQuery<Member> cq=cb.createQuery(Member.class);

  Root<Member> m=cq.from(Member.class);
  Join<Member, Team> t=m.join("team");

  List<Predicate> criteria=new ArrayList<Predicate>();

  if(age!=null)criteria.add(cb.equal(m.<Integer>get("age"),cb.parameter(Integer.class,"age")));
  if(username!=null)criteria.add(cb.equal(m.get("username"),cb.parameter(String.class,"username")));

Criteria 로 동적 쿼리를 구성하면 최소한 공백이나 where, and 의 위치로 인한 에러가 발생하지는 않는다.

QueryDSL

Criteria 는 JPQL 보다 안정적이지만, 너무 복잡하고 어렵다는 단점이 있다. 쿼리를 문자가 아닌 코드로 작성해도 쉽고 간결하며 그 모양도 쿼리와 비슷하게 개발할 수 있는 프로젝트가 바로 QueryDSL 이다.

1
2
3
4
5
6
JPAQuery query=new JPAQeury(em);
  QItem item=QItem.item;
  List<Item> list=query.from(item)
  .where(item.name.eq("좋은상품").and(item.price.gt(20000))).list(item);

대표적인 결과 조회 메소드는 다음과 같다.

  • uniqueResult() : 조회 결과가 한 건일 때 사용한다. 조회 결과가 없으면 null 을 반환하고 결과가 하나 이상이면 NonQuniqueResultException 예외가 발생한다.
  • singleResult() : 결과가 하나 이상이면 처음 데이터를 반환한다.
  • list() : 결과가 하나 이상일 때 사용한다. 결과가 없으면 빈 컬렉션을 반환한다.

서브쿼리

서브쿼리는 다음과 같이 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
QItem item=QItem.item;
  QItem itemSUb=new QItem("itemSub");

  query.from(item)
  .where(item.in(
  new JPASubQuery().from(itemSub).where(item.name.eq(itemSub.name)).list(itemSub)
  ))
  .list(item);

동적 쿼리

BooleanBuilder 를 사용하여 동적 쿼리를 생성할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
QItem item=QItem.item;
  BooleanBuilder builder=new BooleanBuilder();
  if(StringUtils.hasText(param.getName())){
  builder.and(item.name.contains(param.getName()));
  }
  if(param.getPrice()!=null){
  builder.and(item.price.gt(param.getPrice()))
  }
  List<Item> result=query.from(item).where(builder).list(item);

스토어드 프로시저

JPA 는 2.1 부터 스토어드 프로시저를 지원한다.

1
2
3
4
5
6
7
8
StoredProcedureQuery spq=em.createStoredProcedureQuery("proc_multiply");
  spq.registerStoredProcedureParameter(1,Integer.class,ParameterMode.IN);
  spq.registerStoredProcedureParameter(2,Integer.class,ParameterMode.OUT);

  spq.setParameter(1,100);
  spq.execute();

em.createStoreProcedureQuery() 메소드에 사용할 스토어드 프로시저 이름을 입력하는 것으로 호출할 수 있다.

벌크 연산

1
2
3
4
5
int resultCount=em.createQuery(qlString)
  .setParameter("stockAmount",10)
  .executeUpdate();

벌크 연산은 executeUpdate() 메소드를 사용한다.
벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다. 따라서 영속성 컨텍스트에 있는 상품과 데이터베이스의 상품 가격이 다를 수 있다.

  • em.refresh() 를 사용해서 데이터베이스에서 연산 처리된 상품을 다시 조회하게 할 수 있다.
  • 벌트 연산을 수행한 후, 상품을 조회하게 할 수 있다.
  • 벌크 연산 수행 후, 영속성 컨텍스트 초기화를 할 수 있다.

플러시 모드와 최적화

1
2
3
4
5
product.setPrice(2000);

  Product product2=em.createQuery("select p from Product p where p.rice = 2000",Product.class).getSingleResult();

플러시 모드의 기본값인 AUTO 인 경우, 위의 로직상에서는 정상적으로 2000원인 상품이 조회된다. 그럼에도 플러시 모드에 COMMIT 이 존재하는 이유는 쿼리시 발생하는 플러시 횟수를 줄여서 성능을 최적화하기 위함이다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.