[자바 ORM 표준 JPA] JPQL 네임드 쿼리(Named Query)

[자바 ORM 표준 JPA] JPQL 네임드 쿼리(Named Query)

JPQL 네임드 쿼리(Named Query)


Named 쿼리#


@NamedQuery#


쿼리를 엔티티 같은 곳에 미리 선언을 해둘 수 있는 기능. 쿼리를 재활용 해서 사용할 수 있습니다.

@Entity
@NamedQuery(
	name = "member.findByUsername",
	query = "SELECT m FROM Member WHERE n.username :username"
)

public class Member {
	...
}

List<Member> resultList = 
	em.createQuery("Member.findByUsername", Member.class)
	  .setParameter("username", "회원1")
	  .getResultList();

정적 쿼리#


  • 미리 정의해서 이름을 부여해두고 사용하는 JPQL
  • 정적 쿼리
  • 어노테이션, XML에 정의
  • 애플리케이션 로딩 시점에 초기화 후 재사용
    • 애플리케이션 로딩 시점에 정적인 쿼리를 JPA 또는 하이버네이트가 SQL로 파싱을 하고, 캐싱을 합니다.
  • 애플리케이션 로딩 시점에 쿼리를 검증

@NamedQuery 사용 테스트#

Member.java

package jpql.domain;

import javax.persistence.*;
import jpql.domain.*;


@Entity
@NamedQuery(name = "Member.findByUsername", // * 관례로 엔티티명.쿼리명 으로 많이 사용
            query = "SELECT m FROM Member m WHERE m.username = :username"
)
public class Member {

    public Member(){
    }

    @Id @GeneratedValue
    private Long id;

    private String username;

    private int age;

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

    public void changeTeam(Team team){
        this.team = team;
        team.getMembers().add(this);
    }

    @Enumerated(EnumType.STRING) // 기본이 숫자 EnumType.ORDINAL 이기 때문에 String으로 필수로 세팅
    private MemberType type;

    public MemberType getType() {
        return type;
    }

    public void setType(MemberType type) {
        this.type = type;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Team getTeam() {
        return team;
    }

    public void setTeam(Team team) {
        this.team = team;
    }

    @Override
    public String toString() {
        return "Member{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", age=" + age +
                /*", team=" + team  //toString 양방향 적용시 무한루프될 가능성 toString 에서는 연관관계 제거  */
                '}';
    }
}

JpqlMain.java

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

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

            Team team3 = new Team();
            team3.setName("팀C");
            em.persist(team3);

            Member member1 = new Member();
            member1.setUsername("회원1");
            member1.setAge(31);
            member1.changeTeam(team1);
            member1.setType(MemberType.USER);
            em.persist(member1);

            Member member2 = new Member();
            member2.setUsername("회원2");
            member2.setAge(32);
            member2.changeTeam(team1);
            member2.setType(MemberType.USER);
            em.persist(member2);

            Member member3 = new Member();
            member3.setUsername("회원3");
            member3.setAge(33);
            member3.changeTeam(team2);
            member3.setType(MemberType.USER);
            em.persist(member3);

            Member member4 = new Member();
            member4.setUsername("회원4");
            member4.setAge(34);
            member4.changeTeam(team3);
            member4.setType(MemberType.USER);
            em.persist(member4);

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


            List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class)
                    .setParameter("username", "회원1")
                    .getResultList();


            for(Member member : resultList){
                System.out.println("name = " + member.getUsername()+ ", age = " + member.getAge());
            }

            tx.commit();

console

Hibernate: 
    /* Member.findByUsername */ select
        member0_.id as id1_0_,
        member0_.age as age2_0_,
        member0_.TEAM_ID as team_id5_0_,
        member0_.type as type3_0_,
        member0_.username as username4_0_ 
    from
        Member member0_ 
    where
        member0_.username=?
        
name = 회원1, age = 31

애플리케이션 로딩 시점에 쿼리를 검증#

만약 누군가가 쿼리를 작성할때 실수로 오타를 냈다고 하고 테스트를 해보겠습니다.

@Entity
@NamedQuery(name = "Member.findByUsername", // * 관례로 엔티티명.쿼리명 으로 많이 사용
            query = "SELECT m FROM Membe m WHERE m.username = :username"
)

JpqlMain.java - 애플리케이션 재시작

console


Caused by: org.hibernate.HibernateException: Errors in named queries: 
Member.findByUsername failed because of: org.hibernate.hql.internal.ast.QuerySyntaxException: Membe is not mapped [SELECT m FROM Membe m WHERE m.username = :username]
	at org.hibernate.internal.SessionFactoryImpl.<init>(SessionFactoryImpl.java:341)
	at org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:469)
	at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1259)
	... 4 more


어노테이션에 등록이 되어있기 때문에 Jpa가 미리 파싱을 하고 캐싱을 하려는 중

문법이 맞지 않는 상황이 벌어지면 오류를 발생시킵니다.

Member.findByUsername failed because of: org.hibernate.hql.internal.ast.QuerySyntaxException: Membe is not mapped [SELECT m FROM Membe m WHERE m.username = :username]

개발하면서 가장 좋은 에러는 즉시 나는 에러가 좋습니다. 바로 찾을 수 있고 수정할 수 있기 때문입니다. 그러면 가장 안좋은 에러는 사용자가 사용중에 무언가 액션을 했을때 나는 오류입니다. 그리고 그 중간 정도의 오류는 컴파일 시점에 나는 오류입니다. 어떻게든지 로컬에서라도 실행은 시키고 배포를 하기 때문에 대부분의 오류를 다 잡아줄수 있습니다.

Named 쿼리 - XML에 정의#

[META-INF/persistence.xml]

