hades

[Spring] ์Šคํ”„๋ง ๊ธฐ์ดˆ (3) ๋ณธ๋ฌธ

๐Ÿƒ๐Ÿป‍โ™‚๏ธ ๊ธฐ๋ณธํ›ˆ๋ จ/Spring

[Spring] ์Šคํ”„๋ง ๊ธฐ์ดˆ (3)

hades1 2024. 7. 27. 11:12

ํ”„๋กœ์ ํŠธ ํ™˜๊ฒฝ์„ค์ •

 

H2์™€ Thymeleaf ๋™์ž‘ ํ™•์ธ ํ›„, application.yml ํŒŒ์ผ ์ƒ์„ฑ

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

ddl-auto: create๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰ ์‹œ์ ์— ํ…Œ์ด๋ธ”์„ ์ƒˆ๋กœ ์ƒ์„ฑํ•œ๋‹ค.

 

Member Entity

package jpabook.jpashop;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
public class Member {

    @Id
    @GeneratedValue
    private Long id;

    private String username;
}

@Id๋กœ ์ธํ•ด id๊ฐ€ ์ž๋™์œผ๋กœ primary key๋กœ ๋“ฑ๋ก๋˜๊ณ , @GeneratedValue์— ์˜ํ•ด ์ž๋™์œผ๋กœ ๊ฐ’์ด ๋ถ€์—ฌ๋œ๋‹ค.

 

MemberRepository

package jpabook.jpashop;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.stereotype.Repository;

@Repository
public class MemberRepository {

    @PersistenceContext
    private EntityManager em;

    public Long save(Member member){
        em.persist(member);
        return member.getId(); 
    }

    public Member find(Long id){
        return em.find(Member.class, id);
    }
}

persist๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋“ฑ๋กํ•˜๋Š” ์—ญํ• ์„ ํ•œ๋‹ค.

 

find์—์„œ member ์ „์ฒด๊ฐ€ ์•„๋‹Œ id๋งŒ return ํ•˜๋Š” ์ด์œ ๋Š” ์ปค๋งจ๋“œ์™€ ์ฟผ๋ฆฌ๋ฅผ ๋ถ„๋ฆฌํ•˜๊ณ , id๋งŒ์œผ๋กœ๋„ ๋‚˜์ค‘์— ์ฐพ์„ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

Test

package jpabook.jpashop;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(SpringExtension.class)
@SpringBootTest
class MemberRepositoryTest {

    @Autowired
    MemberRepository memberRepository;

    @Test
    public void testMember() throws Exception {
        //given
        Member member = new Member();
        member.setUsername("์‹ ์งฑ๊ตฌ");

        //when
        Long saveId = memberRepository.save(member);

        //then
        Member findMember = memberRepository.find(saveId);
        Assertions.assertThat(findMember.getId()).isEqualTo(member.getId());
        Assertions.assertThat(findMember.getUsername()).isEqualTo(member.getUsername());
    }
}

 

๋„๋ฉ”์ธ ๋ถ„์„ ์„ค๊ณ„

์š”๊ตฌ์‚ฌํ•ญ ๋ถ„์„

 

๋„๋ฉ”์ธ ๋ชจ๋ธ๊ณผ ํ…Œ์ด๋ธ” ์„ค๊ณ„

ํ•œ ํšŒ์›์€ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์ฃผ๋ฌธ์„ ํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์ผ๋Œ€๋‹ค ๊ด€๊ณ„์ด๋‹ค.

 

์ฃผ๋ฌธ ํ•˜๋‚˜ ๋‹น ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์ƒํ’ˆ์ด ์žˆ์„ ์ˆ˜ ์žˆ๊ณ , ์ƒํ’ˆ ํ•˜๋‚˜์™€ ๊ด€๋ จ๋œ ์ฃผ๋ฌธ์ด ์—ฌ๋Ÿฌ ๊ฐœ ์žˆ์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ๋‹ค๋Œ€๋‹ค ๊ด€๊ณ„์ด์ง€๋งŒ, ์ค‘๊ฐ„์— ์ฃผ๋ฌธ์ƒํ’ˆ์ด๋ผ๋Š” ๊ฒƒ์„ ๋„ฃ์–ด ์ผ๋Œ€๋‹ค+๋‹ค๋Œ€์ผ๋กœ ๊ตฌ์„ฑํ•œ๋‹ค.

 

ํ•œ ์ƒํ’ˆ์€ ์—ฌ๋Ÿฌ ์นดํ…Œ๊ณ ๋ฆฌ์— ์†ํ•  ์ˆ˜ ์žˆ๊ณ , ํ•œ ์นดํ…Œ๊ณ ๋ฆฌ ์•ˆ์— ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์ƒํ’ˆ์ด ์†ํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ๋‹ค๋Œ€๋‹ค ๊ด€๊ณ„์ด๋‹ค.

 

๋‹ค๋Œ€๋‹ค ๊ด€๊ณ„๋Š” ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” ๋ฌผ๋ก ์ด๊ณ  ์—”ํ‹ฐํ‹ฐ์—์„œ๋„ ๊ฑฐ์˜ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค. ๋”ฐ๋ผ์„œ ๊ทธ๋ฆผ์ฒ˜๋Ÿผ ์ฃผ๋ฌธ์ƒํ’ˆ์ด๋ผ๋Š” ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ถ”๊ฐ€ํ•ด์„œ ๋‹ค๋Œ€๋‹ค ๊ด€๊ณ„๋ฅผ ์ผ๋Œ€๋‹ค+๋‹ค๋Œ€์ผ ๊ด€๊ณ„๋กœ ๋ณ€๊ฒฝํ•œ๋‹ค.

 

์—”ํ‹ฐํ‹ฐ ๋ถ„์„

 

์ฐธ๊ณ ) ํšŒ์›์ด ์ฃผ๋ฌธ์„ ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ํšŒ์›์ด ์ฃผ๋ฌธ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฐ€์ง€๋Š” ๊ฒƒ์€ ์–ผํ• ๋ณด๋ฉด ์ž˜ ์„ค๊ณ„ํ•œ ๊ฒƒ ๊ฐ™์ง€๋งŒ, ๊ฐ์ฒด ์„ธ์ƒ์€ ์‹ค์ œ ์„ธ๊ณ„์™€๋Š” ๋‹ค๋ฅด๋‹ค. ์‹ค๋ฌด์—์„œ๋Š” ํšŒ์›์ด ์ฃผ๋ฌธ์„ ์ฐธ์กฐํ•˜์ง€ ์•Š๊ณ , ์ฃผ๋ฌธ์ด ํšŒ์›์„ ์ฐธ์กฐํ•˜๋Š” ๊ฒƒ์œผ๋กœ ์ถฉ๋ถ„ํ•˜๋‹ค. ์—ฌ๊ธฐ์„œ๋Š” ์ผ๋Œ€๋‹ค, ๋‹ค๋Œ€์ผ์˜ ์–‘๋ฐฉํ–ฅ ์—ฐ๊ด€๊ด€๊ณ„๋ฅผ ์„ค๋ช…ํ•˜๊ธฐ ์œ„ํ•ด์„œ ์ถ”๊ฐ€ํ–ˆ๋‹ค. ๊ฐ€๊ธ‰์ ์ด๋ฉด ์–‘๋ฐฉํ–ฅ ์—ฐ๊ด€๊ด€๊ณ„๋ณด๋‹ค๋Š” ๋‹จ๋ฐฉํ–ฅ ์—ฐ๊ด€๊ด€๊ณ„๋ฅผ ์“ฐ๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.

 

ํ…Œ์ด๋ธ” ๋ถ„์„

 

์—ฐ๊ด€๊ด€๊ณ„์˜ ์ฃผ์ธ์€ FK(์™ธ๋ž˜ํ‚ค)๊ฐ€ ์žˆ๋Š” ์ชฝ์˜ ๋ฉค๋ฒ„์ด๋‹ค.

 

์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ๊ฐœ๋ฐœ

์‹ค๋ฌด์—์„œ๋Š” ๊ฐ€๊ธ‰์  Getter๋Š” ์—ด์–ด๋‘๊ณ , Setter๋Š” ๊ผญ ํ•„์š”ํ•œ ๊ฒฝ์šฐ์—๋งŒ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ์ถ”์ฒœํ•œ๋‹ค๊ณ  ํ•˜์‹ ๋‹ค.

 

