[Java] 48. JPA에서 Query를 사용하는 방법(JPQL 쿼리 사용법)


Study/Java  2021. 6. 17. 14:23

안녕하세요. 명월입니다.


이 글은 JPA에서 Query를 사용하는 방법(JPQL 쿼리 사용법)에 대한 글입니다.


JPA에서 기본적으로 데이터를 취득하는 방법은 Entity에 선언되어 있는 NameQuery를 통해 데이터를 취득합니다. 그리고 하위 레퍼런스의 Join된 데이터를 얻으려면 취득한 데이터에 List의 함수 get, size 함수를 통해서 읽어옵니다.

import java.util.List;
import java.util.Optional;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import model.User;

public class Main {
  // 람다식 인터페이스
  interface Expression {
    void run(EntityManager em);
  }

  // Persistence로 EntityManager를 가져와서 실행하고 종료하는 함수
  private static void transaction(Expression lambda) {
    // FactoryManager를 생성합니다. "JpaExample"은 persistence.xml에 쓰여 있는 이름이다.
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("JpaExample");
    // Manager를 생성한다.
    EntityManager em = emf.createEntityManager();
    try {
      // transaction을 가져온다.
      EntityTransaction transaction = em.getTransaction();
      try {
        // transaction 실행
        transaction.begin();
        // 람다식을 실행한다.
        lambda.run(em);
        // transaction을 커밋한다.
        transaction.commit();
      } catch (Throwable e) {
        // 에러가 발생하면 rollback한다.
        if (transaction.isActive()) {
          transaction.rollback();
        }
        // 에러 출력
        e.printStackTrace();
      }
    } finally {
      // 각 FactoryManager와 Manager를 닫는다.
      em.close();
      emf.close();
    }
  }

  // 실행 함수
  @SuppressWarnings("unchecked")
  public static void main(String... args) {
    transaction((em) -> {
      // user 클래스의 NamedQuery의 User.findAll의 쿼리대로 데이터를 취득
      List<User> users = em.createNamedQuery("User.findAll").getResultList();
      // nowonbun 데이터 취득
      Optional<User> op = users.stream().filter(x -> "nowonbun".equals(x.getId())).findFirst();
      // 데이터가 있으면
      if (op.isPresent()) {
        // 데이터 취득
        User user = op.get();
        // 콘솔 출력
        System.out.println(user.getId());
        System.out.println(user.getName());
      }
    });
  }
}

그런데 위의 방식의 문제점은 데이터를 모두 가져온 후에 코드 상의 filter와 분기로 데이터를 취득합니다.

즉, 데이터가 많으면 시스템이 느려지겠습니다.


그러면 쿼리로 데이터베이스로부터 데이터를 취득하는 단계에서 어느 정도 데이터를 분류하고 가져와서 처리하는 것이 좋습니다.

이렇게 쿼리로 데이터를 취득하는 방법은 두가지가 있습니다. 하나는 JPA에서 사용하는 JPQL 쿼리와 데이터에서 사용하는 SQL 쿼리를 이용해서 사용하는 방법입니다.


먼저 데이터베이스에서 사용하는 SQL 쿼리를 사용하는 예제입니다.

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import javax.persistence.Query;

public class Main {
  // 람다식 인터페이스
  interface Expression {
    void run(EntityManager em);
  }

  // Persistence로 EntityManager를 가져와서 실행하고 종료하는 함수
  private static void transaction(Expression lambda) {
    // FactoryManager를 생성합니다. "JpaExample"은 persistence.xml에 쓰여 있는 이름이다.
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("JpaExample");
    // Manager를 생성한다.
    EntityManager em = emf.createEntityManager();
    try {
      // transaction을 가져온다.
      EntityTransaction transaction = em.getTransaction();
      try {
        // transaction 실행
        transaction.begin();
        // 람다식을 실행한다.
        lambda.run(em);
        // transaction을 커밋한다.
        transaction.commit();
      } catch (Throwable e) {
        // 에러가 발생하면 rollback한다.
        if (transaction.isActive()) {
          transaction.rollback();
        }
        // 에러 출력
        e.printStackTrace();
      }
    } finally {
      // 각 FactoryManager와 Manager를 닫는다.
      em.close();
      emf.close();
    }
  }

  // 실행 함수
  public static void main(String... args) {
    transaction((em) -> {
      // SQL 쿼리 입력
      Query qy = em.createNativeQuery("select * from user u where u.id = ?");
      // 파라미터 입력
      qy.setParameter(1, "nowonbun");
      // 데이터 취득
      Object[] user = (Object[])qy.getSingleResult();
      // 콘솔 출력
      System.out.println(user[0]);
      System.out.println(user[1]);
    });
  }
}

Manager 클래스에서 createNativeQuery 함수를 사용하여 데이터베이스의 SQL로 취득이 가능합니다.

문제는 JPA로 생성한 Entity 클래스와 매핑이 되지 않는다는 것입니다.


