객체지향 쿼리 언어 2 - 중급 문법 2

LeeKyoungChang·2022년 3월 17일
0
post-thumbnail

자바 ORM 표준 JPA 프로그래밍 - 기본편 수업을 듣고 정리한 내용입니다.

 

📚 3. JPQL - 다향성 쿼리

스크린샷 2022-03-17 오후 3 52 58

✔️ Type

조회 대상을 특정 자식으로 한정할 수 있다.

ex) Item 중에 Book, Movie를 조회해라

JPQL

select i from Item i
where type(i) IN (Book, Movie)

SQL

select i from i
where i.DTYPE in (‘B’, ‘M’)

 

✔️ TREAT(JPA 2.1)

  • 자바의 타입 캐스팅과 유사하다.
  • 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용한다.
  • FROM, WHERE, SELECT (하이버네이트 지원) 사용한다.

ex) 부모인 Item과 자식 Book이 있다.

JPQL

select i from Item i
where treat(i as Book).auther = ‘kim’

SQL

select i.* from Item i
where i.DTYPE = ‘B’ and i.auther = ‘kim’

 

📚 4. JPQL - 엔티티 직접 사용

📖 A. 기본 키 값

객체 인스턴스는 참조 값으로 식별하고 테이블 로우는 기본 키 값으로 식별한다.
따라서 JPQL에서 엔티티 객체를 직접 사용하면 SQL에서는 해당 엔티티의 기본 키 값을 사용한다.

select count(m.id) from Member m   // 엔티티의 아이디를 사용
select count(m) from Member m      // 엔티티를 직접 사용

두 번째의 count(m)을 보면 엔티티의 별칭을 직접 넘겨주었다. 이렇게 엔티티를 직접 사용하면 JPQL이 SQL로 변환될 때 해당 엔티티의 기본 키를 사용한다.

따라서 다음 실제 실행된 SQL은 둘 다 같다.

select count(m.id) as cnt
from Member m
  • JPQLcount(m)이 SQL에서 count(m.id)로 변환된 것을 확인할 수 있다.

이번에는 아래와 같이 엔티티를 파라미터로 직접 받아보자!

String qlString = "select m from Member m where m = :member";
List resultList = em.createQuery(qlString)
                     .setParameter("member", member)
                     .getResultList();

실행된 SQL은 이와 같다.

select m.*
from Member m
where m.id=?

JPQL과 SQL을 비교해보면 JPQL에서 where m = :member로 엔티티를 직접 사용하는 부분이 SQL에서 where m.id=?로 기본 키 값을 사용하도록 변환된 것을 확인할 수 있다.

물론, 아래와 같이 식별자 값을 직접 사용해도 결과는 같다.

String qlString = "select m from Member m where m.id = :memberId";
List resultList = em.createQuery(qlString)
                     .setParameter("memberId", 4L)
                     .getResultList();

 

예제

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

            String query = "select m from Member m where m.id = :memberId";

            Member findMember = em.createQuery(query, Member.class)
                    .setParameter("memberId",member.getId())
                    .getSingleResult();

//            System.out.println("result.size() = " + result.size());

            System.out.println("findMember = " + findMember);

            tx.commit();

 

실행 결과

스크린샷 2022-03-31 오후 1 07 37
  • Member테이블에서 member.idmemberId에 저장
  • 테이블에서 memberIDmemberId에서 조회한다.

 

📖 B. 외래 키 값

이번에는 외래 키를 사용하는 예를 보자. 아래 예제는 특정 팀에 소속된 회원을 찾는다.

Team team = em.find(Team.class, 1L);

String qlString = "select m from Member m where m.team = :team";
List resultList = em.createQuery(qlString)
                    .setParameter("team", team)
                    .getResultList();

기본 키 값이 1L인 팀 엔티티를 파라미터로 사용하고 있다. m.team은 현재 team_id라는 외래 키와 매핑되어 있다.

스크린샷 2022-03-17 오후 4 27 48

그러므로 다음과 같은 SQL이 실행된다.

select m.*
from Member m
where m.team_id=? (팀 파라미터의 ID 값)

 

엔티티 대신 아래 예제와 같이 식별자 값을 직접 사용할 수 있다.

String qlString = "select m from Member m where m.team.id = :teamId";
List resultList = em.createQuery(qlString)
                    .setParameter("teamId", 1L)
                    .getResultList();

