예제 소스코드(Google Drive)
https://drive.google.com/file/d/1YrsS9-sjHM4QSLhe-5GZsvcfVlvMI_Z9/view?usp=sharing
application.property 파일 수정해서 DB 연결해서 테스트해 보실 수 있습니다. (소스코드에서는 xxx처리)
JPQL을 이용해서 DTO를 반환???
JPA를 이용하는 경우, 특정 상황에서 Entity 전체가 아닌 일부 컬럼만을 가져오고 싶을 때,
@Query 어노테이션 내에 작성하는 JPQL에서 Java 객체를 생성하는 코드를 추가하는 방법을 사용할 수 있다.
Spring-data-jpa가 제공하는 JpaRepository<> 인터페이스를 이용할 것이고, 회원과 주문으로 구성된 간단한 예제이다.
회원(MemberEntity)이 존재하고, 회원과 일대다(1:M)으로 묶인 주문(OrderEntity)을 설정했다.
예제 테이블 명도 member_entity, order_entity로 정했다.
이 글에서 다룰 예제는 3가지이다.
- 1. MemberRepository에서 MemberDto를 바로 반환
- 2. 주문 정보를 반환하는데, 회원 정보 일부와 OrderDto를 포함한 객체 반환
- 3-1. 특정 회원에 대한 정보와, 해당 회원이 주문한 내역 List를 반환하는 객체 반환
- 3-2. JPQL를 사용하여 해결
- 3-3. EntityManager를 직접 사용하는 방법
예제에서 사용할 code, table
JPA - @Entity, DB 테이블의 실제 데이터
DTO
DTO code
(예제1) MemberDto
@Getter
@Setter
@NoArgsConstructor
@Builder
public class MemberDto {
private String name;
private int age;
private String phone;
public MemberDto(String name, int age, String phone) {
this.name = name;
this.age = age;
this.phone = phone;
}
public static MemberDto noData(){
return new MemberDto("",0,"");
}
}
(예제 2) OrderInfo
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class OrderInfo {
private Long memberId;
private String memberName;
private String phone;
private OrderDto order;
public OrderInfo(MemberEntity memberEntity, OrderEntity orderEntity) {
this.memberId = memberEntity.getMemberId();
this.memberName = memberEntity.getName();
this.phone = memberEntity.getPhone();
this.order = new OrderDto(orderEntity);
}
public static OrderInfo noData(){
return OrderInfo.builder()
.memberId(-1L)
.memberName("no-data")
.phone("no-data")
.order(OrderDto.noData())
.build();
}
}
(예제 3) OrderListDto
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class OrderListDto {
private Long memberId;
private String memberName;
private String phone;
private List<OrderDto> list;
public OrderListDto(MemberEntity memberEntity) {
this.memberId = memberEntity.getMemberId();
this.memberName = memberEntity.getName();
this.phone = memberEntity.getPhone();
}
}
(공통) OrderDto
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class OrderDto {
private Long orderId;
private String productCode;
private int amount;
public OrderDto(OrderEntity orderEntity) {
this.orderId = orderEntity.getOrderId();
this.productCode = orderEntity.getProductCode();
this.amount = orderEntity.getAmount();
}
public static OrderDto noData(){
return OrderDto.builder()
.orderId(-1L)
.productCode("no-data")
.amount(0)
.build();
}
}
예제
예제 1 : MemberDto 반환
@Repository
public interface MemberRepository extends JpaRepository<MemberEntity, Long> {
@Query(
"SELECT new com.example.springtest.dto.MemberDto(m.name, m.age, m.phone) " +
" FROM member_entity m " +
" WHERE m.name = :name "
)
Optional<MemberDto> findMemberDtoByName(@Param("name") String name);
// .. 생략 ..
}
new com.example.springtest.dto.MemberDto(m.name, m.age, m.phone)
위와 같은 방식으로 JPQL 내에서 new 키워드 DTO 객체를 생성하고,
아래 FROM 절에서 entity의 일부 값을 생성자의 매개변수로 사용한다.
예제 2 : OrderInfo반환
@Repository
public interface OrderRepository extends JpaRepository<OrderEntity, Long> {
@Query(
"SELECT new com.example.springtest.dto.OrderInfo(m, o) " +
" FROM member_entity m, order_entity o " +
" WHERE o.orderId = :orderId AND o.member.memberId = m.memberId "
)
Optional<OrderInfo> findOneOrder(@Param("orderId") Long orderId);
// .. 생략 ..
}
마찬가지로 JPQL 내에서 new 키워드를 이용해서 DTO 객체를 생성하고,
MemberEntity와 OrderEntity를 매개변수로 넣어주는 생성자를 따로 만들어서 DTO를 생성한다.
new com.example.springtest.dto.OrderInfo(m, o)
//OrderInfo 생성자
public OrderInfo(MemberEntity memberEntity, OrderEntity orderEntity) {
this.memberId = memberEntity.getMemberId();
this.memberName = memberEntity.getName();
this.phone = memberEntity.getPhone();
this.order = new OrderDto(orderEntity); // <-Order DTO 생성자 사용
}
//OrderDto 생성자
public OrderDto(OrderEntity orderEntity) {
this.orderId = orderEntity.getOrderId();
this.productCode = orderEntity.getProductCode();
this.amount = orderEntity.getAmount();
}
예제 3-1 : OrderList 반환
특정 회원의 주문 리스트를 모두 반환하는 상황이다.
이 경우에는 JPQL을 이용하기보다는 Member와 List <Order>를 분리해서 생각하는 게 좋다.
아래 Controller에서처럼 MemberEntity를 먼저 조회하고 해당 엔티티로 List<Order>를 조회하는 과정으로 진행한다.
Controller code
/**
* 한명의 회원의 회원 정보와 주문 내역 List를 뽑으려는 경우
*/
@GetMapping("/orders")
public ResponseEntity<OrderListDto> orderListTest(@RequestParam("nm") String memberName) {
// 1. 회원 먼저 조회
MemberEntity memberEntity = memberRepository.findByName(memberName)
.orElseThrow(() -> new RuntimeException("member not found"));
// 2. 회원 정보만 담은 OrderListDto 생성
OrderListDto orderListDto = new OrderListDto(memberEntity);
// 3. OrderList 조회
List<OrderDto> list = orderRepository.findAllByMember(memberEntity)//여기까지는 EntityList
.stream().map(OrderDto::new).collect(Collectors.toList());
// 4. OrderListDto에 세팅
orderListDto.setList(list);
return ResponseEntity.ok(orderListDto);
}
예제 3-2 : JPQL 사용
이 예제는 DB에 SQL을 날린 결과로 여러행의 row를 반환받아서, DTO에 매핑하는 방법을 다룬다.
우선, DB를 확인해 보자. 아래와 같은 SQL을 날리는 상황이다.
SELECT
m.member_id as memberId,
m.name as memberName,
m.phone as phone,
o.order_id as orderId,
o.product_code as productCode,
o.amount as amount
FROM
member_entity m,
order_entity o
WHERE
m.member_id = o.member_id
AND m.name = 'Bob';
쿼리의 결과는 아래와 같이 나올 것이다.
위의 결과를 JPA를 이용해서 불러오고, Java 객체인 DTO로 매핑하여 OrderListDto 객체를 만드는 과정을 확인해 보자.
1. DTO list 조회(JPQL)
아래 Repository 메서드와 JPQL을 이용해서 DTO List를 조회한 뒤 따로 데이터 처리를 한다.
@Repository
public interface OrderRepository extends JpaRepository<OrderEntity, Long> {
@Query(
value = "SELECT new com.example.springtest.dto.MemberOrderDto(m.memberId, m.name, m.phone, o.orderId, o.productCode, o.amount) " +
" FROM member_entity m, order_entity o " +
" WHERE m.memberId = o.member.memberId AND m.name = :memberName"
)
List<MemberOrderDto> findOrdersByMemberNameJpql(@Param("memberName") String memberName);
}
2. 데이터 처리 후 반환 (Java Code)
@GetMapping("/order/jpql/{memberName}")
public ResponseEntity<OrderListDto> findOrdersByMemberNameNJpql(@PathVariable String memberName) {
List<MemberOrderDto> resultList = orderRepository.findOrdersByMemberNameJpql(memberName);
//예외 처리
if(resultList.isEmpty()) {
return ResponseEntity.noContent().build();
}
//결과 데이터 처리
OrderListDto orderListDto = new OrderListDto();
List<OrderDto> orderList = new ArrayList<>();
for (MemberOrderDto mo : resultList) {
orderList.add(new OrderDto(mo.getOrderId(), mo.getProductCode(), mo.getAmount()));
}
MemberOrderDto result = resultList.get(0);
orderListDto.setMemberId(result.getMemberId());
orderListDto.setMemberName(result.getMemberName());
orderListDto.setPhone(result.getPhone());
orderListDto.setList(orderList);
return ResponseEntity.ok(orderListDto);
}
예제 3-3. EntityManager 직접 사용
클래스 전체 code (접은 글)
package com.example.springtest.repository;
import com.example.springtest.dto.MemberOrderDto;
import com.example.springtest.dto.OrderDto;
import com.example.springtest.dto.OrderListDto;
import org.springframework.stereotype.Service;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import java.util.ArrayList;
import java.util.List;
@Service
public class NativeQueryService {
//EntityManager 직접 사용
@PersistenceContext
private EntityManager entityManager;
public OrderListDto findOrdersByMemberName(String memberName) {
//:memberName 부분을 제외한 순수 SQL
String sql = "SELECT " +
" m.member_id as memberId, " +
" m.name as memberName," +
" m.phone as phone," +
" o.order_id as orderId, " +
" o.product_code as productCode," +
" o.amount as amount" +
" FROM member_entity m, order_entity o " +
" WHERE m.member_id = o.member_id AND m.name = :memberName";
//:memberName에 매개변수로 받은 memberName 대입
Query query = entityManager.createNativeQuery(sql);
query.setParameter("memberName", memberName);
//쿼리 결과 => 하나의 Object[]에 row 하나가 담긴다.
List<Object[]> resultList = query.getResultList();
if(resultList.isEmpty()) {
return new OrderListDto();
}
//출력만 해보기
for (Object[] result : resultList) {
MemberOrderDto memberOrderDto = MemberOrderDto.builder()
.memberId(((Number)result[0]).longValue())
.memberName((String) result[1])
.phone((String) result[2])
.orderId(((Number) result[3]).longValue())
.productCode((String) result[4])
.amount((int) result[5])
.build();
System.out.println(memberOrderDto);
}
//실제 사용 방안
OrderListDto orderListDto = new OrderListDto();
Object[] firstResult = resultList.get(0);
orderListDto.setMemberId(((Number)firstResult[0]).longValue());
orderListDto.setMemberName((String) firstResult[1]);
orderListDto.setPhone((String) firstResult[2]);
List<OrderDto> orderList = new ArrayList<>();
for (Object[] result : resultList) {
OrderDto orderDto = new OrderDto();
orderDto.setOrderId(((Number) result[3]).longValue());
orderDto.setProductCode((String) result[4]);
orderDto.setAmount((int) result[5]);
orderList.add(orderDto);
}
orderListDto.setList(orderList);
return orderListDto;
}
}
1. EntityManager 주입
@Service
public class NativeQueryService {
//EntityManager 직접 사용
@PersistenceContext
private EntityManager entityManager;
public OrderListDto findOrdersByMemberName(String memberName) {
//생략
}
}
2. SQL 작성
:memberName 부분은 메서드 매개변수로 받아서 동적으로 할당한다.
String sql = "SELECT " +
" m.member_id as memberId, " +
" m.name as memberName," +
" m.phone as phone," +
" o.order_id as orderId, " +
" o.product_code as productCode," +
" o.amount as amount" +
" FROM member_entity m, order_entity o " +
" WHERE m.member_id = o.member_id AND m.name = :memberName";
3. Query 객체 생성 후 execute
//:memberName에 매개변수로 받은 memberName 대입
Query query = entityManager.createNativeQuery(sql);
query.setParameter("memberName", memberName);
//쿼리 결과 => 하나의 Object[]에 row 하나가 담긴다.
List<Object[]> resultList = query.getResultList();
4. (테스트용) row 별로 출력해 보기
//출력만 해보기
for (Object[] result : resultList) {
MemberOrderDto memberOrderDto = MemberOrderDto.builder()
.memberId(((Number)result[0]).longValue())
.memberName((String) result[1])
.phone((String) result[2])
.orderId(((Number) result[3]).longValue())
.productCode((String) result[4])
.amount((int) result[5])
.build();
System.out.println(memberOrderDto.toString());
}
(결과)
MemberOrderDto{memberId=2, memberName='Bob', phone='010-1765-4321', orderId=3, productCode='b3333', amount=2}
MemberOrderDto{memberId=2, memberName='Bob', phone='010-1765-4321', orderId=4, productCode='b4444', amount=5}
추가로, 아래와 같이 Query에 포함되는 컬럼 alias 명과 DTO의 필드 명을 맞춰줘야한다.
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class MemberOrderDto {
private Long memberId;
private String memberName;
private String phone;
private Long orderId;
private String productCode;
private int amount;
@Override
public String toString() {
return "MemberOrderDto{" +
"memberId=" + memberId +
", memberName='" + memberName + '\'' +
", phone='" + phone + '\'' +
", orderId=" + orderId +
", productCode='" + productCode + '\'' +
", amount=" + amount +
'}';
}
}
5. orderListDto 매핑 과정
//실제 사용 방안
OrderListDto orderListDto = new OrderListDto();
Object[] firstResult = resultList.get(0);
orderListDto.setMemberId(((Number)firstResult[0]).longValue());
orderListDto.setMemberName((String) firstResult[1]);
orderListDto.setPhone((String) firstResult[2]);
List<OrderDto> orderList = new ArrayList<>();
for (Object[] result : resultList) {
OrderDto orderDto = new OrderDto();
orderDto.setOrderId(((Number) result[3]).longValue());
orderDto.setProductCode((String) result[4]);
orderDto.setAmount((int) result[5]);
orderList.add(orderDto);
}
orderListDto.setList(orderList);
return orderListDto;
'Database > JPA' 카테고리의 다른 글
[JPA] 연관 관계 매핑 - @ManyToMany (0) | 2023.10.06 |
---|---|
[JPA] 연관관계 매핑 (1) | 2023.10.06 |
[JPA] 연관관계 매핑 - @OneToOne 일대일 매핑 (0) | 2023.10.06 |
[JPA] 연관관계 매핑 - @ManyToOne, @OneToMany (0) | 2023.10.06 |
[JPA] @Entity - 필드와 컬럼 매핑의 여러가지 속성 (1) | 2023.10.04 |