JPA에서 취득한 데이터로 클래스의 데이터를 수정하면 그 값으로 데이터베이스의 값을 update하고 삭제하고 여러가지 작업을 할 수 있습니다. 하지만 매핑이 되지 않으면 JPA를 사용할 이유가 없어지네요.

그럼 JPQL 쿼리로 데이터를 취득해서 온전하게 JPA기능을 사용할 수 있습니다. 사실 JPQL 쿼리라고 해서 SQL과 전혀 다른 종류의 쿼리는 아닙니다. 거의 90%는 비슷합니다.

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import javax.persistence.Query;
import model.User;

public class Main {
  // 람다식 인터페이스
  interface Expression {
    void run(EntityManager em);
  }

  // Persistence로 EntityManager를 가져와서 실행하고 종료하는 함수
  private static void transaction(Expression lambda) {
    // FactoryManager를 생성합니다. "JpaExample"은 persistence.xml에 쓰여 있는 이름이다.
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("JpaExample");
    // Manager를 생성한다.
    EntityManager em = emf.createEntityManager();
    try {
      // transaction을 가져온다.
      EntityTransaction transaction = em.getTransaction();
      try {
        // transaction 실행
        transaction.begin();
        // 람다식을 실행한다.
        lambda.run(em);
        // transaction을 커밋한다.
        transaction.commit();
      } catch (Throwable e) {
        // 에러가 발생하면 rollback한다.
        if (transaction.isActive()) {
          transaction.rollback();
        }
        // 에러 출력
        e.printStackTrace();
      }
    } finally {
      // 각 FactoryManager와 Manager를 닫는다.
      em.close();
      emf.close();
    }
  }

  // 실행 함수
  public static void main(String... args) {
    transaction((em) -> {
      // JPQL 쿼리
      Query qy = em.createQuery("select u from User u where u.id = :id");
      // 파라미터 입력
      qy.setParameter("id", "nowonbun");
      // 데이터 취득
      User user = (User)qy.getSingleResult();
      // 콘솔 출력
      System.out.println(user.getId());
      System.out.println(user.getName());
    });
  }
}

SQL 쿼리와 다른 점은 테이블명입니다. SQL에서는 테이블명을 대소문자 구분없이 취득이 가능합니다만 JPQL은 테이블명이 아닌 클래스명입니다.

즉, 클래스명은 대소문자를 구분하니 user가 아닌 정확하게 User라고 작성해야 합니다.


그리고 별표시(*)로 모든 데이터 취득이라는 것도 존재하지 않습니다.

리턴할 클래스를 사용합니다. 우리는 User 클래스로 반환을 하기 때문에 User의 치환 이름(Aliases)인 u를 사용합니다.


그리고 where절에 있는 검색 필드명은 SQL의 필드명이 아닌 클래스의 변수명입니다.

현재 예제는 변수명과 SQL 필드명이 같기 때문에 같아 보이네요.


그럼 필드명을 바꾸어서 테스트하겠습니다.

id에 @Column 어트리뷰트를 사용해서 sql의 필드명을 매핑시키고 변수명은 test로 변경했습니다.

그럼 JPQL에서는 id가 아닌 test를 사용해야 합니다.

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import javax.persistence.Query;
import model.User;

public class Main {
  // 람다식 인터페이스
  interface Expression {
    void run(EntityManager em);
  }

  // Persistence로 EntityManager를 가져와서 실행하고 종료하는 함수
  private static void transaction(Expression lambda) {
    // FactoryManager를 생성합니다. "JpaExample"은 persistence.xml에 쓰여 있는 이름이다.
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("JpaExample");
    // Manager를 생성한다.
    EntityManager em = emf.createEntityManager();
    try {
      // transaction을 가져온다.
      EntityTransaction transaction = em.getTransaction();
      try {
        // transaction 실행
        transaction.begin();
        // 람다식을 실행한다.
        lambda.run(em);
        // transaction을 커밋한다.
        transaction.commit();
      } catch (Throwable e) {
        // 에러가 발생하면 rollback한다.
        if (transaction.isActive()) {
          transaction.rollback();
        }
        // 에러 출력
        e.printStackTrace();
      }
    } finally {
      // 각 FactoryManager와 Manager를 닫는다.
      em.close();
      emf.close();
    }
  }

  // 실행 함수
  public static void main(String... args) {
    transaction((em) -> {
      // JQPL 쿼리 입력
      Query qy = em.createQuery("select u from User u where u.test = :id");
      // 파라미터 입력
      qy.setParameter("id", "nowonbun");
      // 데이터 취득
      User user = (User)qy.getSingleResult();
      // 콘솔 출력
      System.out.println(user.getId());
      System.out.println(user.getName());
    });
  }
}

위 쿼리에서는 id가 아닌 test를 사용했습니다. 물론 필드명이기 떄문에 대소문자 구분은 중요합니다.

그럼 결과는 Object 배열 타입이 아닌 User 타입으로 리턴이 되고 사용할 수 있습니다.


JQPL의 쿼리는 FETCH의 레퍼런스 데이터의 결과도 제어할 수 있습니다.