Member

package jpabook.jpashop.domain;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@Setter
public class Member {

    @Id
    @GeneratedValue
    @Column(name="member_id")   // ํ…Œ์ด๋ธ”์— ํ–‰ ์ด๋ฆ„์„ member_id๋กœ ์„ค์ •
    private Long id;

    private String name;

    @Embedded   // Address ํ•„๋“œ๋“ค์ด Member ํด๋ž˜์Šค์˜ ํ•„๋“œ๋กœ ํฌํ•จ๋  ์ˆ˜ ์žˆ์Œ
    private Address address;

    @OneToMany(mappedBy = "member") // Member:Order=์ผ:๋‹ค, Order ํด๋ž˜์Šค์—์„œ member๋กœ ๋งคํ•‘๋˜๋Š” ์ž…์žฅ์ด๋ผ๋Š” ์˜๋ฏธ
    private List<Order> orders = new ArrayList<>();
}

 

Address

package jpabook.jpashop.domain;

import jakarta.persistence.Embeddable;
import lombok.Getter;

@Embeddable // ํด๋ž˜์Šค์— ํ•„๋“œ๋กœ ํฌํ•จ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Œ.
@Getter
public class Address {

    private String city;
    private String street;
    private String zipcode;

    protected Address() {
    }

    public Address(String city, String street, String zipcode) {
        this.city = city;
        this.street = street;
        this.zipcode = zipcode;
    }
}

๊ฐ’์€ ๋ณ€๊ฒฝํ•˜์ง€ ๋ชปํ•˜๋„๋ก ์„ค๊ณ„ํ•ด์•ผ ํ•œ๋‹ค.

 

Order

package jpabook.jpashop.domain;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "orders")
@Getter
@Setter
public class Order {

    @Id
    @GeneratedValue
    @Column(name = "order_id")  // ํ…Œ์ด๋ธ”์— ์ด๋ฆ„์ด order_id์ธ ํ–‰์— ์ €์žฅํ•˜๊ฒ ๋‹ค๋Š” ์˜๋ฏธ
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)  // Order:Member = ๋‹ค:์ผ
    @JoinColumn(name = "member_id")     // member_id ํ–‰ ์ƒ์„ฑ ํ›„, Member Entity์˜ PK์™€ ์ž๋™ ๋งคํ•‘
    private Member member;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)  // Order:OrderItem = ์ผ:๋‹ค
    private List<OrderItem> orderItems = new ArrayList<>();

    @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name="delivery_id")
    private Delivery delivery;

    private LocalDateTime orderDate;

    @Enumerated(EnumType.STRING)    // EnumType.ORDINAL์€ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ๋ฐ˜๋“œ์‹œ STRING์œผ๋กœ ์„ค์ •
    private OrderStatus status;

    // ์—ฐ๊ด€๊ด€๊ณ„ ๋ฉ”์†Œ๋“œ, ์œ„์น˜๋Š” ํ•ต์‹ฌ์ ์œผ๋กœ ์ปจํŠธ๋กคํ•˜๋Š” ๋ถ€๋ถ„
    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);
    }
}

์—ฐ๊ด€๊ด€๊ณ„ ๋ฉ”์†Œ๋“œ๋Š” ๋‹น์—ฐํžˆ ์‹คํ–‰์‹œ์ผœ์•ผ ๋™์ž‘ํ•œ๋‹ค.

 

OrderItem

package jpabook.jpashop.domain;

import jakarta.persistence.*;
import jpabook.jpashop.domain.item.Item;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
public class OrderItem {

    @Id
    @GeneratedValue
    @Column(name = "order_item_id") // ํ…Œ์ด๋ธ”์— order_item_id ํ–‰์— ์ €์žฅ
    private Long id;

    @ManyToOne
    @JoinColumn(name = "item_id")   // item_id ํ–‰๊ณผ ์—ฐ๊ฒฐ
    private Item item;

    @ManyToOne
    @JoinColumn(name = "order_id")  // order_id ํ–‰๊ณผ ์—ฐ๊ฒฐ
    private Order order;

    private int orderPrice;

    private int count;
}

 

Item

package jpabook.jpashop.domain.item;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)   // ํ…Œ์ด๋ธ” ํ•˜๋‚˜์— ๋‹ค ์ €์žฅํ•  ๊ฒƒ์ด๋ผ๋Š” ์˜๋ฏธ
@DiscriminatorColumn(name = "dtype")    // 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;
}

Book, Movie, Album์—์„œ ๋ณต์ œํ•ด์„œ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด ์ถ”์ƒ ํด๋ž˜์Šค๋กœ ์ƒ์„ฑํ•˜์˜€๋‹ค. OrderItem๊ณผ Item์€ ๋‹จ๋ฐฉํ–ฅ ๊ด€๊ณ„์ด๋ฏ€๋กœ, MappedBy๊ฐ€ ์—†๋‹ค!

 

Book

package jpabook.jpashop.domain.item;

import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
import lombok.Getter;
import lombok.Setter;

@Entity
@DiscriminatorValue("B")	// dtype=B
@Getter
@Setter
public class Book extends Item {
    private String author;
    private String isbn;
}

 

Movie

package jpabook.jpashop.domain.item;

import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
import lombok.Getter;
import lombok.Setter;

@Entity
@DiscriminatorValue("M")	// dtype=M
@Getter
@Setter
public class Movie extends Item {
    private String director;
    private String actor;
}

 

Album

package jpabook.jpashop.domain.item;

import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
import lombok.Getter;
import lombok.Setter;

@Entity
@DiscriminatorValue("A")	// dtype=A
@Getter
@Setter
public class Album extends Item {
    private String artist;
    private String etc;
}

 

Category

package jpabook.jpashop.domain;

import jakarta.persistence.*;
import jpabook.jpashop.domain.item.Item;
import lombok.Getter;
import lombok.Setter;

import java.util.ArrayList;
import java.util.List;

@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"),  // category_item ํ…Œ์ด๋ธ”์— ์žˆ๋Š” category_id
            inverseJoinColumns = @JoinColumn(name = "item_id")
    )
    private List<Item> items = new ArrayList<>();

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinTable(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(parent);
    }
}

 

FK๋Š” ์ผ๋Œ€๋‹ค ๊ด€๊ณ„์—์„œ๋Š” ๋‹ค์— ๋‘”๋‹ค. ์ผ๋Œ€์ผ ๊ด€๊ณ„์—์„œ๋Š” ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” ๊ณณ์— ๋‘”๋‹ค. ์—ฐ๊ด€ ๊ด€๊ณ„ ์ฃผ์ธ์€ FK๊ฐ€ ์œ„์น˜ํ•œ ํด๋ž˜์Šค์—์„œ ์—ฐ๊ด€ ๊ด€๊ณ„์— ์žˆ๋Š” ๋ฉค๋ฒ„์ด๋‹ค.

 

์‹ค๋ฌด์—์„œ๋Š” ๋‹ค๋Œ€๋‹ค๋ฅผ ์ ˆ๋Œ€ ์‚ฌ์šฉํ•˜์ง€ ๋ง์ž.

 

์—”ํ‹ฐํ‹ฐ ์„ค๊ณ„์‹œ ์ฃผ์˜์ 

์—”ํ‹ฐํ‹ฐ์—๋Š” ๊ฐ€๊ธ‰์  Setter๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ๋ง์ž.

 

