728x90
이번 포스팅에서는 Spring Boot 환경에서 JPA와 Criteria API를 활용하여 여러 테이블을 Join 하는 방법을 소개하겠습니다. 특히, Criteria API를 사용하면 동적 쿼리 작성이 간편해지고, 복잡한 쿼리를 객체지향적으로 관리할 수 있습니다.
이번 예제에서는 users, orders, products 테이블을 기준으로 사용자별 주문 내역을 조회하는 과정을 살펴보겠습니다. VO를 활용하여 데이터를 매핑하고, 실무에서 많이 사용하는 Join 예제를 통해 보다 효과적으로 JPA의 활용 방법을 익히실 수 있습니다.
JPA를 처음 사용하시는 분이나 Criteria API를 활용해보고 싶은 분들에게 도움이 되길 바랍니다. 그럼 시작해보겠습니다!
전체소스는 Github를 참고해 주세요.
1. 테이블 생성
- MariaDB 기준으로 작성되었습니다.
CREATE TABLE `users` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(255) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
`created_at` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
)
CREATE TABLE `products` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`description` varchar(255) DEFAULT NULL,
`price` double NOT NULL,
`stock` int(11) DEFAULT NULL,
`created_at` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
)
CREATE TABLE `orders` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL,
`product_id` bigint(20) NOT NULL,
`total` double NOT NULL,
`created_at` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
)
2. SpringBoot 프로젝트 생성
- SpringBoot 3.3.4 + Java + Gradle로 작성하였습니다.
2.1. build.gradle
dependencies {
// JPA + MariaDB
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.mariadb.jdbc:mariadb-java-client'
// lombok
annotationProcessor 'org.projectlombok:lombok'
compileOnly 'org.projectlombok:lombok'
// undertow
implementation 'org.springframework.boot:spring-boot-starter-undertow'
}
configurations {
configureEach {
// was tomcat 제외
exclude module: 'spring-boot-starter-tomcat'
}
}
- Criteria API는 JPA의 표준 스펙에 포함되어 있어, 별도의 의존성을 추가하지 않아도 "spring-boot-starter-data-jpa"에 내장되어 있습니다.
2.2. application.properties
# 서버 포트 지정
server.port=12345
# 데이터베이스 연결 정보
spring.datasource.url=jdbc:mariadb://localhost:3306/mymall
spring.datasource.username=root
spring.datasource.password=root
# JPA 테이블 관리 옵션
# none : Hibernate가 데이터베이스 스키마를 변경하거나 검증하지 않음[운영단계]
# validate : 엔티티와 데이터베이스 테이블 구조가 일치하는지만 확인[테스트, 운영단계]
# update : 엔티티와 데이터베이스 테이블을 비교하여 필요한 경우 스키마를 업데이트[개발단계]
# create : 애플리케이션 시작 시 기존 테이블을 모두 삭제하고 엔티티 구조에 맞춰 새로 생성[개발단계]
# create-drop : create와 동일하게 애플리케이션 시작 시 테이블을 생성하지만, 애플리케이션 종료 시점에 모든 테이블 삭제[개발단계]
spring.jpa.hibernate.ddl-auto=update
# JPA 쿼리 콘솔 출력 옵션
spring.jpa.show-sql=true
- 본 예제는 개발단계이므로 "update" 옵션으로 진행합니다.
2.3. 테이블 Model 생성
2.3.1. User.java
package com.criteria.criteriaexample.model;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Entity
@Getter
@Setter
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private String email;
private String createdAt;
@OneToMany(mappedBy = "user")
private List<Order> orders;
}
- users 테이블과 맵핑되는 Model입니다.
- @OneToMany(mappedBy = "user") : User와 Order 엔티티 간의 일대다(One-to-Many) 관계를 나타냅니다.
- mappedBy = "user"는 Order 엔티티에서 user 필드가 이 관계를 소유하고 있음을 의미합니다.
- 즉, 한 명의 사용자는 여러 개의 주문을 가질 수 있습니다.
2.3.2. Product.java
package com.criteria.criteriaexample.model;
import jakarta.persistence.*;
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
private Double price;
private Integer stock;
private String createdAt;
}
- products 테이블과 맵핑되는 Model입니다.
2.3.3. Order.java
package com.criteria.criteriaexample.model;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
@ManyToOne
@JoinColumn(name = "product_id")
private Product product;
private Double total;
private String createdAt;
}
- orders 테이블과 맵핑되는 Model입니다.
- @ManyToOne : 이 필드가 다대일(Many-to-One) 관계를 나타냅니다.
- @JoinColumn : 이 필드와 데이터베이스의 열 간의 매핑을 정의합니다.
2.3.4. UserOrderVO.java
package com.criteria.criteriaexample.vo;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UserOrderVO {
private Long orderId; // 주문번호
private String userName; // 사용자명
private String productName; // 상품명
private String productDesc; // 상품설명
private Double total; // 총 금액
}
- 쿼리 결과를 담을 VO을 선언합니다.
2.4. Service 작성
2.4.1. UserService.java
package com.criteria.criteriaexample.service;
import com.criteria.criteriaexample.model.Order;
import com.criteria.criteriaexample.model.Product;
import com.criteria.criteriaexample.model.User;
import com.criteria.criteriaexample.vo.UserOrderVO;
import jakarta.persistence.EntityManager;
import jakarta.persistence.criteria.*;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class UserService {
private final EntityManager entityManager;
public List<UserOrderVO> getUserWithOrders() {
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<UserOrderVO> query = cb.createQuery(UserOrderVO.class);
Root<User> user = query.from(User.class);
Join<User, Order> order = user.join("orders", JoinType.LEFT);
Join<Order, Product> product = order.join("product", JoinType.LEFT);
query.select(cb.construct(
UserOrderVO.class,
order.get("id"),
user.get("username"),
product.get("name"),
product.get("description"),
order.get("total")
));
query.orderBy(cb.asc(order.get("id")));
return entityManager.createQuery(query).getResultList();
}
}
- 사용자의 주문 정보를 조회하여 UserOrderVO 리스트를 반환하는 메서드입니다.
- User 엔티티의 루트(root) 객체를 생성합니다. 이 객체를 기준으로 쿼리를 작성합니다.
2.4.2. UserController.java
package com.criteria.criteriaexample.controller;
import com.criteria.criteriaexample.service.UserService;
import com.criteria.criteriaexample.vo.UserOrderVO;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/users/orders")
@ResponseBody
public List<UserOrderVO> getUserWithOrders() {
return userService.getUserWithOrders();
}
}
- "/users/orders" 요청이 들어오면 userService를 호출하여 Json 형태로 반환합니다.
3. 실행
요청 : http://localhost:12345/users/orders
응답 :
[
{
"orderId": 1,
"userName": "일번",
"productName": "모니터",
"productDesc": "24인치 모니터",
"total": 5
},
{
"orderId": 2,
"userName": "이번",
"productName": "TV",
"productDesc": "55인치 벽걸이 TV",
"total": 2
},
{
"orderId": 3,
"userName": "삼번",
"productName": "마우스",
"productDesc": "찍찍",
"total": 4
},
{
"orderId": 4,
"userName": "사번",
"productName": "스피커",
"productDesc": "사운드 빵빵",
"total": 7
},
{
"orderId": 5,
"userName": "오번",
"productName": "키보드",
"productDesc": "무소음",
"total": 10
},
{
"orderId": 6,
"userName": "육번",
"productName": "모니터",
"productDesc": "24인치 모니터",
"total": 2
},
{
"orderId": 7,
"userName": "칠번",
"productName": "TV",
"productDesc": "55인치 벽걸이 TV",
"total": 1
},
{
"orderId": 8,
"userName": "팔번",
"productName": "마우스",
"productDesc": "찍찍",
"total": 12
},
{
"orderId": 9,
"userName": "구번",
"productName": "스피커",
"productDesc": "사운드 빵빵",
"total": 3
},
{
"orderId": 10,
"userName": "십번",
"productName": "키보드",
"productDesc": "무소음",
"total": 5
}
]
감사합니다.
728x90
'프레임워크 > SpringBoot' 카테고리의 다른 글
[SpringBoot] AOP로 메서드 호출 로그 남기기 (1) | 2024.09.25 |
---|---|
[SpringBoot] JPA 쿼리 메서드 키워드 정리 및 예제 (2) | 2024.09.24 |
[SpringBoot] Apache Kafka 간단한 예제만들기 (0) | 2024.09.19 |
[SpringBoot] 공공데이터포탈 IP 정보검색 구현 (5) | 2024.08.30 |
[SpringBoot] HATEOAS 개념 및 간단한 예제 (1) | 2024.08.28 |