스프링부트와 JPA를 사용해보며 Spring에 익숙해져보자
사실 이 편은 따라치며 활용하는 방법을 깨우치는데만 집중해보고, 이걸 다 보면 다시 스프링 핵심원리를 보면서 이해하도록 한다.
역시나 스프링 부트 스타터를 이용해 스프링을 시작한다.
https://start/spring/io/
build.gradle
plugins {
id 'org.springframework.boot' version '2.4.1'
id 'io.spring.dependency-management' version '1.0.10.RELEASE'
id 'java'
}
group = 'jpabook'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
//JUnit4 추가
testImplementation("org.junit.vintage:junit-vintage-engine") {
exclude group: "org.hamcrest", module: "hamcrest-core"
}
}
test {
useJUnitPlatform()
}
main/resources/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
서비스를 만들 때 가장 중요한 건 테이블 설계이다. 데이터가 서로 어떻게 연관되어 있는지 판단하는 것이 우선순위!
외래키가 있는 곳을 연관관계의 주인으로 정해라.
ex) 자동차와 바퀴가 있다면 바퀴
일대 다 관계에서 다쪽에 외래키가 있기 때문이다.
엔티티를 개발하면서 조회할 일은 엄청 많기 때문에 @Getter는 열어두지만 @Setter는 닫아두자.
엔티티가 언제 변경되는지 추적하기가 힘들어진다.
domain/Member
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String name;
@Embedded
private Address address;
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
Id
: 식별자로 id를 사용하겠다.
GeneratedValue
: 생성전략
@Column(name = "member_id")
: PK 컬럼명을 member_id를 사용했다.
@Entity
@Table(name = "orders")
@Getter @Setter
public class Order {
@Id @GeneratedValue
@Column(name = "order_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id")
private Member member; //주문 회원
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
@OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "delivery_id")
private Delivery delivery; //배송정보
private LocalDateTime orderDate; //주문시간 @Enumerated(EnumType.STRING)
private OrderStatus status; //주문상태 [ORDER, CANCEL]
//==연관관계 메서드==//
public void setMember(Member member) {
this.member = member;
member.getOrders().add(this);
}
public void addOrderItem(OrderItem orderItem) {
orderItems.add(orderItem);
orderItem.setOrder(this);
}
public void setDelivery(Delivery delivery) {
this.delivery = delivery;
delivery.setOrder(this);
}
}
public enum OrderStatus {
ORDER, CANCEL
}
@Entity
@Table(name = "order_item")
@Getter @Setter
public class OrderItem {
@Id @GeneratedValue
@Column(name = "order_item_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "item_id")
private Item item; //주문 상품
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order; //주문
private int orderPrice; //주문 가격 private int count; //주문 수량
}
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "dtype")
@Getter @Setter
public abstract class Item {
@Id @GeneratedValue
@Column(name = "item_id")
private Long id;
private String name;
private int price;
private int stockQuantity;
@ManyToMany(mappedBy = "items")
private List<Category> categories = new ArrayList<Category>();
}
@Entity
@DiscriminatorValue("B")
@Getter @Setter
public class Book extends Item {
private String author;
private String isbn;
}
동일
public class Album extends Item {
private String artist;
private String etc;
}
동일
public class Movie extends Item {
private String director;
private String actor;
}
@Entity
@Getter @Setter
public class Delivery {
@Id @GeneratedValue
@Column(name = "delivery_id")
private Long id;
@OneToOne(mappedBy = "delivery", fetch = FetchType.LAZY)
private Order order;
@Embedded
private Address address;
@Enumerated(EnumType.STRING)
private DeliveryStatus status; //ENUM [READY(준비), COMP(배송)]
}
public enum DeliveryStatus {
READY, COMP
}
@Entity
@Getter @Setter
public class Category {
@Id @GeneratedValue
@Column(name = "category_id")
private Long id;
private String name;
@ManyToMany
@JoinTable(name = "category_item",
joinColumns = @JoinColumn(name = "category_id"),
inverseJoinColumns = @JoinColumn(name = "item_id"))
private List<Item> items = new ArrayList<>();
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private Category parent;
@OneToMany(mappedBy = "parent")
private List<Category> child = new ArrayList<>();
//==연관관계 메서드==//
public void addChildCategory(Category child) {
this.child.add(child);
child.setParent(this);
}
}
@ManyToMany
는 실무에서 사용하지 말것, 중간 테이블에 컬럼을 추가할 수 없고, 세밀한 쿼리 실행이 불가하다.
중간 엔티티를 만들어서@OneToMany
,@ManyToOne
으로 매핑해서 사용하자.
@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;
}
}
protected
기본 생성자를 protected로 설정한다면 생성자에서 초기화가 불가능해진다.