๋ชจ๋“  ์—ฐ๊ด€๊ด€๊ณ„๋Š” ์ง€์—ฐ ๋กœ๋”ฉ์œผ๋กœ ์„ค์ •ํ•˜์ž.โ˜…

  • ์ฆ‰์‹œ๋กœ๋”ฉ(EAGER)์€ ์˜ˆ์ธก์ด ์–ด๋ ต๊ณ , ์–ด๋–ค SQL์ด ์‹คํ–‰๋ ์ง€ ์ถ”์ ํ•˜๊ธฐ ์–ด๋ ต๋‹ค. ํŠนํžˆ JPQL์„ ์‹คํ–‰ํ•  ๋•Œ N+1 ๋ฌธ์ œ๊ฐ€ ์ž์ฃผ ๋ฐœ์ƒํ•œ๋‹ค.
  • ์‹ค๋ฌด์—์„œ ๋ชจ๋“  ์—ฐ๊ด€๊ด€๊ณ„๋Š” ์ง€์—ฐ๋กœ๋”ฉ(LAZY)์œผ๋กœ ์„ค์ •ํ•ด์•ผ ํ•œ๋‹ค.
  • ์—ฐ๊ด€๋œ ์—”ํ‹ฐํ‹ฐ๋ฅผ ํ•จ๊ป˜ DB์—์„œ ์กฐํšŒํ•ด์•ผ ํ•˜๋ฉด, fetch join ๋˜๋Š” ์—”ํ‹ฐํ‹ฐ ๊ทธ๋ž˜ํ”„ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•œ๋‹ค.
  • @XToOne(OneToOne, ManyToOne) ๊ด€๊ณ„๋Š” ๊ธฐ๋ณธ์ด ์ฆ‰์‹œ๋กœ๋”ฉ์ด๋ฏ€๋กœ ์ง์ ‘ ์ง€์—ฐ๋กœ๋”ฉ์œผ๋กœ ์„ค์ •ํ•ด์•ผ ํ•œ๋‹ค.

 

์ปฌ๋ ‰์…˜์€ ํ•„๋“œ์—์„œ ์ดˆ๊ธฐํ™”ํ•˜์ž.

  • null ๋ฌธ์ œ์—์„œ ์•ˆ์ „ํ•˜๋‹ค.
  • ํ•˜์ด๋ฒ„๋„ค์ดํŠธ๋Š” ์—”ํ‹ฐํ‹ฐ๋ฅผ ์˜์†ํ™” ํ•  ๋•Œ, ์ปฌ๋ ‰์…˜์„ ๊ฐ์‹ธ์„œ ํ•˜์ด๋ฒ„๋„ค์ดํŠธ๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๋‚ด์žฅ ์ปฌ๋ ‰์…˜์œผ๋กœ ๋ณ€๊ฒฝํ•œ๋‹ค. ๋งŒ์•ฝgetOrders() ์ฒ˜๋Ÿผ ์ž„์˜์˜ ๋ฉ”์„œ๋“œ์—์„œ ์ปฌ๋ ฅ์…˜์„ ์ž˜๋ชป ์ƒ์„ฑํ•˜๋ฉด ํ•˜์ด๋ฒ„๋„ค์ดํŠธ ๋‚ด๋ถ€ ๋ฉ”์ปค๋‹ˆ์ฆ˜์— ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค. ๋”ฐ๋ผ์„œ ํ•„๋“œ๋ ˆ๋ฒจ์—์„œ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ์•ˆ์ „ํ•˜๊ณ , ์ฝ”๋“œ๋„ ๊ฐ„๊ฒฐํ•˜๋‹ค.

 

cf) ์˜์†ํ™” = ์ž๋ฐ” ์„ธ์ƒ ๋ฐ–(๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค, ํŒŒ์ผ)์—์„œ๋„ ์œ ์ง€ํ•˜๊ฒŒ ํ•จ.

 

ํ…Œ์ด๋ธ”, ์ปฌ๋Ÿผ๋ช… ์ƒ์„ฑ ์ „๋žต(SpringPhysicalNamingStrategy)

์Šคํ”„๋ง ๋ถ€ํŠธ ์‹ ๊ทœ ์„ค์ • (์—”ํ‹ฐํ‹ฐ(ํ•„๋“œ) -> ํ…Œ์ด๋ธ”(์ปฌ๋Ÿผ))
1. ์นด๋ฉœ ์ผ€์ด์Šค ์–ธ๋”์Šค์ฝ”์–ด(memberPoint -> member_point)
2. .(์ ) -> _(์–ธ๋”์Šค์ฝ”์–ด)
3. ๋Œ€๋ฌธ์ž -> ์†Œ๋ฌธ์ž

 

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ตฌํ˜„ ์ค€๋น„

๊ตฌํ˜„ ์š”๊ตฌ ์‚ฌํ•ญ

  • ํšŒ์› ๊ธฐ๋Šฅ
    • ํšŒ์› ๋“ฑ๋ก
    • ํšŒ์› ์กฐํšŒ (์ „์ฒด, ์ด๋ฆ„์œผ๋กœ ํ•ด๋‹นํ•˜๋Š” ์‚ฌ๋žŒ๋“ค, ID๋กœ ๊ฐœ์ธ)
  • ์ƒํ’ˆ ๊ธฐ๋Šฅ
    • ์ƒํ’ˆ ๋“ฑ๋ก
    • ์ƒํ’ˆ ์ˆ˜์ •
    • ์ƒํ’ˆ ์กฐํšŒ
  • ์ฃผ๋ฌธ ๊ธฐ๋Šฅ
    • ์ƒํ’ˆ ์ฃผ๋ฌธ
    • ์ฃผ๋ฌธ ๋‚ด์—ญ ์กฐํšŒ
    • ์ฃผ๋ฌธ ์ทจ์†Œ

 

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์•„ํ‚คํ…์ณ

 

ํšŒ์› ๋„๋ฉ”์ธ ๊ฐœ๋ฐœ

ํšŒ์› ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๊ฐœ๋ฐœ

package jpabook.jpashop.repository;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jpabook.jpashop.domain.Member;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Repository
@RequiredArgsConstructor
@Transactional
public class MemberRepository {

    // @PersistenceContext // Spring์ด EntityManager ์ฃผ์ž…
    private final EntityManager em;

    public void save(Member member){
        em.persist(member);
    }

    public Member findOne(Long id){
        return em.find(Member.class, id);	// em.find๋Š” ๊ฐ์ฒด ๋ฐ˜ํ™˜
    }

    public List<Member> findByName(String name){
        return em.createQuery("select m from Member m where m.name = :name", Member.class)  // :name์€ ๋ณ€์ˆ˜๊ฐ€ ๋“ค์–ด๊ฐˆ ์ž๋ฆฌ
                .setParameter("name", name)
                .getResultList();
    }

    public List<Member> findAll(){
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }
}

 

ํšŒ์› ์„œ๋น„์Šค ๊ฐœ๋ฐœ

package jpabook.jpashop.service;

import jpabook.jpashop.domain.Member;
import jpabook.jpashop.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true) // JPA๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ณณ์—์„œ๋Š” Transactional ํ•„์ˆ˜!!
public class MemberService {

    private final MemberRepository memberRepository;

    @Transactional  // readonly๋ฅผ ๊ธฐ๋ณธ์œผ๋กœ ์„ค์ •ํ•˜๊ณ , join๋งŒ writeํ•˜๋ฏ€๋กœ ๋”ฐ๋กœ ์„ค์ •
    public Long join(Member member){
        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();
    }

    private void validateDuplicateMember(Member member){
        List<Member> findMembers = memberRepository.findByName(member.getName());
        if (!(findMembers.isEmpty())){
            throw new IllegalStateException("์ด๋ฏธ ์กด์žฌํ•˜๋Š” ํšŒ์›!");
        }
    }

    public List<Member> findMembers(){
        return memberRepository.findAll();
    }

    public Member findOne(Long memberId) {
        return memberRepository.findOne(memberId);
    }
}

 

ํšŒ์› ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ

package jpabook.jpashop.service;

import jpabook.jpashop.domain.Member;
import jpabook.jpashop.repository.MemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.annotation.Transactional;

import static org.junit.jupiter.api.Assertions.*;

@ExtendWith(SpringExtension.class)
@SpringBootTest
@Transactional
class MemberServiceTest {

    MemberService memberService;
    MemberRepository memberRepository;

    @Autowired
    public MemberServiceTest(MemberService memberService, MemberRepository memberRepository) {
        this.memberService = memberService;
        this.memberRepository = memberRepository;
    }

    @Test
    public void ํšŒ์›๊ฐ€์ž…() throws Exception {
        //given
        Member member = new Member();
        member.setName("Peter");

        //when
        Long saveId = memberService.join(member);

        //then
        Assertions.assertThat(member).isEqualTo(memberRepository.findOne(saveId));
    }

