[JPA 활용1] 1. 프로젝트 생성 / 엔티티

heyhey·2023년 2월 13일
0

Spring

목록 보기
18/23

[JPA 활용1]

스프링부트와 JPA를 사용해보며 Spring에 익숙해져보자
사실 이 편은 따라치며 활용하는 방법을 깨우치는데만 집중해보고, 이걸 다 보면 다시 스프링 핵심원리를 보면서 이해하도록 한다.

역시나 스프링 부트 스타터를 이용해 스프링을 시작한다.
https://start/spring/io/

  • gradle - groovy project
  • thymeleaf,jpa,h2,lombok,validation,web

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()
}

JPA와 DB 설정

  • h2 실행 방법
    터미널로 bin/h2.sh 실행
    JDBC URL : jdbc:h2:~/jpashop

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를 사용했다.

  • PK : primary key

주문 엔티티

@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로 설정한다면 생성자에서 초기화가 불가능해진다.

엔티티 설계시 주의점

  1. Setter를 사용하지 말자
  2. 모든 연관관계를 지연로딩으로 설정하자 .
  • 즉시 로딩은 예측이 어렵다(EAGER) -> 어떤 SQL이 실행될지 추적하기 어렵다.
  • 지연로딩(LAZY)를 이요하도록 하자
  • @XToOne(OneToOne, ManyToOne) 관계는 기본이 즉시로딩이므로 직접 지연로딩으로 설정해야 한다.
  1. 컬렉션은 필드에서 초기화 하자.
  • null 문제에서 안전하다.
profile
주경야독

0개의 댓글