위 예제처럼 info테이블에 age가 21인 데이터를 다량 넣었습니다.

그러나 저는 age가 20인 데이터만 Info 리스트에 검색하고 싶습니다.

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import javax.persistence.Query;
import model.User;

public class Main {
  // 람다식 인터페이스
  interface Expression {
    void run(EntityManager em);
  }

  // Persistence로 EntityManager를 가져와서 실행하고 종료하는 함수
  private static void transaction(Expression lambda) {
    // FactoryManager를 생성합니다. "JpaExample"은 persistence.xml에 쓰여 있는 이름이다.
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("JpaExample");
    // Manager를 생성한다.
    EntityManager em = emf.createEntityManager();
    try {
      // transaction을 가져온다.
      EntityTransaction transaction = em.getTransaction();
      try {
        // transaction 실행
        transaction.begin();
        // 람다식을 실행한다.
        lambda.run(em);
        // transaction을 커밋한다.
        transaction.commit();
      } catch (Throwable e) {
        // 에러가 발생하면 rollback한다.
        if (transaction.isActive()) {
          transaction.rollback();
        }
        // 에러 출력
        e.printStackTrace();
      }
    } finally {
      // 각 FactoryManager와 Manager를 닫는다.
      em.close();
      emf.close();
    }
  }

  // 실행 함수
  public static void main(String... args) {
    transaction((em) -> {
      // JPQL 작성
      Query qy = em.createQuery("select u from User u join fetch u.infos i where u.id = :id and i.age = 20");
      // 파라미터 입력
      qy.setParameter("id", "nowonbun");
      // 데이터 취득
      User user = (User)qy.getSingleResult();
      // 콘솔 출력
      System.out.println(user.getInfos().size());
      System.out.println(user.getInfos().get(0).getAge());
    });
  }
}

join fetch를 넣어서 infos를 i로 치환하고 i.age를 20만 검색되게 설정했습니다.

결과는 getInfos의 데이터 개수는 한개이고 그 데이터의 age는 20만 나오는 것을 확인할 수 있습니다.


역시 JPQL도 string으로 작성하는 쿼리이기 때문에 소스 이곳 저곳에 작성하면 관리가 어렵습니다.

그래서 보통은 Entity에 쿼리를 작성하고 실제 소스에서는 만들어진 쿼리를 가져다 쓰는 형식으로 작성합니다.

Entity 클래스 위에 @NamedQueries 어트리뷰트를 이용해서 쿼리를 정리할 수 있습니다.


참고로 저는 IDE를 eclipse를 사용합니다만 Entity위에 쿼리를 작성하면 String 데이터라도 Query 검사를 합니다. 그런데 이게 버그가 있습니다. fetch join 치환 값을 인식하지 못합니다.

물론 무시하고 디버그 실행해도 문제없지만 툴에 에러 표시가 나오면 보기 불편합니다.

preferences로 들어가서 JPA -> query 부분의 에러 표시를 변경합니다.

출처 - https://www.eclipse.org/forums/index.php/t/369011/
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import javax.persistence.Query;
import model.User;

public class Main {
  // 람다식 인터페이스
  interface Expression {
    void run(EntityManager em);
  }

  // Persistence로 EntityManager를 가져와서 실행하고 종료하는 함수
  private static void transaction(Expression lambda) {
    // FactoryManager를 생성합니다. "JpaExample"은 persistence.xml에 쓰여 있는 이름이다.
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("JpaExample");
    // Manager를 생성한다.
    EntityManager em = emf.createEntityManager();
    try {
      // transaction을 가져온다.
      EntityTransaction transaction = em.getTransaction();
      try {
        // transaction 실행
        transaction.begin();
        // 람다식을 실행한다.
        lambda.run(em);
        // transaction을 커밋한다.
        transaction.commit();
      } catch (Throwable e) {
        // 에러가 발생하면 rollback한다.
        if (transaction.isActive()) {
          transaction.rollback();
        }
        // 에러 출력
        e.printStackTrace();
      }
    } finally {
      // 각 FactoryManager와 Manager를 닫는다.
      em.close();
      emf.close();
    }
  }

  // 실행 함수
  public static void main(String... args) {
    transaction((em) -> {
      // Entity의 @NamedQuery로 데이터를 취득한다.
      Query qy = em.createNamedQuery("User.nowonbun", User.class);
      // 파라미터 입력
      qy.setParameter("id", "nowonbun");
      // 데이터 취득
      User user = (User)qy.getSingleResult();
      // 콘솔 출력
      System.out.println(user.getInfos().size());
      System.out.println(user.getInfos().get(0).getAge());
    });
  }
}

그리고 createNamedQuery를 통해서 Entity 클래스로 부터 쿼리를 가져오고 데이터 취득하고 결과는 같습니다.

이렇게 하면 모든 쿼리문을 한 곳에서 관리하는 것이 가능하겠네요. 테이블 변경이 발생시에 그에 대한 업데이트 관리도 편해 집니다.


여기까지 JPA에서 Query를 사용하는 방법(JPQL 쿼리 사용법)에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.