    @Test
    public void ์ค‘๋ณต_ํšŒ์›_์˜ˆ์™ธ() throws Exception {
        //given
        Member member1 = new Member();
        Member member2 = new Member();
        member1.setName("Park");
        member2.setName("Park");

        //when
        memberService.join(member1);

        //then
        org.junit.jupiter.api.Assertions.assertThrows(IllegalStateException.class, () -> memberService.join(member2));
    }
}

 

์ƒํ’ˆ ๋„๋ฉ”์ธ ๊ฐœ๋ฐœ

์ƒํ’ˆ ์—”ํ‹ฐํ‹ฐ ๊ฐœ๋ฐœ

package jpabook.jpashop.domain.item;

import jakarta.persistence.*;
import jpabook.jpashop.domain.Category;
import jpabook.jpashop.exception.NotEnoughStockException;
import lombok.Getter;
import lombok.Setter;

import java.util.ArrayList;
import java.util.List;

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)   // ํ…Œ์ด๋ธ” ํ•˜๋‚˜์— ๋‹ค ์ €์žฅํ•  ๊ฒƒ์ด๋ผ๋Š” ์˜๋ฏธ
@DiscriminatorColumn(name = "dtype")    // 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<>();

    // ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง
    public void addStock(int quantity) {
        this.stockQuantity += quantity;
    }

    public void removeStock(int quantity) {
        int restStock = this.stockQuantity -= quantity;
        if (restStock < 0) {
            throw new NotEnoughStockException("์žฌ๊ณ ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค.");
        }
        this.stockQuantity = restStock;
    }
}

Item ๋ฐ–์—์„œ ๊ฐ’์„ ๊ณ„์‚ฐํ•ด์„œ Setter๋ฅผ ํ†ตํ•ด ๊ฐ’์„ ๊ฐฑ์‹ ํ•˜๊ธฐ ๋ณด๋‹ค๋Š” Item ์ž์ฒด์— ๋ฉ”์†Œ๋“œ๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์ด ๋ฐ”๋žŒ์งํ•˜๋‹ค.

 

์ƒํ’ˆ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๊ฐœ๋ฐœ

package jpabook.jpashop.repository;

import jakarta.persistence.EntityManager;
import jakarta.transaction.Transactional;
import jpabook.jpashop.domain.item.Item;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
@Transactional
@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();
    }
}

 

์ƒํ’ˆ ์„œ๋น„์Šค ๊ฐœ๋ฐœ

package jpabook.jpashop.service;

import jpabook.jpashop.domain.item.Item;
import jpabook.jpashop.repository.ItemRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@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
@NoArgsConstructor(access = AccessLevel.PROTECTED)  // ์ƒ์„ฑ์ž๊ฐ€ ์•„๋‹ˆ๋ผ ์ƒ์„ฑ ๋ฉ”์†Œ๋“œ๋กœ ์ƒ์„ฑํ•˜๊ฒŒ ํ•จ!!
public class Order {
	
    ...

    // ์ƒ์„ฑ ๋ฉ”์†Œ๋“œ
    public static Order createOrder(Member member, Delivery delivery, OrderItem... orderItems){
        Order order = new Order();
        order.setMember(member);
        order.setDelivery(delivery);
        for (OrderItem orderItem : orderItems){
            order.addOrderItem(orderItem);
        }
        order.setStatus(OrderStatus.ORDER);
        order.setOrderDate(LocalDateTime.now());
        return order;
    }

    // ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง
    /*
    ์ฃผ๋ฌธ ์ทจ์†Œ
    */
    public void cancel() {
        if (delivery.getStatus() == DeliveryStatus.COMP){
            throw new IllegalStateException("์ด๋ฏธ ๋ฐฐ์†ก ์™„๋ฃŒ๋œ ์ƒํ’ˆ์€ ์ทจ์†Œ ๋ถˆ๊ฐ€");
        }
        this.setStatus(OrderStatus.CANCEL);
        for (OrderItem orderItem : orderItems){
            orderItem.cancel(); // Item์˜ ์žฌ๊ณ ๋ฅผ ์›์ƒ๋ณต๊ตฌ์‹œํ‚ด
        }
    }

    // ์กฐํšŒ ๋กœ์ง
    /*
    * ์ „์ฒด ์ฃผ๋ฌธ ๊ฐ€๊ฒฉ ์กฐํšŒ
    * */
    public int getTotalPrice(){
        int totalPrice = 0;
        for (OrderItem orderItem : orderItems){
            totalPrice += orderItem.getTotalPrice();
        }
        return totalPrice;
    }
}

 

์ฃผ๋ฌธ์ƒํ’ˆ ์—”ํ‹ฐํ‹ฐ ๊ฐœ๋ฐœ

package jpabook.jpashop.domain;

import jakarta.persistence.*;
import jpabook.jpashop.domain.item.Item;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)  // ์ƒ์„ฑ์ž๊ฐ€ ์•„๋‹ˆ๋ผ ์ƒ์„ฑ ๋ฉ”์†Œ๋“œ๋กœ ์ƒ์„ฑํ•˜๊ฒŒ ํ•จ!!
public class OrderItem {

    @Id
    @GeneratedValue
    @Column(name = "order_item_id") // ํ…Œ์ด๋ธ”์— order_item_id ํ–‰์— ์ €์žฅ
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "item_id")   // item_id ํ–‰๊ณผ ์—ฐ๊ฒฐ
    private Item item;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")  // order_id ํ–‰๊ณผ ์—ฐ๊ฒฐ
    private Order order;

    private int orderPrice;

    private int count;

    // ์ƒ์„ฑ ๋ฉ”์†Œ๋“œ
    public static OrderItem createOrderItem(Item item, int orderPrice, int count){
        OrderItem orderItem = new OrderItem();
        orderItem.setItem(item);
        orderItem.setOrderPrice(orderPrice);
        orderItem.setCount(count);

        item.removeStock(count);
        return orderItem;
    }

    // ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง
    public void cancel(){
        getItem().addStock(count);
    }

    // ์กฐํšŒ ๋กœ์ง
    public int getTotalPrice(){
        return getOrderPrice() * getCount();
    }
}

์ƒ์„ฑํ•  ๋•Œ, ๋ฐ–์—์„œ new ํ•˜๊ณ , Setter๋ฅผ ์ด์šฉํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค public static ์ƒ์„ฑ ๋ฉ”์†Œ๋“œ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.

 

์ฃผ๋ฌธ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๊ฐœ๋ฐœ

package jpabook.jpashop.repository;

import jakarta.persistence.EntityManager;
import jpabook.jpashop.domain.Order;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.List;

@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);
    }
}

 

์ฃผ๋ฌธ ์„œ๋น„์Šค ๊ฐœ๋ฐœ

package jpabook.jpashop.service;

import jpabook.jpashop.domain.*;
import jpabook.jpashop.domain.item.Item;
import jpabook.jpashop.repository.ItemRepository;
import jpabook.jpashop.repository.MemberRepository;
import jpabook.jpashop.repository.OrderRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@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());
        delivery.setStatus(DeliveryStatus.READY);

        // ์ฃผ๋ฌธ์ƒํ’ˆ ์ƒ์„ฑ
        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();
    }

    // ์ฃผ๋ฌธ ๊ฒ€์ƒ‰

}

Delivery์™€ OrderItem์€ Order ๋„๋ฉ”์ธ์—์„œ๋งŒ ์‚ฌ์šฉํ•œ๋‹ค. ์ด๋ ‡๊ฒŒ ์‚ฌ์šฉ ํ™˜๊ฒฝ์ด ์ œํ•œ๋œ ๊ฒฝ์šฐ์—๋งŒ, cascade๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.

 

cancelOrder์— @Transactional์ด ๋ถ™์„ ํ•„์š”๊ฐ€ ์—†์ง€ ์•Š๋‚˜? ๋ผ๊ณ  ์ƒ๊ฐํ–ˆ์—ˆ๋Š”๋ฐ, ๊ฐ’์„ ๋ณ€๊ฒฝํ•˜๋ฉด, JPA๊ฐ€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์•Œ์•„์„œ ๋ณ€๊ฒฝํ•˜๋ฏ€๋กœ @Transactional์ด ํ•„์š”ํ•˜๋‹ค.

 