m.team.id를 보면 MemberTeam 간에 묵시적 조인이 일어날 것 같지만 MEMBER 테이블이 team_id 외래 키를 가지고 있으므로 묵시적 조인은 일어나지 않는다.
물론 m.team.name을 호출하면 묵시적 조인이 일어난다.
따라서 m.team을 사용하든 m.team.id를 사용하든 생성되는 SQL은 같다.

 

예제

스크린샷 2022-03-17 오후 4 27 48
            Team teamA = new Team();
            teamA.setName("teamA");
            em.persist(teamA);

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

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

		    // 이렇게 할시, member 클래스안에 team이라는 변수를 뜻한다.(name은 TEAM_ID이다.)
            String query = "select m from Member m where m.team = :team";

            List<Member> members = em.createQuery(query, Member.class)
                    .setParameter("team", teamA)
                    .getResultList();

            for (Member member1 : members) {
                System.out.println("member1 = " + member1);
            }

            tx.commit();

 

실행 결과

스크린샷 2022-03-31 오후 1 21 11

 

📚 5. Named 쿼리 - 정적 쿼리

JPQL 쿼리는 크게 동적 쿼리와 정적 쿼리로 나눌 수 있다.

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

Named 쿼리는 애플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱해 둔다. 따라서 오류를 빨리 확인할 수 있고, 사용하는 시점에는 파싱된 결과를 재사용하므로 성능상 이점도 있다. (초기화 후 재사용)
그리고 Named 쿼리는 변하지 않는 정적 SQL이 생성되므로 데이터베이스의 조회 성능 최적화에도 도움이 된다.

Named 쿼리는 @NamedQuery 어노테이션을 사용해서 자바 코드에 작성하거나 또는 XML 문서에 작성할 수 있다.

 

📖 A. Named 쿼리를 어노테이션에 정의 (중요)

Named 쿼리 : 이름 그대로 쿼리에 이름을 부여해서 사용하는 방법이다.

  • 애플리케이션 로딩 시점에 초기화 후 재사용한다.
  • 애플리케이션 로딩 시점에 쿼리를 검증한다.

@NamedQuery 어노테이션을 사용하는 예시

@Entity
@NamedQuery(
    name = "Member.findByUsername",
    query = "select m from Member m where m.username = :username")
public class Member {
    ...
}
  • @NamedQuery.name에 쿼리 이름을 부여한다.
  • @NamedQuery.query에 사용할 쿼리를 입력한다.

 

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

Named 쿼리를 사용할 때는 위의 예제와 같이 em.createNamedQuery() 메소드에 Named 쿼리 이름을 입력하면 된다.

 

💡 참고
Named 쿼리는 영속성 유닛 단위로 관리되므로 충돌을 방지하기 위해 엔티티 이름을 앞에 주었다. 그리고 엔티티 이름이 앞에 있으면 관리하기가 쉽다.

 

하나의 엔티티에 2개 이상의 Named 쿼리를 정의하려면 아래 예제와 같이 @NamedQueries 어노테이션을 사용하면 된다.

@Entity
@NamedQueries({
    @NamedQuery(
        name = "Member.findByUsername",
        query = "select m from Member m where m.username = :username"),
    @NamedQuery(
        name = "Member.count",
        query = "select count(m) from Member m")
})
public class Member {...}

 

✔️ @NamedQuery 어노테이션

@Target({TYPE})
public @interface NamedQuery {

    String name();     // Named 쿼리 이름 (필수)
    String query();    // JPQL 정의 (필수)
    LockModeType lockMode() default NONE;   // 쿼리 실행 시 락모드를
                                            // 설정할 수 있다.
    QueryHint[] hints() default {};   // JPA 구현체에 쿼리 힌트를 줄 수 있다.
  • lockMode : 쿼리 실행 시 락을 건다.
  • hints : 여기서 힌트는 SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트다. 예를 들어 2차 캐시를 다룰 때 사용한다.

 

예시
Member

@Entity
@NamedQuery(
        name = "Member.findByUsername",
        query = "select m from Member m where m.username = :username"
)
public class Member {}

 

Main

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

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

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

            for (Member member1 : resultList) {
                System.out.println("member1 = " + member1);
            }
            
