코딩공작소
JPA 프로그래밍 학습 정리 (7) - 객체지향 쿼리언어 (2) 본문
https://taehoon9393.tistory.com/370
JPA 프로그래밍 학습 정리 (7) - 객체지향 쿼리언어 (1)
JPQL (Java Persistence Query Language) 테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리이며, SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는 특성을 가지고 있다. //쿼리 생성 String jpql =
taehoon9393.tistory.com
QueryDSL
: 데이터를 조회하는 데 기능이 특화되어 있다.
✔️ 해당 기능을 사용하기 위해서는 의존성과 라이브러리를 추가 해야한다.
pom.xml
<dependencies>
<!-- QueryDSL JPA -->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>4.4.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>4.4.0</version>
</dependency>
</dependencies>
version 및 scope는 알아서 관리하면 된다.
+ 요즘엔 chatGPT를 활용하면 이런 의존성과 라이브러리 코드는 쉽게 쉽게 짜주니까 활용하면 좋을 것 같다.
- querydsl-jpa : 라이브러리
- querydsl-apt : 쿼리 타입(Q)을 생성할 때 필요한 라이브러리
<build>
<plugins>
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
엔티티를 기반으로 쿼리 타입이라는 쿼리용 클래스를 생성해야 한다. 해당 플러그인을 pom.xml에 추가해줘야 한다.
mvn compile 명령어를 콘솔에 입력해주면 outputDirectory에 지정한 위치에 Q로 시작하는 쿼리타입들이 생성된다.
1
2
3
4
5
6
7
8
9
10
11
12
|
public void queryDSL() {
EntityManager em = emf.createEntityManager();
JPAQuery query = new JPAQuery(em); //em 주입 해줘야 함
QMember qMember = new QMember("m"); //생성되는 JPQL의 별칭
List<Member> members =
query.from(qMember)
.where(qMember.name.eq("A"))
.orderBy(qMember.name.desc())
.list(qMember);
}
|
cs |
JPAQuery 객체를 생성하고 엔티티 매니저를 생성자에 넘겨줘야 한다. 그리고 쿼리 타입(Q)를 생성하고 별칭을 준다.
쿼리 타입은 기본적으로 기본 인스턴스를 보관하고 있다.
1
2
3
4
5
|
public class QMember extends EntityPathBase<MEMBER> {
public static final QMember member = new QMember("member1");
...
}
|
cs |
하지만, 같은 엔티티를 조인하거나 같은 엔티티를 서브쿼리에 사용해야 한다면 별칭을 직접 지정해서 사용해야 한다.
QMember qMember = new QMember("a"); // 직접 지정
QMember qMember = Qmember.member; // 기본 인스턴스 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import static jpabook.jpashop.domain.QMember.member; // Basic instance
public void basic(){
EntityManager em = emf.createEntityManager();
JPAQuery query = new JPAQuery(em);
List<Member> members =
query.from(member)
.where(member.name.eq("a"))
.orderBy(member.name.desc())
.list(member); // 조회할 프로젝션 지정
}
|
cs |
where절에는 and나 or를 사용할 수 있다. 그리고 결과를 조회하기 위해 함수를 사용한다.
- list() : 결과가 하나 이상일 때, 결과가 없으면 빈 컬렉션을 반환
- uniqueResult() : 조회 결과가 한건일때, 결과가 없으면 null을 반환하고, 하나 이상이면 예외가 발생한다.
- singleResult() : uniqueResult()와 같지만 결과가 하나 이상이면 처음 데이터를 반환한다
페이징과 정렬
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
QItem item = QItem.item;
qeuery.from(item)
.where(item.price.ge(20000))
.orderBy(item.price.desc(), item.stockQuantoty.asc())
.offset(10).limit(20)
.list(item);
//com.mysema.query.QueryModifiers를 사용할 때,
QueryModifiers = queryModifiers = new QueryModifiers(20L, 10L); //limit, offset
List<Item> list =
query.from(item)
.restrict(queryModifiers)
.list(item)
// 실제 페이징 처리 후 검색된 전체 데이터 수를 알아야 할 때,
SerachResults<Item> result =
query.from(item)
.where(item.price.gt(10000))
.offset(10).limit(20)
.listResults(item);
long total = result.getTotal(); // 검색된 전체 데이터 수
long limit = result.getLimit();
long offset = result.getOffset();
List<item> results = result.getResuls(); // 조회된 데이터
|
cs |
그룹
query.from(item)
.groupBy(item.price)
.having(item.price.gt(1000))
.list(item);
조인
- join(조인대상, 별칭으로 사용할 쿼리 타입)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
query.from(order)
.join(order.member, member)
.leftJoin(order.orderItems, orderItem)
.list(order);
//on 절
query.from(order)
.leftJoin(order.orderItems, orderItem)
.on(orderItem.count.gt(2))
.list(order);
//fetch join
query.from(order)
.innerJoin(order.member, member).fetch()
.leftJoin(order.orderItems, orderItem).fetch()
.list(order);
//세타 조인
query.from(order, member)
.where(order.member.eq(member))
.list(order);
|
cs |
서브 쿼리
com.mysema.query.jpa.JPASubQuery를 생성해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// 한 건
query.from(item)
.where(item.price.eq(
new JPASubQuery().from(itemSub).unique(itemSub.price.max())
))
.list(item);
// 여러 건
query.from(item)
.where(item.in(
new JPASubQuery().from(itemSub)
.where(item.name.eq(itemSub.name))
.list(itemSub)
))
.list(item);
|
cs |
프로젝션과 결과 반환
프로젝션 대상이 하나
List<String> result = query.from(item).list(item.name);
여러 컬럼 반환과 튜플
com.mysema.query.Tuple 사용한다. 결과 조회는 tuple.get() 사용
List<Tuple> result = query.from(item).list(list.name, list.price);
List<Tuple> result = query.from(item).list(new QTuple(item.name, item.price));
for(Tuple t : result) {
t.get(item.name);
t.get(item.price);
}
Bean 생성
쿼리 결과를 특정 객체로 받고 싶을 때 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public class ItemDTO {
private String username;
private int price;
public ItemDTO(){}
public ItemDTO(String username, int price){
this.username = username;
this.price = price;
}
//getter, setter
//,,,
}
|
cs |
- 프로퍼티 접근( setter를 사용해서 값을 채움)
- 필드 직접 접근
- 생성자 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
//property
List<ItemDTO> result = query.from(item).list(
Projections.bean(ItemDTO.class, item.name.as("username"), item.price);
);
//field
List<ItemDTO> result = query.from(item).list(
Projections.fields(ItemDTO.class, item.name.as("username"), item.price);
);
//contructor
List<ItemDTO> result = query.from(item).list(
Projections.constructor(ItemDTO.class, item.name, item.price);
);
|
cs |
DISTINCT
query.distinct().from(item)....
수정, 삭제 배치 쿼리
영속성 컨텍스트를 무시하고 데이터베이스를 직접 쿼리한다!
1
2
3
4
5
6
7
8
9
10
|
// update
JPAUpdateClause updateClause = new JPAUpdateClause(em, item);
long count = updateClause.where(item.name.eq("A"))
.set(item.price, item.price.add(100))
.execute();
// delete
JPADeleteClause deleteClause = new JPADeleteClause(em, item);
long count = deleteClause.where(item.name.eq("a"))
.execute();
|
cs |
동적 쿼리
com.mysema.query.BooleanBuilder를 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
SearchParam param = new SearchParam();
param.setName("a");
param.price(1000);
QItem item = QItem.item;
BooleanBuilder builder = new BooleanBuilder();
if(StringUtils.hasText(param.getName())){
builder.and(item.name.contains(param.getName()));
}
if(param.getPrice() != null){
builder.and(item.price.gt(param.getPrice()));
}
List<Item> result = query.from(item)
.where(builder)
.list(item);
|
cs |
메소드 위임(Delegate methods)
검색 조건을 직접 정의할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class ItemExpression{
@QueryDelegate(Item.class)
public static BooleanExpression isExpensive(QItem item, Integer price){
return item.price.gt(price);
}
}
public class QItem extends EntityPathBase<Item> {
...
public com.mysema.query.types.expr.BooleanExpression
isExpensive(Integer price) {
return ItemExpression.isExpensive(this, price);
}
}
|
cs |
메소드 위임 기능을 사용하려면 정적 메소드를 만들고 어노테이션 속성으로 적용할 엔티티를 지정한다.
query.from(item).where(item.isExpensive(30000)).list(item);
쿼리 타입에 기능이 추가 되어있으며 위의 예시와 같이 메소드 위임 기능을 사용한다.
QueryDSL 정리
문자가 아닌 안전하게 쿼리를 작성하고 복잡한 동적 쿼리를 해결해주는 특징을 가지고 있다.
네이티브 SQL
때로는 특정 데이터 베이스에 종속적인 기능이 필요하다. SQL을 직접 사용할 수 있는 기능을 제공하며 엔티티를 조회할 수 있고 JPA가 지원하는 영속성 컨텍스트의 기능을 그대로 사용할 수 있다.
엔티티 조회
1
2
3
4
5
6
7
8
|
String sql =
"SELECT ID, AGE, NAME, TEAM_ID " +
"FROM MEMBER WHERE AGE > ?";
Query nativeQuery = em.createNativeQuery(sql, Member.class)
.setParamter(1, 20);
List<Member> resultList = nativeQuery.getResultList();
|
cs |
조회한 엔티티도 영속성 컨텍스트에서 관리된다.
값 조회
1
2
3
4
5
6
7
|
String sql =
"SELECT ID, AGE, NAME, TEAM_ID " +
"FROM MEMBER WHERE AGE > ?";
Query nativeQuery = em.createNativeQuery(sql).setParameter(1,10);
List<Object[]> resultList = nativeQuery.getResultList();
|
cs |
단순 값으로 조회하기 때문에 영속성 컨텍스트가 관리하지 않는다.
결과 매핑 사용
1
2
3
4
5
6
7
8
9
10
|
Query nativeQuery = em.createNativeQuery(sql, "memberWithOrderCount");
@Entity
@SqlResultSetMapping(name = "memberWithOrderCount",
entities = {@EntityResult(entityClass = Member.class)},
columns = {@ColumnResult(name = "ORDER_COUNT")}
)
public class Member {...}
|
cs |
@FieldResult, @ColumnResult를 통해 필드 매핑도 할 수 있다.
- @SqlResultSetMapping
- @EntityResult
- @FieldResult
- @ColumnResult
1
2
3
4
5
6
7
8
|
@SqlResultSetMapping(name = "A",
entities = {
@EntityResult(entityClass = cm.acme.Order.class, fields = {
@FieldResult(name = "id", column = "order_id"),
@FieldResult(name = "quantity", column = "order_quantity"),
@FieldResult(name = "item", column = "order_item")})},
columns = {
@ColumnResult(name = "item_name")}
|
cs |
Named 네이티브 SQL
네이티브 SQL에서도 정적 SQL을 작성할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@Entity
@NamedNativeQuery(
name = "Member.memberSQL",
query = "SELECT ID, AGE, NAME, TEAM_ID" +
"FROM MEMBER WHERE AGE > ?",
resultClass = Member.class
)
public class Member{...}
[사용]
TypedQuery<Member> nativeQuery =
em.createNamedQuery("Member.memberSQL", Member.class)
.setParameter(1,20);
|
cs |
네이티브 SQL에서도 결과 매핑을 사용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@Entity
@SqlResultSetMapping(name = "memberWithOrderCount" ,
entities = {@EntityResult(entityClass = Member.class)},
columns = {@ColumnResult(name = "ORDER_COUNT")}
)
@NamedNativeQuery(
name = "Member.memberWithOrderCount",
query = "SELECT ... FROM ... WHERE ...",
resultSetMapping = "memberWithOrderCount"
)
public class Member{...}
[사용]
List<Object[]> resultList =
em.createNamedQuery("Member.memberWithOrderCount")
.getResultList();
|
cs |
네이티브 SQL XML에 정의
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<entity-mappings ...>
<named-native-query name = "Member.memberWithOrderCountXml"
result-set-mapping = "memberWithOrderCountResultMap" >
<query><CDATA[
SELECT ...
FROM ...
]></qeury>
</named-native-query>
<sql-result-set-mapping name = "memberWithOrderCountResultMap">
<entity-result entity-class = "jpabook.domain.Member" />
<column-result name = "ORDER_COUNT" />
</sql-result-set-mapping>
</entity-mappings>
[사용]
List<Object[]> resultList =
em.createNamedQuery("Member.memberWithOrderCount")
.getResultList();
|
cs |
<named-native-query> → <sql-result-set-mapping> 순으로 정의해야 한다.
네이티브 SQL도 페이징 처리가 가능하다.
1
2
3
|
em.createNativeQuery(sql, Member.class)
.setFirstResult(10)
.setMaxResults(20)
|
cs |
1) 가능하면 표준 JPQL을 사용하고
2) 기능이 부족하면 차선책으로 하이버네이트 같은 JPA 구현체가 제공하는 기능을 사용하자.
3) 그래도 안되면 마지막 방법으로 네이티브 SQL을 사용하자.
(네이티브 SQL을 자주 사용하면 특정 데이터베이스에 종속적인 쿼리가 증가해서 이식성이 떨어진다)
객체지향 쿼리 심화
벌크 연산
여러 건을 한 번에 수정하거나 삭제하는 벌크 연산이 존재한다. (수백 개 이상의 엔티티를 처리할 때 오래걸리니까)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
//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)
.executeUpdate();
//delete SQL
String qlString =
"delete from Product p " +
"where p.price < :price";
int resultCount = em.createQuery(qlString)
.setParameter("price", 100)
.executeUpdate();
|
cs |
벌크 연산은 executeUpdate() 메소드를 사용한다.
벌크 연산을 사용할 때는 벌크 연산이 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다는 점에 주의해야 한다.
즉 영속성 컨텍스트와 데이터베이스 간의 싱크가 안맞아 의도치 않는 결과를 불러일으킬 수 있다.
해결방법
- em.refresh() 사용
벌크 연산 직후에 정확한 엔티티를 사용해야한다면 em.refresh()를 통해 다시 조회한다 - 벌크 연산 먼저 실행
벌크 연산을 가장 먼저 실행한다. - 벌크 연산 직후에 영속성 컨텍스트를 초기화해서 엔티티를 제거한다.
→ 가능하면 벌크 연산을 가장 먼저 수행하고 상황에 따라서 영속성 컨텍스트를 초기화하자!
영속성 컨텍스트와 JPQL
JPQL의 조회 대상은 엔티티, 임베디드 타입, 값 타입 같이 다양한 종류가 있다. 이 중 조회한 엔티티만 영속성 컨텍스트가 관리한다.
만약, 조회한 엔티티가 영속성 컨텍스트에 이미 있으면 JPQL로 데이터베이스를 조회한 결과를 버리고 영속성 컨텍스트에 있던 엔티티를 반환한다. 영속성 컨텍스트는 영속 상태인 엔티티의 동일성을 보장한다.
find() vs JPQL
em.find() 메소드는 영속성 컨텍스트에서 먼저 찾고 없으면 데이터베이스에서 찾는다. 이것은 1차 캐시이며 성능상의 이점을 갖는다.
JPQL은 항상 데이터베이스에서 SQL을 실행해서 결과를 조회한다.
JPQL과 플러시 모드
플러시는 영속성 컨텍스트의 변경 내역을 데이터베이스에 동기화하는 것이다.
JPA는 FlushModeType.AUTO가 기본값이므로 트랜잭션 커밋 직전이나 쿼리 실행 직전에 자동으로 플러시를 호출한다.
JPQL은 데이터베이스에서 먼저 조회하기 때문에, 실행 전에 영속성 컨텍스트의 내용을 데이터베이스에 반영해야 한다.
이때, 기본적으로 AUTO모드이기 때문에 쿼리 실행 직전 자동으로 영속성 컨텍스트가 플러시 된다.
COMMIT 모드라면 ?
1
2
3
4
5
6
7
8
9
10
11
12
13
|
em.setFlushMode(FlushModeType.COMMIT); //커밋 시에만 플러시
//가격을 1000->2000으로 변경
product.setPrice(2000);
//1. em.flush() 직접 호출
//2000원인 상품 조회
Product product2 =
em.createQuery("select p from Product p where p.price = 2000",
Product.class)
.setFlushMode(FlushModeType.AUTO) //2. setFlushMode() 설정
.getSingleResult();
|
cs |
커밋모드이면 쿼리 직전에 플러시가 되지 않으므로 1. em.flush() 처럼 직접 플러시를 호출하거나 2. setFlushMode() 를 통해 AUTO로 쿼리 직전에 플러시가 실행되도록 해야 한다.
그럼 왜 COMMIT 모드를 사용할까?
쿼리시 발생하는 플러시 횟수가 너무 많은 경우 성능을 최적화하기 위해 사용한다.
📌 별도의 JDBC 호출은 플러시 모드를 AUTO 설정해도 플러시가 일어나지 않으므로 em.flush() 직접 호출을 통해 영속성 컨텍스트와 데이터베이스를 동기화 해줘야 한다.
'어플리케이션개발 > JPA' 카테고리의 다른 글
JPA 프로그래밍 학습 정리 (8) - 웹 어플리케이션 제작(2) (0) | 2024.02.16 |
---|---|
JPA 프로그래밍 학습 정리 (8) - 웹 어플리케이션 제작(1) (0) | 2024.02.14 |
JPA 프로그래밍 학습 정리 (7) - 객체지향 쿼리언어 (1) (1) | 2024.01.29 |
JPA 프로그래밍 학습 정리 (6) - 값 타입 (1) | 2024.01.27 |
JPA 프로그래밍 학습 정리 (5) - 프록시와 연관관계 관리 (0) | 2024.01.22 |