์ฐธ๊ณ ) ํ˜„์žฌ ์ฃผ๋ฌธ ์„œ๋น„์Šค์˜ ์ฃผ๋ฌธ๊ณผ ์ฃผ๋ฌธ ์ทจ์†Œ ๋ฉ”์†Œ๋“œ๋Š” ์—”ํ‹ฐํ‹ฐ์— ๊ตฌํ˜„๋˜์–ด ์žˆ๋‹ค. ์„œ๋น„์Šค ๊ณ„์ธต์€ ์—”ํ‹ฐํ‹ฐ์˜ ๋ฉ”์†Œ๋“œ๋ฅผ ์ด์šฉํ•œ๋‹ค. ์ด์ฒ˜๋Ÿผ ์—”ํ‹ฐํ‹ฐ๊ฐ€ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๊ฐ€์ง€๊ณ  ๊ฐ์ฒด ์ง€ํ–ฅ์˜ ํŠน์„ฑ์„ ์ ๊ทน ํ™œ์šฉํ•˜๋Š” ๊ฒƒ์„ ๋„๋ฉ”์ธ ๋ชจ๋ธ ํŒจํ„ด์ด๋ผ๊ณ  ํ•œ๋‹ค. ๋ฐ˜๋Œ€๋กœ ์—”ํ‹ฐํ‹ฐ์—๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ์—†๊ณ , ์„œ๋น„์Šค ๊ณ„์ธต์—์„œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์„ ํŠธ๋žœ์žญ์…˜ ์Šคํฌ๋ฆฝํŠธ ํŒจํ„ด์ด๋ผ๊ณ  ํ•œ๋‹ค.

 

์ฃผ๋ฌธ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ

package jpabook.jpashop.service;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderStatus;
import jpabook.jpashop.domain.item.Book;
import jpabook.jpashop.domain.item.Item;
import jpabook.jpashop.exception.NotEnoughStockException;
import jpabook.jpashop.repository.OrderRepository;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.annotation.Transactional;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
@ExtendWith(SpringExtension.class)
@Transactional
class OrderServiceTest {

    private final OrderService orderService;
    private final OrderRepository orderRepository;
    private final EntityManager em;

    @Autowired
    public OrderServiceTest(OrderService orderService, OrderRepository orderRepository, EntityManager em) {
        this.orderService = orderService;
        this.orderRepository = orderRepository;
        this.em = em;
    }

    @Test
    public void ์ƒํ’ˆ์ฃผ๋ฌธ() {
        //given
        Member member = createMember();
        Item item = createBook("์Šฌ๋žจ๋ฉํฌ", 10000, 10);
        int orderCount = 3;

        //when
        Long orderId = orderService.order(member.getId(), item.getId(), orderCount);

        //then
        Order getOrder = orderRepository.findOne(orderId);

        Assertions.assertEquals(OrderStatus.ORDER, getOrder.getStatus());
        Assertions.assertEquals(1, getOrder.getOrderItems().size());
        Assertions.assertEquals(10000*3, getOrder.getTotalPrice());
        Assertions.assertEquals(7, item.getStockQuantity());
    }
    
    @Test
    public void ์ƒํ’ˆ์ฃผ๋ฌธ_์žฌ๊ณ ์ˆ˜๋Ÿ‰์ดˆ๊ณผ() {
        //given
        Member member = createMember();
        Item item = createBook("์Šฌ๋žจ๋ฉํฌ", 10000, 1);
        int orderCount = 3;

        //when

        //then
        Assertions.assertThrows(NotEnoughStockException.class, ()->orderService.order(member.getId(), item.getId(), orderCount));
    }

    @Test
    public void ์ฃผ๋ฌธ์ทจ์†Œ() {
        //given
        Member member = createMember();
        Item item = createBook("์Šฌ๋žจ๋ฉํฌ", 10000, 10);
        int orderCount = 3;

        Long orderId = orderService.order(member.getId(), item.getId(), orderCount);

        //when
        orderService.cancelOrder(orderId);

        //then
        Order getOrder = orderRepository.findOne(orderId);
        Assertions.assertEquals(getOrder.getStatus(), OrderStatus.CANCEL);
        Assertions.assertEquals(10, item.getStockQuantity());
    }

    private Member createMember(){
        Member member = new Member();
        member.setName("๊ฐ•๋Œ€๋งŒ");
        member.setAddress(new Address("์„œ์šธ", "์‚ฐ์™•", "123-123"));
        em.persist(member);
        return member;
    }

    private Book createBook(String name, int price, int stockQuantity){
        Book book = new Book();
        book.setName(name);
        book.setStockQuantity(stockQuantity);
        book.setPrice(price);
        em.persist(book);
        return book;
    }
}

 

์ฃผ๋ฌธ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ

querydsl ๋ฐฐ์šฐ๊ณ  ์ž‘์„ฑ

 

์›น ๊ณ„์ธต ๊ฐœ๋ฐœ

ํ™ˆ ํ™”๋ฉด๊ณผ ๋ ˆ์ด์•„์›ƒ

HomeController

package jpabook.jpashop.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@Slf4j  // ๋กœ๊ทธ๋ฅผ ์œ„ํ•œ ์• ๋…ธํ…Œ์ด์…˜
public class HomeController {

    @RequestMapping("/")
    public String home(){
        log.info("home controller");
        return "home";
    }
}

 

home.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header">
    <title>Hello</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<div class="container">
    <div th:replace="fragments/bodyHeader :: bodyHeader" />
    <div class="jumbotron">
        <h1>HELLO SHOP</h1>
        <p class="lead">ํšŒ์› ๊ธฐ๋Šฅ</p>
        <p>
            <a class="btn btn-lg btn-secondary" href="/members/new">ํšŒ์› ๊ฐ€์ž…</a>
            <a class="btn btn-lg btn-secondary" href="/members">ํšŒ์› ๋ชฉ๋ก</a>
        </p>
        <p class="lead">์ƒํ’ˆ ๊ธฐ๋Šฅ</p>
        <p>
            <a class="btn btn-lg btn-dark" href="/items/new">์ƒํ’ˆ ๋“ฑ๋ก</a>
            <a class="btn btn-lg btn-dark" href="/items">์ƒํ’ˆ ๋ชฉ๋ก</a>
        </p>
        <p class="lead">์ฃผ๋ฌธ ๊ธฐ๋Šฅ</p>
        <p>
            <a class="btn btn-lg btn-info" href="/order">์ƒํ’ˆ ์ฃผ๋ฌธ</a>
            <a class="btn btn-lg btn-info" href="/orders">์ฃผ๋ฌธ ๋‚ด์—ญ</a>
        </p>
    </div>
    <div th:replace="fragments/footer :: footer" />
</div> <!-- /container -->
</body>
</html>

 

ํšŒ์› ๋“ฑ๋ก

MemberForm

package jpabook.jpashop.controller;

import jakarta.validation.constraints.NotEmpty;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class MemberForm {

    @NotEmpty(message = "ํšŒ์› ์ด๋ฆ„์€ ํ•„์ˆ˜")
    private String name;

    private String city;

    private String street;

    private String zipcode;
}

Member๋กœ ์ „๋‹ฌํ•ด๋„ ๋˜๊ธด ๋˜์ง€๋งŒ, Form๊ณผ ์ •ํ™•ํ•˜๊ฒŒ ๋งž์ง€ ์•Š๊ณ , @NotEmpty ๊ฐ™์€ ์ถ”๊ฐ€ ๊ธฐ๋Šฅ์„ ๋„ฃ์œผ๋ฉด ๋ณต์žกํ•ด์ง€๊ธฐ ๋•Œ๋ฌธ์— MemberForm์„ ์ƒˆ๋กœ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.

 

MemberController

package jpabook.jpashop.controller;