            tx.commit();

 

실행 결과

스크린샷 2022-03-31 오후 3 23 35

 

그런데 Memeber에서

@Entity
@NamedQuery(
        name = "Member.findByUsername",
        query = "select m from MemberQQQ m where m.username = :username"
)
public class Member {

와 같이 테이블 이름을 잘못 지정하였을 경우

실행 결과

스크린샷 2022-03-31 오후 3 27 55

sql문이 문자열로 입력되다 테이블 이름이 잘못되어 쿼리 오류가 발생한다.

➡️ 이와 같이 Named 쿼리를 애노테이션에 정의할 경우 왠만한 오류를 다 잡아 준다.

 

📖 B. Named 쿼리를 XML에 정의

  • JPA에서 어노테이션으로 작성할 수 있는 것은 XML로도 작성할 수 있다.
  • 물론 어노테이션을 사용하는 것이 직관적이고 편리하다.
  • 하지만 Named 쿼리를 작성할 때는 XML을 사용하는 것이 더 편리하다.

자바 언어로 멀티라인 문자를 다루는 것은 상당히 귀찮은 일이다. (어노테이션을 사용해도 마찬가지다.)

"select " +
    "case t.name when '팀A' then '인센티브110%' " +
    "            when '팀B' then '인센티브120%' " +
    "            else '인센티브150%' end " +
"from Team t";

 

자바에서 이런 불편함을 해결하려면 아래 예제와 같이 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>select count(m) from Member m</query>
  </named-query>

</entity-mappings>

 

💡 참고
XML에서 &, <, >XML 예약문자다. 대신에 &, <, >를 사용해야 한다. <![CDATA[]]>를 사용하면 그 사이에 문장을 그대로 출력하므로 예약문자도 사용할 수 있다.

 

그리고 정의한 ormMember.xml을 인식하도록 META-INF/persistence.xml에 다음 코드를 추가해야 한다.

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

 

💡 참고

  • META-INF/orm.xmlJPA가 기본 매핑파일로 인식해서 별도의 설정을 하지 않아도 된다.
  • 이름이나 위치가 다르면 설정을 추가해야 한다.
  • 예제에서는 매핑 파일 이름이 ormMember.xml이므로 persistence.xml에 설정 정보를 추가했다.

 

✔️ 환경에 따른 설정

만약 XML과 어노테이션에 같은 설정이 있으면 XML이 우선권을 가진다.
예를 들어 같은 이름의 Named 쿼리가 있으면 XML에 정의된 것이 사용된다.
따라서 애플리케이션이 운영 환경에 따라 다른 쿼리를 실행해야 한다면 각 환경에 맞춘 XML을 준비해두고 XML만 변경해서 배포하면 된다.

 

📚 6. 벌크 연산

  • update문, delete문의 조합이라고 생각해도 된다.
  • 엔티티를 수정하려면 영속성 컨텍스트의 변경 감지 기능이나 병합을 사용
  • 삭제하려면 EntityManager.remove() 메소드를 사용한다.

하지만 위 두 가지 방법으로는 수백 개 이상의 엔티티를 하나씩 처리하기에는 시간이 너무 오래 걸린다.
이럴 때 여러 건을 한 번에 수정하거나 삭제하는 벌크 연산을 사용하면 된다.

 

ex) 재고가 10개 미만인 모든 상품의 가격을 10% 상승시키려면?
JPA 변경 감지 기능으로 실행하려면 너무 많은 SQL 실행한다.

  • (1) 재고가 10개 미만인 상품을 리스트로 조회한다.
  • (2) 상품 엔티티의 가격을 10% 증가한다.
  • (3) 트랜잭션 커밋 시점에 변경감지가 동작한다.

➡️ 변경된 데이터가 100건이라면 100번의 UPDATE SQL을 실행해야 한다. (하나씩 다 바꾸는 것은 귀찮다.)

이때 벌크 연산을 사용하면 된다. 벌크 연산을 사용하면 한 번에 수정, 삭제를 할 수 있다.

String qlString = 
    "update Product p " +
    "set p.price = p.price * 1.1 " +
    "where p.stockAmount < :stockAmount";

int resultCount = em.createQuery(qlString)
                    .setParameter("stockAmount", 10)
                    .executeQuery();

executeUpdate()

