JPA - 객체지향 쿼리언어(1)

DevSeoRex·2022년 12월 4일
0
post-thumbnail

객체지향 쿼리

EntityManager.find( ) 메서드를 사용하면 식별자로 엔티티 하나를 조회할 수 있다.
이렇게 조회한 엔티티에 객체 그래프 탐색을 사용하면 연관된 엔티티들을 찾을 수 있다.

만약 나이가 30살 이상인 회원을 모두 검색해야 한다면, 모든 회원 엔티티를 메모리에 올려두고 애플리케이션에서 30살 이상인 회원을 검색하는 것은 현실성이 없다.

일반 SQL을 직접 작성한다면 최대한 걸러서 조회할 수 있겠지만 ORM을 사용하면 엔티티 객체를 대상으로 개발하기 때문에 검색도 테이블이 아닌 엔티티 객체를 대상으로 하는 방법이 필요하다.

💡 JPQL은 이런 문제를 해결하기 위해 만들어졌다.

JPQL

  • 테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리다.
  • SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
  • 데이터베이스 방언(Dialect)만 변경하면 JPQL을 수정하지 않아도 데이터베이스 변경이 가능하다.

JPA는 JPQL와 다양한 검색 방법을 제공한다.

  • JPQL
  • Creiteria Query : JPQL을 편하게 작성하도록 도와주는 API, 빌더 클래스 모음
  • Native SQL : JPA에서 JPQL 대신 직접 SQL을 사용할 수 있다.
  • QueryDSL : Criteria 쿼리처럼 JPQL을 편하게 작성하도록 도와주는 빌더 클래스 모음, 비표준 오픈소스 프레임워크다.
  • JDBC 직접사용, MyBatis와 같은 SQL 매퍼 프레임워크 사용 : 필요하면 직접 JDBC를 사용할 수 있다.
// JPQL 사용 예제
String jpql = "select m from Member as m where m.username = 'kim'";
List<Member> resultList = em.createQuery(jpql, Member.class).getResultList();

Criteria 쿼리

  • Criteria는 JPQL을 생성하는 빌더 클래스다.

Criteria의 장점

  • 문자가 아닌 프로그래밍(자바) 코드로 JPQL을 작성할 수 있다는 점이다.
  • 컴파일 시점에 오류를 발견할 수 있다.
  • IDE를 사용하면 코드 자동완성을 지원한다.
  • 동적 쿼리를 작성하기 편하다.
// Criteria Query 사용 예제

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

QueryDSL

  • QueryDSL도 Criteria처럼 JPQL 빌더 역할을 한다.
  • QueryDSL의 장점은 코드 기반이면서 단순하고 사용하기 쉽다.
// QueryDSL 사용 에제

// 준비
JPAQuery query = new JPAQuery(em);
Qmember member = Qmember.member;