<persistance-unit name="jpabook">
	<mapping-file>META-INF/ormMember.xml</mapping-file>

[META-INF/ormMemer.xml]

<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm" version="2.1">
	
	<named-query name="Member.findByUsername">
		<query><![CDATA[
			SELECT m
			FROM Member m
			WHERE m.username = :username
		]]></query>
	</named-query>
	
	<named-query name="Member.count">
		<query><![CDATA[
			SELECT COUNT(m) FROM Member m
		]]></query>
	</named-query>
	
</entity-mappings>

Named 쿼리 환경에 따른 설정#

  • XML이 항상 우선권을 가진다.
  • 애플리케이션 운영 환경에 따라 다른 XML을 배포할 수 있다.

스프링 데이터 JPA를 사용하게 되면 인터페이스 바로 위에 사용 가능합니다.

Spring Data JPA

public interface UserRepository extends JpaReposotory<User, Long>{
	@Query("SELECT u from User u WHERE u.emailAddress = ?1")
	User findByEmailAddress(String emailAddress);
}

스프링 JPA는 JPA를 편하게 사용하기 위해 추상화 하여, JPA의 껍데기 역활만 합니다.
@Query(“SELECT u from User u WHERE u.emailAddress = ?1”) 가 바로 Named 쿼리 입니다.
JPA가 해당 어노테이션과 쿼리를 Named 쿼리로 등록합니다. 그래서 애플리케이션 로딩 시점에 파싱하게 되고 문법 오류가 있으면 바로 잡아주게 됩니다.

결론#


@Entity
@NamedQuery(
	name = "member.findByUsername",
	query = "SELECT m FROM Member WHERE n.username :username"
)

public class Member {
	...
}

List<Member> resultList = 
	em.createQuery("Member.findByUsername", Member.class)
	  .setParameter("username", "회원1")
	  .getResultList();

위와 같이 엔티티에 Named Query를 추가하는 것은 코드가 매우 지저분 해지고,
결국 실무에서는 Spring Data JPA를 섞어 쓰는게 좋기 때문에

Spring Data JPA

public interface UserRepository extends JpaReposotory<User, Long>{
	@Query("SELECT u from User u WHERE u.emailAddress = ?1")
	User findByEmailAddress(String emailAddress);
}

이와 같이 Spring Data JPA 방식으로 개발하는게 좋습니다.

이전 소스#


src/main/java/jpql/domain/Member.java

Member.java
package jpql.domain;

import javax.persistence.*;


@Entity
public class Member {

    public Member(){
    }

    @Id @GeneratedValue
    private Long id;

    private String username;

    private int age;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team = new Team();

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public jpql.domain.Team getTeam() {
        return team;
    }

    public void setTeam(jpql.domain.Team team) {
        this.team = team;
    }
    
    @Override
    public String toString() {
        return "Member{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", age=" + age +
                '}';
    }
}

src/main/java/jpql/domain/Team.java

Team.java
package jpql.domain;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;


@Entity
public class Team {

    public Team() {
    }

    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<Member> getMembers() {
        return members;
    }

    public void setMembers(List<Member> members) {
        this.members = members;
    }
    
    @Override
    public String toString() {
        return "Team{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

src/main/java/jpql/domain/Order.java

Order.java
package jpql.domain;

import javax.persistence.*;

@Entity
@Table(name = "ORDERS") //ORDER 가 예약어라 ORDERS로 테이블 명칭 지정
public class Order {
    public Order() {
    }

    @Id @GeneratedValue
    private Long id;

    private int orderAmount;

    @Embedded
    private Address orderAddress;

    @ManyToOne
    @JoinColumn(name = "PRODUCT_ID")
    private Product product;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public int getOrderAmount() {
        return orderAmount;
    }

    public void setOrderAmount(int orderAmount) {
        this.orderAmount = orderAmount;
    }

    public Address getOrderAddress() {
        return orderAddress;
    }

    public void setOrderAddress(Address orderAddress) {
        this.orderAddress = orderAddress;
    }

    public Product getProduct() {
        return product;
    }

    public void setProduct(Product product) {
        this.product = product;
    }
}

src/main/java/jpql/domain/Address.java

Address.java
package jpql.domain;

import javax.persistence.Embeddable;
import java.util.Objects;

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

    public String getCity() {
        return city;
    }

    private void setCity(String city) {
        this.city = city;
    }

    public String getStreet() {
        return street;
    }

    private void setStreet(String street) {
        this.street = street;
    }

    public String getZipcode() {
        return zipcode;
    }

    private void setZipcode(String zipcode) {
        this.zipcode = zipcode;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Address)) return false;
        Address address = (Address) o;
        return Objects.equals(getCity(), address.getCity()) && Objects.equals(getStreet(), address.getStreet()) && Objects.equals(getZipcode(), address.getZipcode());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getCity(), getStreet(), getZipcode());
    }

}

src/main/java/jpql/domain/Product.java

Product.java
package jpql.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Product {
    public Product() {
    }

    @Id @GeneratedValue
    private Long id;

    private String name;

    private int price;

    private int stockAmount;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    public int getStockAmount() {
        return stockAmount;
    }

    public void setStockAmount(int stockAmount) {
        this.stockAmount = stockAmount;
    }
} 	

src/main/java/jpql/JpqlMain.java

JpqlMain.java
package jpql;

import jpql.domain.*;

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

public class JpqlMain {
    //psvm 단축키로 생성 가능
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpql");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        tx.begin(); // [트랜잭션] 시작

        try{
		

        }catch (Exception e){
            e.printStackTrace();
            tx.rollback();
        }finally {
            em.close();
        }
        emf.close();
    }

}

참고#