  • 벌크 연산 메소드
  • 이 메소드는 벌크 연산으로 영향을 받은 엔티티 건수를 반환한다.

 

ex) 가격이 100원 미만인 상품을 삭제하는 코드다.

String qlString = 
    "delete from Product p " +
    "where p.price < :price";

int resultCount = em.createQuery(qlString)
                    .setParameter("price", 100)
                    .executeUpdate();
  • 삭제할 때도 executeUpdate()를 사용한다.

 

ex) 테이블 데이터들을 20살로 변경하기

            int resultCount = em.createQuery("update Member m set m.age = 20")
                    .executeUpdate();

            System.out.println("resultCount = " + resultCount);

            tx.commit();

 

실행 결과

스크린샷 2022-03-31 오후 3 48 37 스크린샷 2022-03-31 오후 3 49 48
  • Member 테이블에서 사람들의 나이는 전부 20살로 변경되었다.

 

💡 참고
JPA 표준은 아니지만 하이버네이트는 INSERT 벌크 연산도 지원한다.

String qlString =
   "insert into ProductTemp(id, name, price, stockAmount) "  +
   "select p.id, p.name, p.price, p.stockAmount from Product p " +
   "where p.price < :price";
   
   int resultCount = em.createQuery(qlString)
   .setParameter("price", 100)
   .executeUpdate();

 

📖 A. 벌크 연산의 주의점

벌크 연산을 사용할 때는 벌크 연산이 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다는 점에 주의해야 한다.
벌크 연산 시 어떤 문제가 발생할 수 있는지 아래 예제를 통해 알아보자. 데이터베이스에는 가격이 1000원인 상품A(productA)가 있다.

