์ผ | ์ | ํ | ์ | ๋ชฉ | ๊ธ | ํ |
---|---|---|---|---|---|---|
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 | 29 | 30 | 31 |
- ๋ฌธ์์ด
- ์ต๋จ ๊ฒฝ๋ก
- BFS
- web
- ๊ตฌํ
- ๋ฐฑํธ๋ํน
- ๋ฐ์ดํฌ์คํธ๋ผ
- ์คํ
- ๊ทธ๋ฆฌ๋
- DP
- java
- c++
- Reversing
- GCP
- dfs
- ๋งต
- JPA
- Spring
- thymeleaf
- ์ด๋ถ ํ์
- ์๋ฎฌ๋ ์ด์
- CVE
- dynamic debugging
- ์ฌ๊ท
- ๋ถํ ์ ๋ณต
- ์ฐ์ ์์ ํ
- ๋์ ํฉ
- OS
- error
- ์์ ์ ๋ ฌ
- Today
- Total
hades
[Spring] ์คํ๋ง ๊ธฐ์ด (3) ๋ณธ๋ฌธ
ํ๋ก์ ํธ ํ๊ฒฝ์ค์
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์ ์ฃผ๊ธฐ๋ ๊ฐ๋ฅํ์ง๋ง, ๋ฐ๊ธฐ๋ ๊ฐ๋ฅํจ.(์ถ์ )
'๐๐ปโโ๏ธ ๊ธฐ๋ณธํ๋ จ > Spring' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Thymeleaf] ํ ์คํธ ๋ฆฌํฐ๋ด (0) | 2024.08.05 |
---|---|
[Thymeleaf] ํ ํ๋ฆฟ ์์น (0) | 2024.07.29 |
[Spring] ๊ฐ์ ํด๋์ค์ ๋น์ด ๋ ๊ฐ ์ด์์ผ ๋ (0) | 2024.07.25 |
[Spring] ์คํ๋ง ๊ธฐ์ด (2) (0) | 2024.07.24 |
[Spring] ์คํ๋ง ๊ธฐ์ด (1) (0) | 2024.07.22 |