import jakarta.validation.Valid;
import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;

    @GetMapping("/members/new")
    public String createForm(Model model){
        model.addAttribute("memberForm", new MemberForm()); // MemberForm์„ ๋งŒ๋“ค์–ด์„œ ์ „๋‹ฌ
        return "members/createMemberForm";
    }

    @PostMapping("/members/new")
    public String create(@Valid MemberForm form, BindingResult result){   // @NotEmpty ๊ฒ€์ฆ, ๊ฒฐ๊ณผ๋ฅผ ๋ชจ๋‘ result ๋ณ€์ˆ˜์— ์ €์žฅ, MemberForm๊ณผ result๋ฅผ return์— ๊ฐ€์„œ ์“ธ ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์คŒ
        if (result.hasErrors()){
            return "/members/createMemberForm";
        }

        Address address = new Address(form.getCity(), form.getStreet(), form.getZipcode());

        Member member = new Member();
        member.setName(form.getName());
        member.setAddress(address);

        Long memberId = memberService.join(member);
        return "redirect:/";
    }
}

ํšŒ์› ๊ฐ€์ž…์„ ๋ˆ„๋ฅด๋ฉด, /members/new๋กœ GET Request๋ฅผ ๋ณด๋‚ธ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด MemberForm๊ณผ ํ•จ๊ป˜ ํšŒ์› ๊ฐ€์ž… ํŽ˜์ด์ง€๋ฅผ ๋„์šด๋‹ค.

 

ํšŒ์› ๊ฐ€์ž… ํŽ˜์ด์ง€์—์„œ POST Request๋ฅผ ๋ณด๋‚ด๋ฉด, MemberForm์— ๋ฐ˜์˜๋˜์–ด ์ „๋‹ฌ๋ฐ›๋Š”๋‹ค. @Valid๋Š” @NotEmpty์ด์–ด์•ผ ํ•˜๋Š” ์ด๋ฆ„์„ ๊ฒ€์ฆํ•˜๋ฉฐ, BindingResult๋ฅผ ํ†ตํ•ด ์˜ค๋ฅ˜ ๊ฐ™์€ ๊ฒฐ๊ณผ๋ฅผ result์— ์ €์žฅํ•œ๋‹ค. ์ด๊ฒƒ๋“ค์€ return ํ•˜๋Š” ๊ณณ์— ๊ฐ™์ด ๋ณด๋‚ธ๋‹ค.

 

Member ๋ชฉ๋ก ์กฐํšŒ

MemberController

@Controller
@RequiredArgsConstructor
public class MemberController {
	...
    
    @GetMapping("/members")
    public String list(Model model){
        List<Member> members = memberService.findMembers();
        model.addAttribute("members", members);
        return "members/memberList";
    }
}

 

memberList.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<body>
<div class="container">
    <div th:replace="fragments/bodyHeader :: bodyHeader" />
    <div>
        <table class="table table-striped">
            <thead>
            <tr>
                <th>#</th>
                <th>์ด๋ฆ„</th>
                <th>๋„์‹œ</th>
                <th>์ฃผ์†Œ</th>
                <th>์šฐํŽธ๋ฒˆํ˜ธ</th>
            </tr>
            </thead>
            <tbody>
            <tr th:each="member : ${members}">
                <td th:text="${member.id}"></td>
                <td th:text="${member.name}"></td>
                <td th:text="${member.address?.city}"></td>	// ?๋Š” null์ด๋ฉด ์ง„ํ–‰ํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š” ์˜๋ฏธ
                <td th:text="${member.address?.street}"></td>
                <td th:text="${member.address?.zipcode}"></td>
            </tr>
            </tbody>
        </table>
    </div>
    <div th:replace="fragments/footer :: footer" />
</div> <!-- /container -->
</body>
</html>

 

์ƒํ’ˆ ๋“ฑ๋ก

ItemController

package jpabook.jpashop.controller;

import jpabook.jpashop.domain.item.Book;
import jpabook.jpashop.domain.item.Item;
import jpabook.jpashop.service.ItemService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import java.util.List;

@Controller
@RequiredArgsConstructor
public class ItemController {
    private final ItemService itemService;

    @GetMapping("/items/new")
    public String newItem(Model model){
        BookForm bookForm = new BookForm();
        model.addAttribute("form", bookForm);
        return "items/createItemForm";
    }

    @PostMapping("/items/new")
    public String create(BookForm bookForm){
        Book book = Book.setFromForm(bookForm);

        itemService.saveItem(book);
        return "redirect:/";
    }
}

Setter๋Š” ์ตœ๋Œ€ํ•œ ์ž์ œํ•˜๊ณ , ์—”ํ‹ฐํ‹ฐ์— ๋ฉ”์†Œ๋“œ๋ฅผ ๋งŒ๋“ค์–ด ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.

 

Book (์ƒ์„ฑ ๋ฉ”์†Œ๋“œ ์ถ”๊ฐ€)

package jpabook.jpashop.domain.item;

import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
import jpabook.jpashop.controller.BookForm;
import lombok.Getter;
import lombok.Setter;

@Entity
@DiscriminatorValue("B")
@Getter
@Setter
public class Book extends Item {
    private String author;
    private String isbn;

    public static Book setFromForm(BookForm bookForm){
        Book book = new Book();
        book.setPrice(bookForm.getPrice());
        book.setName(bookForm.getName());
        book.setStockQuantity(bookForm.getStockQuantity());
        book.setId(bookForm.getId());
        book.setIsbn(bookForm.getIsbn());
        book.setAuthor(bookForm.getAuthor());
        return book;
    }
}

 

์ƒํ’ˆ ๋ชฉ๋ก

@Controller
@RequiredArgsConstructor
public class ItemController {
    ...
    
    @GetMapping("/items")
    public String list(Model model){
        List<Item> items = itemService.findItems();
        model.addAttribute("items", items);
        return "items/itemList";
    }
}

 

itemList.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<body>
<div class="container">
    <div th:replace="fragments/bodyHeader :: bodyHeader"/>
    <div>
        <table class="table table-striped">
            <thead>
            <tr>
                <th>#</th>
                <th>์ƒํ’ˆ๋ช…</th>
                <th>๊ฐ€๊ฒฉ</th>
                <th>์žฌ๊ณ ์ˆ˜๋Ÿ‰</th>
                <th></th>
            </tr>
            </thead>
            <tbody>
            <tr th:each="item : ${items}">
                <td th:text="${item.id}"></td>
                <td th:text="${item.name}"></td>
                <td th:text="${item.price}"></td>
                <td th:text="${item.stockQuantity}"></td>
                <td>
                    <a href="#" th:href="@{/items/{id}/edit (id=${item.id})}"
                       class="btn btn-primary" role="button">์ˆ˜์ •</a>
                </td>
            </tr>
            </tbody>
        </table>
    </div>
    <div th:replace="fragments/footer :: footer"/>
</div> <!-- /container -->
</body>
</html>

 

์ƒํ’ˆ ์ˆ˜์ •

ItemController

package jpabook.jpashop.controller;

import jpabook.jpashop.domain.item.Book;
import jpabook.jpashop.domain.item.Item;
import jpabook.jpashop.service.ItemService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;

import java.util.List;

@Controller
@RequiredArgsConstructor
public class ItemController {
    private final ItemService itemService;

    @GetMapping("/items/new")
    public String newItem(Model model){
        BookForm bookForm = new BookForm();
        model.addAttribute("form", bookForm);
        return "items/createItemForm";
    }

    @PostMapping("/items/new")
    public String create(BookForm bookForm){
        Book book = Book.setFromForm(bookForm);

        itemService.saveItem(book);
        return "redirect:/";
    }

    @GetMapping("/items")
    public String list(Model model){
        List<Item> items = itemService.findItems();
        model.addAttribute("items", items);
        return "items/itemList";
    }

    @GetMapping("/items/{itemId}/edit")
    public String updateItemForm(@PathVariable("itemId") Long itemId, Model model){
        Book item = (Book)itemService.findOne(itemId);

        BookForm form = new BookForm();
        form.setId(item.getId());
        form.setName(item.getName());
        form.setPrice(item.getPrice());
        form.setStockQuantity(item.getStockQuantity());
        form.setAuthor(item.getAuthor());
        form.setIsbn(item.getIsbn());

        model.addAttribute("form", form);
        return "items/updateItemForm";
    }

