객체지향 쿼리 언어 2 - 중급 문법 2
자바 ORM 표준 JPA 프로그래밍 - 기본편 수업을 듣고 정리한 내용입니다.
📚 3. JPQL - 다향성 쿼리
✔️ 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
JPQL
의count(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();
실행 결과
Member
테이블에서member.id
를memberId
에 저장- 테이블에서
member
의ID
를memberId
에서 조회한다.
📖 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
라는 외래 키와 매핑되어 있다.
그러므로 다음과 같은 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
를 보면 Member
와 Team
간에 묵시적 조인이 일어날 것 같지만 MEMBER
테이블이 team_id
외래 키를 가지고 있으므로 묵시적 조인은 일어나지 않는다.
물론 m.team.name
을 호출하면 묵시적 조인이 일어난다.
따라서 m.team
을 사용하든 m.team.id
를 사용하든 생성되는 SQL은 같다.
예제
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();
실행 결과
📚 5. Named 쿼리 - 정적 쿼리
JPQL
쿼리는 크게 동적 쿼리와 정적 쿼리로 나눌 수 있다.
- 동적 쿼리 :
em.createQuery("select ..")
처럼 JPQL
을 문자로 완성해도 직접 넘기는 것을 동적 쿼리라 한다. 런타임에 특정 조건에 따라 JPQL
을 동적으로 구성할 수 있다.
- 정적 쿼리 : 미리 정의한 쿼리에 이름을 부여해서 필요할 때 사용할 수 있는데 이것을
Named 쿼리
라 한다. 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();
실행 결과
그런데 Memeber
에서
@Entity
@NamedQuery(
name = "Member.findByUsername",
query = "select m from MemberQQQ m where m.username = :username"
)
public class Member {
와 같이 테이블 이름을 잘못 지정하였을 경우
실행 결과
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.xml
은JPA
가 기본 매핑파일로 인식해서 별도의 설정을 하지 않아도 된다.- 이름이나 위치가 다르면 설정을 추가해야 한다.
- 예제에서는 매핑 파일 이름이
ormMember.xml
이므로persistence.xml
에 설정 정보를 추가했다.
✔️ 환경에 따른 설정
만약 XML
과 어노테이션에 같은 설정이 있으면 XML
이 우선권을 가진다.
예를 들어 같은 이름의 Named
쿼리가 있으면 XML
에 정의된 것이 사용된다.
따라서 애플리케이션이 운영 환경에 따라 다른 쿼리를 실행해야 한다면 각 환경에 맞춘 XML
을 준비해두고 XML
만 변경해서 배포하면 된다.
📚 6. 벌크 연산
- update문, delete문의 조합이라고 생각해도 된다.
- 엔티티를 수정하려면 영속성 컨텍스트의 변경 감지 기능이나 병합을 사용
- 삭제하려면
EntityManager.remove()
메소드를 사용한다.
- 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();
실행 결과
- 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원이 출력된다.
✔️ 그림을 통한 문제점 분석
벌크 연산 직전
- 위의 그림은 벌크 연산 직전의 상황을 나타낸다.
상품A
를 조회했으므로 가격이 1000원인상품A
가 영속성 컨텍스트에 관리된다.
벌크 연산 수행 후
- 위의 그림을 보자. 벌크 연산은 영속성 컨텍스트를 통하지 않고 데이터베이스에 직접 쿼리한다.
- 영속성 컨텍스트에 있는
상품A
와 데이터베이스에 있는상품A
의 가격이 다를 수 있다. - 따라서 벌크 연산은 주의해서 사용해야 한다.
📖 B. 벌크 연산 문제점 해결방안
✔️ em.refresh()
사용
em.refresh(productA); // 데이터베이스에서 상품A를 다시 조회한다.
벌크 연산을 수행한 직후에 정확한 상품A
엔티티를 사용해야 한다면 em.refresh()
를 사용해서 데이터베이스에서 상품A
를 다시 조회하면 된다.
✏️ 벌크 연산 문제점 해결방안
(1) 벌크 연산 먼저 실행
(2) 벌크 연산 수행 후 영속성 컨텍스트 초기화
✔️ 벌크 연산 먼저 실행
가장 실용적인 해결책은 벌크 연산을 가장 먼저 실행하는 것이다.
ex)
- 벌크 연산을 먼저 실행하고 나서
상품A
를 조회하면 벌크 연산으로 이미 변경된 상품A를 조회하게 된다. - 이 방법은
JPA
와JDBC
를 함께 사용할 때도 유용하다.
✔️ 벌크 연산 수행 후 영속성 컨텍스트 초기화
벌크 연산을 수행한 직후에 바로 영속성 컨텍스트를 초기화해서 영속성 컨텍스트에 남아 있는 엔티티를 제거하는 것도 좋은 방법이다.
그렇지 않으면 엔티티를 조회할 때 영속성 컨텍스트에 남아 있는 엔티티를 조회할 수 있는데 이 엔티티에는 벌크 연산이 적용되지 않는다.
➡️ 영속성 컨텍스트를 초기화하면 이후 엔티티를 조회할 때 벌크 연산이 적용된 데이터베이스에서 엔티티를 조회한다.
예시
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();
실행 결과
int resultCount = em.createQuery("update Member m set m.age = 20")
.executeUpdate();
가 호출되기 전까지 (쿼리문 호출되기 전에는)
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으로 저장되어 있다.
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차 캐시를 무시하고 데이터베이스에 직접 실행한다.
- 따라서 영속성 컨텍스트와 데이터베이스 간에 데이터 차이가 발생할 수 있으므로 주의해서 사용해야 한다.
- 벌크 연산을 먼저 실행하자!
- 벌크 연산 수행 후 영속성 컨텍스트를 초기화 하자!
Author And Source
이 문제에 관하여(객체지향 쿼리 언어 2 - 중급 문법 2), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@chang626/객체지향-쿼리-언어-2-중급-문법-2저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)