// 쿼리, 결과조회
List<Member> members = 
		query.from(member)
        .where(member.username.eq("kim))
        .list(member);

💡 QueryDSL도 어노테이션 프로세서를 사용해서 쿼리 전용 클래스를 만들어야 한다.

Native SQL

  • JPA에서 SQL을 직접 사용할 수 있는 기능이다.
  • 특정 데이터베이스에 의존하는 기능을 사용해야 할 때 사용한다.
  • 데이터베이스를 변경하면 Native SQL도 수정해야 한다.
// Native SQL 사용 예제
String sql = "SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = 'kim'";

List<Member> resultList = em.createNativeQuery(sql, Member.class).getResultList();

JDBC 직접 사용, MyBatis 같은 SQL 매퍼 프레임워크 사용

  • JDBC 커넥션에 직접 접근하고 싶다면 JPA 구현체가 제공하는 방법을 사용해야 한다.
// JDBC 커넥션 직접 접근 예제
Session session = entityManager.unwrap(Session.class);
session.doWork(new Work() {
	
    @Override
    public void execute(Connection connection) throws SQLException {
    	// work ...
    }

});
  • 먼저 JPA EntityManager에서 하이버네이트 Session을 구하고, Session의 doWork( ) 메서드를 호출하면 된다.
  • JDBC나 마이바티스를 JPA와 함께 사용하면 영속성 컨텍스트를 적절한 시점에 강제로 플러시해야 한다.
  • JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트를 수동으로 플러시해서 데이터베이스와 영속성 컨텍스트를 동기화 해줘야 한다.

JPQL - SELECT 문

  • select 문은 다음과 같이 사용한다.
SELECT m FROM MEMBER AS m where m.username = 'Hello'
  • 대소문자 구분
    엔티티와 속성은 대소문자를 구분한다. SELECT, FROM, AS같은 JPQL 키워드는 대소문자를 구분하지 않는다.
  • 엔티티 이름
    JPQL에서 사용한 Member는 클래스 명이 아니라 엔티티 명이다. 기본값인 클래스 명을 엔티티 명으로 사용하는 것이 좋다.
  • 별칭은 필수
    JPQL은 별칭을 필수로 사용해야 한다. 별칭 없이 작성하면 잘못된 문법이라는 오류가 발생한다.

💡 JPA 표준 명세는 별칭을 식별 변수(identification variable)라는 용어로 정의했다.

TypeQuery & Query

  • 반환할 타입을 명확하게 지정할 수 있으면 TypeQuery, 그렇지 않으면 Query객체를 사용하면 된다.
// TypeQuery 사용 예제 - 반환할 타입이 명확한 경우
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);

List<Member> resultList = query.getResultList();


// Query 사용 예제 - 반환할 타입이 명확하지 않은 경우
Query query = em.createQuery("SELECT m.username, m.age from MEMBER m");
List resultList = query.getResultList();
  • SELECT 절에서 여러 엔티티나 컬럼을 선택할 때는 반환할 타입이 명확하지 않으므로 Query 객체를 사용해야 한다.
  • Query 객체는 SELECT 절의 조회 대상이 둘 이상일 경우 Object[ ]를 반환한다.
  • 타입을 변환할 필요가 없는 TypeQuery를 사용하는 것이 더 편리하다.

결과 조회 메서드

  • query.getResultList( ) : 결과를 리스트로 반환한다. 만약 결과가 없으면 빈 컬렉션을 반환한다.
  • query.getSingleResult() : 결과가 정확히 하나일때만 사용해야 한다.
    결과가 없으면 NoResultException이 발생한다.
    결과가 1개보다 많으면 NonUniqueResultException이 발생한다.

💡 getSingleResult( )는 결과가 정확히 1개가 아니면 예외가 발생한다는 점에 주의해야 한다.

파라미터 바인딩

  • JDBC는 위치 기준 파라미터 바인딩만 지원하지만, JPQL은 이름 기준 파라미터 바인딩을 지원한다.

이름 기준 파라미터 바인딩

  • 이름 기준 파라미터는 파라미터를 이름으로 구분하는 방법이다.
  • 이름 기준 파라미터는 앞에 :를 사용한다.
// 이름 기준 파라미터 사용
String usernameParam = "User1";

TypedQuery<Member> query = 
		em.createQuery("SELECT m FROM Member m where m.username = :username", Member.class);
        
query.setParameter("username", usernameParam);
List<Member> resultList = query.getResultList();
  • JPQL API는 대부분 메서드 체인 방식으로 설계되어 있어서 연속해서 작성 가능하다.
// JPQL 메서드 체이닝 예제
List<Member> members = 
		em.createQuery("SELECT m FROM Member m where m.username = :username", Member.class)
    	.setParameter("username", usernameParam)
    	.getResultList();

위치 기준 파라미터 바인딩

  • 위치 기준 파라미터를 사용하려면 ? 다음에 위치 값을 주면 된다.
// 위치 기준 파라미터 사용 예제
List<Member> members = 
	em.createQuery("SELECT m FROM Member m where m.username = ?1", Member.class)
    .setParameter(1, usernameParam)
    .getResultList();

💡 위치 기준 파라미터 방식 보다는 이름 기준 파라미터 바인딩 방식을 사용하는 것이 더 명확하다.

파라미터 바인딩 방식을 사용해야 하는 이유

  • 파라미터 바인딩 방식을 사용하지 않고 직접 문자를 더해 만들어 넣으면 악의적인 사용자에 의해 SQL 인젝션 공격을 당할 수 있다.
  • 파라미터 바인딩 방식을 사용하면 파라미터의 값이 달라도 같은 쿼리로 인식해서 JPQ는 JPQL을 SQL로 파싱한 결과를 재사용 가능해서 성능이 좋아진다.
  • 파라미터 바인딩 방식은 선택이 아닌 필수다.
// 파라미터 바인딩 방식을 사용하지 않고 직접 JPQL을 만들면 위험하다.
"select m from Member m where m.username = '" + usernameParam + "'";

출처 : 자바 ORM 표준 JPA 프로그래밍(에이콘, 김영한 저)

0개의 댓글