```java
// 상품A 조회(상품 A의 가격은 1000원이다.)  1)
Product productA =
    em.createQuery("select p from Product p where p.name = :name",
        Product.class)
        .setParameter("name", "productA")
        .getSingleResult();

// 출력 결과 : 1000
System.out.println("productA 수정 전 = " + productA.getPrice());

// 벌크 연산 수행으로 모든 상품 가격 10% 상승  2)
em.createQuery("update Product p set p.price = p.price * 1.1")
    .executeUpdate();

// 출력 결과 : 1000  3)
System.out.println("productA 수정 후 = " + productA.getPrice());

1) 가격이 1000원인 상품A를 조회했다. 조회된 상품A는 영속성 컨텍스트에서 관리된다.
2) 벌크 연산으로 모든 상품의 가격을 10% 상승시켰다. 따라서 상품A의 가격은 1100원이 되어야 한다.
3) 벌크 연산을 수행한 후에 상품A의 가격을 출력하면 기대했던 1100원이 아니라 1000원이 출력된다.

✔️ 그림을 통한 문제점 분석
벌크 연산 직전
스크린샷 2022-03-17 오후 5 52 57

  • 위의 그림은 벌크 연산 직전의 상황을 나타낸다.
  • 상품A를 조회했으므로 가격이 1000원인 상품A가 영속성 컨텍스트에 관리된다.

 

벌크 연산 수행 후

스크린샷 2022-03-17 오후 5 53 10
  • 위의 그림을 보자. 벌크 연산은 영속성 컨텍스트를 통하지 않고 데이터베이스에 직접 쿼리한다.
  • 영속성 컨텍스트에 있는 상품A와 데이터베이스에 있는 상품A의 가격이 다를 수 있다.
  • 따라서 벌크 연산은 주의해서 사용해야 한다.

 

📖 B. 벌크 연산 문제점 해결방안

✔️ em.refresh() 사용

em.refresh(productA);   // 데이터베이스에서 상품A를 다시 조회한다.

벌크 연산을 수행한 직후에 정확한 상품A 엔티티를 사용해야 한다면 em.refresh()를 사용해서 데이터베이스에서 상품A를 다시 조회하면 된다.

 

✏️ 벌크 연산 문제점 해결방안
(1) 벌크 연산 먼저 실행
(2) 벌크 연산 수행 후 영속성 컨텍스트 초기화

 

✔️ 벌크 연산 먼저 실행

가장 실용적인 해결책은 벌크 연산을 가장 먼저 실행하는 것이다.

ex)

  • 벌크 연산을 먼저 실행하고 나서 상품A를 조회하면 벌크 연산으로 이미 변경된 상품A를 조회하게 된다.
  • 이 방법은 JPAJDBC를 함께 사용할 때도 유용하다.

 

✔️ 벌크 연산 수행 후 영속성 컨텍스트 초기화

벌크 연산을 수행한 직후에 바로 영속성 컨텍스트를 초기화해서 영속성 컨텍스트에 남아 있는 엔티티를 제거하는 것도 좋은 방법이다.
그렇지 않으면 엔티티를 조회할 때 영속성 컨텍스트에 남아 있는 엔티티를 조회할 수 있는데 이 엔티티에는 벌크 연산이 적용되지 않는다.

➡️ 영속성 컨텍스트를 초기화하면 이후 엔티티를 조회할 때 벌크 연산이 적용된 데이터베이스에서 엔티티를 조회한다.

 

예시


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

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

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

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

            // FLUSH 자동 호출
            // flush는 commit, query를 호출할 때 자동 호출된다.
            // flush 되기 전까지 Member의 age는 0이 저장되어 있다.
            // 이로인해 영속성 컨텍스트에는 member의 age는 0이 저장되고 DB도 0이 저장된다.

            int resultCount = em.createQuery("update Member m set m.age = 20")
                    .executeUpdate();

            // 이때 createQuery update문으로 Database에 나이를 20으로 업데이트 하는 문장이 있다.
            // 이로써, Database는 20으로 업데이트 되지만, 영속성 컨텍스트에는 여전히 Member의 age는 0으로 저장되어 있다.

            System.out.println("resultCount = " + resultCount);

            System.out.println("member = " +member.getAge());
            System.out.println("member2 = " +member2.getAge());
            System.out.println("member3 = " +member3.getAge());

            // 그리고 영속성 컨텍스트 find 메서드를 호출해서 조회해도 여전히 0으로 저장된다.
            Member findMember = em.find(Member.class, member.getId());

            System.out.println("findMember = " + findMember);


            // 그냥 벌크 연산을 할 경우, db에만 반영된다.
            // 그러므로 벌크 연산을 사용한 후 바로, clear()메서드를 호출해야 한다.
            em.clear();


            tx.commit();

 

실행 결과
스크린샷 2022-03-31 오후 4 23 12

스크린샷 2022-03-31 오후 4 24 00

 

            int resultCount = em.createQuery("update Member m set m.age = 20")
                    .executeUpdate();

가 호출되기 전까지 (쿼리문 호출되기 전에는)

  • flushcommit, query를 호출할 때 자동 호출된다.
  • flush 되기 전까지 Memberage는 0이 저장되어 있다.
  • 이로인해 영속성 컨텍스트에는 memberage는 0이 저장되고 DB도 0이 저장된다.

 

            int resultCount = em.createQuery("update Member m set m.age = 20")
                    .executeUpdate();

가 호출된 후 (쿼리문 호출된 후)

  • 이때 createQuery update문으로 Database에 나이를 20으로 업데이트 하는 문장이 있다.
  • 이로써, Database는 20으로 업데이트 되지만, 영속성 컨텍스트에는 여전히 Memberage는 0으로 저장되어 있다.

 

Member findMember = em.find(Member.class, member.getId());
  • 그리고 영속성 컨텍스트 find 메서드를 호출해서 조회해도 여전히 0으로 저장된다.
  • 벌크 연산을 사용할 경우 DB에만 반영되기 때문이다.

 

그래서?

            int resultCount = em.createQuery("update Member m set m.age = 20")
                    .executeUpdate();
            em.clear();
  • 그러므로 벌크 연산을 사용한 후 바로, clear()메서드를 호출해야 한다.
  • 영속성 컨텍스트 캐시를 지워야 한다.

 

✏️ 정리

  • 벌크 연산은 영속성 컨텍스트와 2차 캐시를 무시하고 데이터베이스에 직접 실행한다.
  • 따라서 영속성 컨텍스트와 데이터베이스 간에 데이터 차이가 발생할 수 있으므로 주의해서 사용해야 한다.
  • 벌크 연산을 먼저 실행하자!
  • 벌크 연산 수행 후 영속성 컨텍스트를 초기화 하자!

 


참고 : https://velog.io/@jsj3282/51.-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-%EC%BF%BC%EB%A6%AC-%EC%96%B8%EC%96%B418

profile
"야, (오류 만났어?) 너두 (해결) 할 수 있어"

0개의 댓글