이 포스팅은 (인프런) 김영한 - 실전! 스프링 부트와 JPA 활용1 강의를 정리한 내용입니다.
개발 기술
dependencies
check
spring:
datasource:
url: jdbc:h2:tcp://localhost/~/jpashop
username: sa
password:
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
# show_sql: true
format_sql: true
logging.level:
org.hibernate.SQL: debug # 쿼리 파라미터 로그 남기기
#org.hibernate.orm.jdbc.bind: trace
controller
: request handler, reponse sender
service
: transaction 처리, 비즈니스 로직 수행
repository
: Data Access object, JPA 사용시 엔티티 매니저 활용(영속성 컨텍스트;컨테이너 로 관리)
domain
: 엔티티 모델, DB 테이블 구조와 함께 엔티티 필드와 직접 관련된 메소드 포함. 주로 repository에서 사용. (service 또는 controller에서는 dto 사용 권장)
@Entity
@Getter @Setter
@NoArgsConstructor
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String name;
@Getter @Setter
public class MemberForm {
@NotEmpty(message = "회원 이름은 필수 입니다.")
private String name;
private String city;
private String street;
private String zipcode;
}
@Repository
@RequiredArgsConstructor
public class MemberRepository {
private final EntityManager em;
public void save(Member member) {
em.persist(member);
}
public Member findOne(Long id) {
return em.find(Member.class, id); // .find(<조회 타입>, <PK>)
}
}
@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
class MemberServiceTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
// @PersistenceContext EntityManager em;
@Test
public void join() throws Exception {
//given
Member member = new Member();
member.setName("kim");
//when
Long savedId = memberService.join(member);
//then
// em.flush(); // DB에 강제로 쿼리를 날리는 메소드
assertEquals(member, memberRepository.findOne(savedId));
}
@Test
public void check_dupl_member() throws Exception {
//given
Member member1 = new Member();
member1.setName("kim");
Member member2 = new Member();
member2.setName("kim");
//when
memberService.join(member1);
//then
assertThrows(IllegalStateException.class, () -> {
memberService.join(member2); // 에외 발생 의도 지점
});
}
}
스프링부트는 기본적으로 Datasource 설정이 없다면 메모리 모드로 실행을 수행합니다. 따라서 test환경을 설정할 때에 application 파일을 분리할텐데, 이 때 test 환경에서는 메모리 모드 활용을 하는 것도 좋다.
DB는 상속
을 지원하지 않습니다. 따라서 자바 코드에서 구현되어있는 상속 관계를 "슈퍼타입", "서브타입" 논리 모델로 가져가면서, DB에 이를 물리적으로 구현하는 방안이 필요합니다.
JPA는 이를 위해 @Inheritance
, @DiscriminatorColumn
, @DiscriminatorValue
어노테이션을 지원합니다. 이를 이용하여 DB에 해당 논리 모델을 물리적으로 구현하는 방법은 3가지 정도로 추릴 수 있습니다.
@Inheritance(strategy=InheritanceType.XXX)
@DiscriminatorColumn(name="DTYPE")
@DiscriminatorValue("XXX")
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "dtype")
@Getter @Setter
public abstract class Item {
@Id @GeneratedValue
@Column(name = "item_id")
private Long id;
private String name;
private int price;
private int stockQuantity;
@ManyToMany(mappedBy = "items")
private List<Category> categories = new ArrayList<Category>();
public void addStock(int quantity) {
this.stockQuantity += quantity;
}
public void removeStock(int quantity) {
int restStock = this.stockQuantity - quantity;
if (restStock < 0) {
throw new NotEnoughStockException("need more stock");
}
this.stockQuantity = restStock;
}
}
package jpabook.jpashop.domain.item;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
@Entity
@DiscriminatorValue("B")
@Getter @Setter
public class Book extends Item {
private String author;
private String isbn;
}
@Getter @Setter
public class BookForm {
private Long id;
private String name;
private int price;
private int stockQuantity;
private String author;
private String isbn;
}
@Entity
@DiscriminatorValue("A")
@Getter @Setter
public class Album extends Item {
private String artist;
private String etc;
}
@Entity
@DiscriminatorValue("M")
@Getter @Setter
public class Movie extends Item {
private String director;
private String actor;
}
@Entity
@Getter @Setter
public class Category {
@Id @GeneratedValue
@Column(name = "category_id")
private Long id;
private String name;
@ManyToMany
@JoinTable(name = "category_item",
joinColumns = @JoinColumn(name = "category_id"),
inverseJoinColumns = @JoinColumn(name = "item_id"))
private List<Item> items = new ArrayList<>();
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private Category parent;
@OneToMany(mappedBy = "parent")
private List<Category> child = new ArrayList<>();
public void addChildCategory(Category child) {
this.child.add(child);
child.setParent(this);
}
}
@Repository
@RequiredArgsConstructor
public class ItemRepository {
private final EntityManager em;
public void save(Item item) {
if (item.getId() == null) {
em.persist(item);
} else {
em.merge(item);
}
}
public Item findOne(Long id) {
return em.find(Item.class, id);
}
public List<Item> findAll() {
return em.createQuery("select i from Item
i",Item.class).getResultList();
}
}
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ItemService {
private final ItemRepository itemRepository;
@Transactional
public void saveItem(Item item) {
itemRepository.save(item);
}
public List<Item> findItems() {
return itemRepository.findAll();
}
public Item findOne(Long itemId) {
return itemRepository.findOne(itemId);
}
}
주문은 이번 프로젝트의 핵심이라고 할 수 있습니다. 주문 기능을 구현하기 위해 엔티티 간 연관관계가 발생하며, 주문 서비스에서는 여러 레포지토리를 참조해야 합니다.
@Entity
@Table(name = "orders")
@Getter @Setter
public class Order {
@Id @GeneratedValue
@Column(name = "order_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "delivery_id")
private Delivery delivery;
private LocalDateTime orderDate;
@Enumerated(EnumType.STRING)
private OrderStatus status; [ORDER, CANCEL]
public void setMember(Member member) {
this.member = member;
member.getOrders().add(this);
}
public void addOrderItem(OrderItem orderItem) {
orderItems.add(orderItem);
orderItem.setOrder(this);
}
public void setDelivery(Delivery delivery) {
this.delivery = delivery;
delivery.setOrder(this);
}
}
@Entity
@Table(name = "order_item")
@Getter @Setter
public class OrderItem {
@Id @GeneratedValue
@Column(name = "order_item_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "item_id")
private Item item;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
private int orderPrice;
private int count;
}
public enum OrderStatus {
ORDER, CANCEL
}
@Entity
@Getter @Setter
public class Delivery {
@Id @GeneratedValue
@Column(name = "delivery_id")
private Long id;
@OneToOne(mappedBy = "delivery", fetch = FetchType.LAZY)
private Order order;
@Embedded
private Address address;
@Enumerated(EnumType.STRING)
private DeliveryStatus status; //ENUM [READY(준비), COMP(배송)]
}
public enum DeliveryStatus {
READY, COMP
}
@Entity
@Getter @Setter
public class OrderSearch {
@Id @GeneratedValue
private Long id;
private String memberName; //회원 이름
private OrderStatus orderStatus;//주문 상태[ORDER, CANCEL]
}
@Repository
@RequiredArgsConstructor
public class OrderRepository {
private final EntityManager em;
public void save(Order order) {
em.persist(order);
}
public Order findOne(Long id) {
return em.find(Order.class, id);
}
/**
* 검색기능
*/
public List<Order> findAll(OrderSearch orderSerach) {
return em.createQuery("select o from Order o", Order.class).getResultList();
}
}
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class OrderService {
private final OrderRepository orderRepository;
private final MemberRepository memberRepository;
private final ItemRepository itemRepository;
/**
* 주문 *
*/
@Transactional
public Long order(Long memberId, Long itemId, int count) {
// 엔티티 조회
Member member = memberRepository.findOne((memberId));
Item item = itemRepository.findOne(itemId);
// 배송정보 생성
Delivery delivery = new Delivery();
delivery.setAddress(member.getAddress());
// 주문상품 생성
OrderItem orderItem = OrderItem.createOrderItem(item, item.getPrice(), count);
// 주문 생성
Order order = Order.createOrder(member, delivery, orderItem);
// 주문 저장
orderRepository.save(order);
return order.getId();
}
/**
* 주문 취소 *
*/
@Transactional
public void cancelOrder(Long orderId) {
// 주문 엔티티 조회
Order order = orderRepository.findOne(orderId);
// 주문 취소
order.cancel();
}
/**
* 주문 검색 *
*/
public List<Order> findOrders(OrderSearch orderSearch) {
return orderRepository.findAll(orderSearch);
}
}
@Controller
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
private final MemberService memberService;
private final ItemService itemService;
@GetMapping(value = "/order")
public String createForm(Model model) {
List<Member> members = memberService.findMembers();
List<Item> items = itemService.findItems();
model.addAttribute("members", members);
model.addAttribute("items", items);
return "order/orderForm";
}
@PostMapping(value = "/order")
public String order(@RequestParam("memberId") Long memberId,
@RequestParam("itemId") Long itemId, @RequestParam("count") int count) {
orderService.order(memberId, itemId, count);
return "redirect:/orders";
}
@GetMapping(value = "/orders")
public String orderList(@ModelAttribute("orderSearch") OrderSearch orderSearch, Model model) {
List<Order> orders = orderService.findOrders(orderSearch);
model.addAttribute("orders", orders);
return "order/orderList";
}
@PostMapping(value = "/orders/{orderId}/cancel")
public String cancelOrder(@PathVariable("orderId") Long orderId) {
orderService.cancelOrder(orderId);
return "redirect:/orders";
}
}