    @PostMapping("items/{itemId}/edit")
    public String updateItem(@ModelAttribute("form") BookForm form){    // form์œผ๋กœ ์ „๋‹ฌ๋ฐ›์€ Model Attribute๋ฅผ ๊ฐ€์ ธ์˜ด. ์—†์–ด๋„ ๋จ.
        Book book = Book.setFromForm(form);

        itemService.saveItem(book);
        return "redirect:/items";
    }
}

createItemForm๊ณผ updateItemForm์€ ๋ณ„ ์ฐจ์ด๊ฐ€ ์—†๋‹ค. ์ƒํ’ˆ์„ ๋“ฑ๋ก(create)ํ•  ๋•Œ๋Š” ๊ฐ’์ด ์ง€์ •๋˜์ง€ ์•Š์€ ์ƒˆ๋กœ์šด Form์„ ๋งŒ๋“ค์–ด์„œ ์ „๋‹ฌํ•˜๋ฏ€๋กœ ์ž…๋ ฅ ์นธ๋“ค์ด ๋น„์–ด์žˆ์ง€๋งŒ, ์ƒํ’ˆ์„ ์ˆ˜์ •(update)ํ•  ๋•Œ๋Š” id๋กœ๋ถ€ํ„ฐ Book์„ ์ฐพ๊ณ , ๊ทธ ์ •๋ณด๋ฅผ Form์— ๋ฐ˜์˜ํ•˜์—ฌ ์ „๋‹ฌํ•˜๋ฏ€๋กœ ์ž…๋ ฅ ์นธ๋“ค์ด ์ฑ„์›Œ์ ธ ์žˆ๋‹ค.

 

๋ณ€๊ฒฝ ๊ฐ์ง€์™€ ๋ณ‘ํ•ฉ โ˜…โ˜…โ˜…โ˜…โ˜…

์ค€์˜์† ์—”ํ‹ฐํ‹ฐ

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๊ฐ”๋‹ค์™€์„œ id๊ฐ€ ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๊ฐ์ฒด๋ฅผ ๋งํ•œ๋‹ค. ์ƒˆ๋กœ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค๊ณ  id๋ฅผ ๋ถ€์—ฌํ•ด๋„ ์ค€์˜์† ์—”ํ‹ฐํ‹ฐ๋กœ ๊ฐ„์ฃผํ•œ๋‹ค. ์ค€์˜์† ์—”ํ‹ฐํ‹ฐ๋Š” ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ๊ฐ€ ๋”๋Š” ๊ด€๋ฆฌํ•˜์ง€ ์•Š๋Š”๋‹ค. ์ฆ‰, ๋ณ€๊ฒฝ์ด ์ผ์–ด๋‚˜๋„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋ฐ˜์˜๋˜์ง€ ์•Š๋Š”๋‹ค.

 

์ค€์˜์† ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ˆ˜์ •ํ•˜๋Š” 2๊ฐ€์ง€ ๋ฐฉ๋ฒ•

  • ๋ณ€๊ฒฝ ๊ฐ์ง€ ๊ธฐ๋Šฅ ์‚ฌ์šฉ
  • ๋ณ‘ํ•ฉ ์‚ฌ์šฉ

 

๋ณ€๊ฒฝ ๊ฐ์ง€ ๊ธฐ๋Šฅ = ๋”ํ‹ฐ ์ฒดํ‚น

@Transactional
void update(Item itemParam) { //itemParam: ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋„˜์–ด์˜จ ์ค€์˜์† ์ƒํƒœ์˜ ์—”ํ‹ฐํ‹ฐ
	Item findItem = em.find(Item.class, itemParam.getId()); // ๊ฐ™์€ ์˜์†์„ฑ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์กฐํšŒ
	findItem.setPrice(itemParam.getPrice()); // ์˜์†์„ฑ ์—”ํ‹ฐํ‹ฐ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ •ํ•˜๋ฏ€๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋ฐ˜์˜๋จ
}

 

(์˜ˆ์‹œ)

@Transactional
public Item updateItem(Long itemId, Book param){
    Item findItem = itemRepository.findOne(itemId);
    findItem.setPrice(param.getPrice());
    findItem.setName(param.getName());
    findItem.setStockQuantity(param.getStockQuantity());
    return findItem;
}

 

๋ณ‘ํ•ฉ ์‚ฌ์šฉ

๋ณ‘ํ•ฉ์€ ์ค€์˜์† ์ƒํƒœ์˜ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์˜์† ์ƒํƒœ์˜ ์—”ํ‹ฐํ‹ฐ๋กœ ๋ณ€๊ฒฝํ•œ๋‹ค.

@Transactional
void update(Item itemParam) { //itemParam: ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋„˜์–ด์˜จ ์ค€์˜์† ์ƒํƒœ์˜ ์—”ํ‹ฐํ‹ฐ
	Item mergeItem = em.merge(itemParam);
}

itemParam์ด ์ „๋‹ฌ๋˜๋ฉด, EntityManager๊ฐ€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ itemParam๊ณผ ๊ฐ™์€ id๋ฅผ ๊ฐ€์ง€๋Š” ๊ฒƒ์„ ์ฐพ์•„ ๋ชจ๋‘ itemParam๊ณผ ๊ฐ™๊ฒŒ ๋งŒ๋“ ๋‹ค.

๋ณ€๊ฒฝ ๊ฐ์ง€์™€ ๋‹ฌ๋ฆฌ ๋ณ‘ํ•ฉ์€ ๋ชจ๋“  ํ•„๋“œ๋ฅผ ๋ชจ๋‘ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์œ„ํ—˜ํ•˜๋‹ค. ๊ทธ๋ž˜์„œ ์ž…๋ ฅ๊ฐ’์ด ์ „๋‹ฌ๋˜์ง€ ์•Š์•˜์„ ๊ฒฝ์šฐ, null๋กœ ๋ณ€๊ฒฝ๋  ์ˆ˜ ์žˆ๋‹ค. ๋”ฐ๋ผ์„œ ๋ณ‘ํ•ฉ์€ ์ž์ œํ•˜๊ณ , ๋ณ€๊ฒฝ ๊ฐ์ง€๋ฅผ ์‚ฌ์šฉํ•˜์ž.

 

์—”ํ‹ฐํ‹ฐ๋ฅผ ๋ณ€๊ฒฝํ•  ๋•Œ๋Š” ํ•ญ์ƒ ๋ณ€๊ฒฝ ๊ฐ์ง€๋ฅผ ์‚ฌ์šฉ ๊ถŒ์žฅ

  • ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์–ด์„คํ”„๊ฒŒ ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ ๊ธˆ์ง€
  • ํŠธ๋žœ์žญ์…˜์ด ์žˆ๋Š” ์„œ๋น„์Šค ๊ณ„์ธต์— ์‹๋ณ„์ž(id)์™€ ๋ณ€๊ฒฝํ•  ๋ฐ์ดํ„ฐ๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ์ „๋‹ฌ(ํŒŒ๋ผ๋ฏธํ„ฐ or dto)
  • ํŠธ๋žœ์žญ์…˜์ด ์žˆ๋Š” ์„œ๋น„์Šค ๊ณ„์ธต์—์„œ ์˜์† ์ƒํƒœ์˜ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์กฐํšŒํ•˜๊ณ , ์—”ํ‹ฐํ‹ฐ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ง์ ‘ ๋ณ€๊ฒฝ
  • ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ ์‹œ์ ์— ๋ณ€๊ฒฝ ๊ฐ์ง€๊ฐ€ ์‹คํ–‰

ItemService

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ItemService {

    ...

    @Transactional
    public Item updateItem(Long itemId, String name, int price, int stockQuantity){
        Item findItem = itemRepository.findOne(itemId);
        findItem.setName(name);
        findItem.setPrice(price);
        findItem.setStockQuantity(stockQuantity);
        return findItem;
    }
}

 

ItemController

package jpabook.jpashop.controller;

import jpabook.jpashop.domain.item.Book;
import jpabook.jpashop.domain.item.Item;
import jpabook.jpashop.repository.ItemRepository;
import jpabook.jpashop.service.ItemService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;

import java.util.List;

@Controller
@RequiredArgsConstructor
public class ItemController {
    private final ItemService itemService;
    private final ItemRepository itemRepository;

    @GetMapping("/items/new")
    public String newItem(Model model){
        BookForm bookForm = new BookForm();
        model.addAttribute("form", bookForm);
        return "items/createItemForm";
    }

    @PostMapping("/items/new")
    public String create(BookForm bookForm){
        Book book = Book.setFromForm(bookForm);

        itemService.saveItem(book);
        return "redirect:/";
    }

    @GetMapping("/items")
    public String list(Model model){
        List<Item> items = itemService.findItems();
        model.addAttribute("items", items);
        return "items/itemList";
    }

    @GetMapping("/items/{itemId}/edit")
    public String updateItemForm(@PathVariable("itemId") Long itemId, Model model){
        Book item = (Book)itemService.findOne(itemId);

        BookForm form = new BookForm();
        form.setId(item.getId());
        form.setName(item.getName());
        form.setPrice(item.getPrice());
        form.setStockQuantity(item.getStockQuantity());
        form.setAuthor(item.getAuthor());
        form.setIsbn(item.getIsbn());

        model.addAttribute("form", form);
        return "items/updateItemForm";
    }

    @PostMapping("items/{itemId}/edit")
    public String updateItem(@PathVariable("itemId") Long itemId, @ModelAttribute("form") BookForm form){    // form์œผ๋กœ ์ „๋‹ฌ๋ฐ›์€ Model Attribute๋ฅผ ๊ฐ€์ ธ์˜ด. ์—†์–ด๋„ ๋จ.
        itemService.updateItem(form.getId(), form.getName(), form.getPrice(), form.getStockQuantity());
        return "redirect:/items";
    }
}

 

์ƒํ’ˆ ์ฃผ๋ฌธ

OrderController

package jpabook.jpashop.controller;

import jpabook.jpashop.domain.Member;
import jpabook.jpashop.domain.item.Item;
import jpabook.jpashop.service.ItemService;
import jpabook.jpashop.service.MemberService;
import jpabook.jpashop.service.OrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

@Controller
@RequiredArgsConstructor
public class OrderController {

    private final OrderService orderService;
    private final MemberService memberService;
    private final ItemService itemService;

    @GetMapping("/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("/order")
    public String order(@RequestParam("memberId") Long memberId, @RequestParam("itemId") Long itemId, @RequestParam("count") int count){
        Long orderId = orderService.order(memberId, itemId, count);
        return "redirect:/orders";
    }
}

 

์ฃผ๋ฌธ ๋ชฉ๋ก ๊ฒ€์ƒ‰

@Controller
@RequiredArgsConstructor
public class OrderController {

	...

    @GetMapping("/orders")
    public String orderList(@ModelAttribute("orderSearch") OrderSearch orderSearch, Model model){
        List<Order> orders = orderService.findOrders(orderSearch);

        model.addAttribute("orders", orders);

        return "order/orderList";
    }
}

 

 

orderList.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header"/>
<body>
<div class="container">
    <div th:replace="fragments/bodyHeader :: bodyHeader"/>
    <div>
        <div>
            <form th:object="${orderSearch}" class="form-inline">
                <div class="form-group mb-2">
                    <input type="text" th:field="*{memberName}" class="formcontrol"
                           placeholder="ํšŒ์›๋ช…"/>
                </div>
                <div class="form-group mx-sm-1 mb-2">
                    <select th:field="*{orderStatus}" class="form-control">
                        <option value="">์ฃผ๋ฌธ์ƒํƒœ</option>
                        <option th:each=
                                        "status : ${T(jpabook.jpashop.domain.OrderStatus).values()}"
                                th:value="${status}"
                                th:text="${status}">option
                        </option>
                    </select>
                </div>
                <button type="submit" class="btn btn-primary mb-2">๊ฒ€์ƒ‰</button>
            </form>
        </div>
        <table class="table table-striped">
            <thead>
            <tr>
                <th>#</th>
                <th>ํšŒ์›๋ช…</th>
                <th>๋Œ€ํ‘œ์ƒํ’ˆ ์ด๋ฆ„</th>
                <th>๋Œ€ํ‘œ์ƒํ’ˆ ์ฃผ๋ฌธ๊ฐ€๊ฒฉ</th>
                <th>๋Œ€ํ‘œ์ƒํ’ˆ ์ฃผ๋ฌธ์ˆ˜๋Ÿ‰</th>
                <th>์ƒํƒœ</th>
                <th>์ผ์‹œ</th>
                <th></th>
            </tr>
            </thead>
            <tbody>
            <tr th:each="item : ${orders}">
                <td th:text="${item.id}"></td>
                <td th:text="${item.member.name}"></td>
                <td th:text="${item.orderItems[0].item.name}"></td>
                <td th:text="${item.orderItems[0].orderPrice}"></td>
                <td th:text="${item.orderItems[0].count}"></td>
                <td th:text="${item.status}"></td>
                <td th:text="${item.orderDate}"></td>
                <td>
                    <a th:if="${item.status.name() == 'ORDER'}" href="#"
                       th:href="'javascript:cancel('+${item.id}+')'"
                       class="btn btn-danger">CANCEL</a>
                </td>
            </tr>
            </tbody>
        </table>
    </div>
    <div th:replace="fragments/footer :: footer"/>
</div> <!-- /container -->
</body>
<script>
    function cancel(id) {
        var form = document.createElement("form");
        form.setAttribute("method", "post");
        form.setAttribute("action", "/orders/" + id + "/cancel");
        document.body.appendChild(form);
        form.submit();
    }
</script>
</html>

Form Method์˜ default๋Š” GET์ด๋ฏ€๋กœ, GET Mapping์— ํ•ด๋‹นํ•˜๋Š” ๊ฒƒ์ด ์‹คํ–‰๋œ๋‹ค.

 

OrderSearch (๊ฒ€์ƒ‰ ๊ธฐ์ค€)

package hades.self.form;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class OrderSearch {
    private String memberName;
    private String orderStatus;
}

 

์ฃผ๋ฌธ ์ทจ์†Œ

@Controller
@RequiredArgsConstructor
public class OrderController {

    ...

    @PostMapping("/orders/{orderId}/cancel")
    public String cancel(@PathVariable("orderId") Long orderId){
        orderService.cancelOrder(orderId);
        return "redirect:/orders";
    }
}

 

๊ธฐ์–ตํ•˜๋ฉด ์ข‹์„ ๊ฒƒ

  • @Id๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ๊ฑฐ์ณ์„œ id๋ฅผ ๋ถ€์—ฌํ•˜๊ฒŒ ํ•œ๋‹ค.
  • ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ, ๋˜๋„๋ก์ด๋ฉด id๋ฅผ ๋„˜๊ธฐ๋Š” ๊ฒƒ์ด ์ข‹์ง€๋งŒ, ๋„ˆ๋ฌด ์–ฝ๋ฉ”์ด์ง€ ๋ง ๊ฒƒ.
  • ํด๋ž˜์Šค ๋‚ด๋ถ€์—์„œ this.๋ฉค๋ฒ„๋ณ€์ˆ˜ = ๋ณ€๊ฒฝ๊ฐ’์œผ๋กœ ํ•˜๋˜์ง€ Setter๋ฅผ ์ด์šฉํ•˜๋˜์ง€ ๋„ˆ๋ฌด ์–ฝ๋ฉ”์ด์ง€ ๋ง ๊ฒƒ.
  • ์˜์†์„ฑ ๊ฐ์ฒด์˜ ํ•„๋“œ๊ฐ’์„ ๋ณ€๊ฒฝํ•˜๋ฉด, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ๋„ ๋ณ€๊ฒฝ ๋‚ด์šฉ์ด ๋ฐ˜์˜๋œ๋‹ค. ์˜์†์„ฑ ๊ฐ์ฒด์˜ ํ•„๋“œ๊ฐ’์„ ๋ณ€๊ฒฝํ–ˆ๋Š”๋ฐ, ๋ฐ˜์˜๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด, @Transactional(readonly=true)์ธ ๊ฒƒ์€ ์•„๋‹Œ์ง€ ํ™•์ธํ•  ๊ฒƒ.
  • Model์€ ์ฃผ๊ธฐ๋„ ๊ฐ€๋Šฅํ•˜์ง€๋งŒ, ๋ฐ›๊ธฐ๋„ ๊ฐ€๋Šฅํ•จ.(์ถ”์ •)