본문 바로가기
스프링/JPA

JPA 활용 1

by 공부 안하고 싶은 사람 2021. 4. 23.
반응형
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    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'
}

 

H2

jdbc:h2:~/jpashop -> 초기에 파일로 접근(최소 한번, 세션키 유지한 상태로 실행)

-> ~/jpashop.mv.db 파일 생성

jdbc:h2:tcp://localhost/~/jpashop -> 이후에 tcp로 접근

spring:
  datasource:
    url: jdbc:h2:tcp://localhost/~/jpashop;MVCC=TRUE
    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

 

JPA

persistence.xml, LocalContainerEntityManagerFactoryBean 설정 없이 Boot에서 자동 설정해준다.

 

 

설계

  • 회원 - 주문 : 다대일 양방향 -> 주인관계 정해야함 -> 주문이 외래키 보유하므로 주문
  • 주문상품 - 주문 : 다대일 양방향 관계 -> 주문상품이 외래키보유
  • 주문상품 - 상품 : 다대일 단방향
  • 주문 - 배송 : 일대일 단방향
  • 카테고리와 상품 : 다대다 관계

 

주의점

  • @Getter는 열어두고, @Setter는 꼭 필요한 경우에만 사용하는것을 추천
    (유지보수를 위해 변경 비즈니스 메서드를 별도로 만드는게 좋다)
  • 값 타입은 변경불가능하게 설계해야한다.
    @Setter는 제거하고, 생성자 부여
    @Embeddable은 리플랙션/프록시 같은 기술 사용할 수 있도록, 기본 생성자 필요(protected로 선언하는것을 추천)
  • 모든 연관관계는 지연로딩(LAZY)으로 (@ManyToOne, @OneToOne 즉시로딩이 디폴트이므로 주의)
    entitymanager 사용으로 가져오면 상관없지만,
    JPQL 사용시 N+1문제 발생 -> fetch join, 엔티티 그래프 활용
  • 컬렉션 필드는 필드에서 초기에 직접 초기화하자.
    null safe
    영속성 관리를 위해 컬렉션 필드를 감싸서 추적한다. ( 초기화이후 변경되지 않도록 하자 )
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
  • 엔티티 필드명 -> 테이블 컬럼명 (캐멀 케이스 사용) -> 설정으로 변경은 가능
  • @OneToMany(mappedBy = "order", cascade = CascadeType.ALL) // 연관 테이블을 persist 한번으로 저장가능
    private List<OrderItem>; orderItems = new ArrayList<>();
  • 양방향 연관관계 편의 메서드
public class Order
    ...
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);
}

 

 

의존성 주입

 

  1. 필드 주입 (비추)
@Autowired
private MemberRepository memberRepository;
  1. 생성자 주입
private final MemberRepository memberRepository;
@Autowired
public MemberService(MemberRepository memberRepository) {
    this.memberRepository = memberRepository;
} // 의존성 주입 교체 가능, final으로 컴파일 시점 오류 검사 가능
  1. @RequiredArgsConstructor // final 필드 생성자 생성
    public class MemberService {
     private final MemberRepository memberRepository;
  2.  
@Repository
@RequiredArgsConstructor
public class MemberRepository {
    private final EntityManager em; 
    // 부트에선 @PersistenceContext 대신 @Autowired로 인젝션 가능하기 때문에 이렇게 선언가능

 

 

테스트

src/test/resources/appication.yml 이 있다면, 테스트환경시 이쪽을 참조

또, DB properties를 비워놓으면 default로 메모리 DB로 실행

@Transactional -> Test에선 실행 뒤 롤백이 default

@NoArgsConstructor(access = AccessLevel.PROTECTED) -> 다른 생성자로인한 코드 분산 막기

도메인 모델 패턴(JPA추천) - 서비스 계층은 단순히 엔티티에 필요한 요청을 위임, 엔티티가 비즈니스 로직 가짐

드랜잭션 스크립트 패턴 - 반대로 엔티티에 비즈니스 로직 없고, 서비스가 비즈니스 로직을 처리

 

 

thymeleaf

@Valid + @NotEmpty(message = "회원 이름은 필수입니다.") + BindingResult -> 백단의 에러를 뷰까지 끌어감

<input type="text" th:field="*{name}" class="form-control" placeholder="이름을 입력하세요"
                   th:class="${#fields.hasErrors('name')}? 'form-control fieldError' : 'form-control'">
<p th:if="${#fields.hasErrors('name')}" th:errors="*{name}">Incorrect date</p>

? -> null 처리

<!-- fragments 사용 (like include) -->
<div th:replace="fragments/footer :: footer"></div>
<!-- 반복 -->
<tr th:each="member : ${members}">
    <td th:text="${member.id}"></td>
    <td th:text="${member.name}"></td>
    <td th:text="${member.address?.city}"></td>
    <td th:text="${member.address?.street}"></td>
    <td th:text="${member.address?.zipcode}"></td>
</tr>
<!-- 가변 uri -->
<a href="#" th:href="@{/items/{id}/edit (id=${item.id})}" class="btn btn-primary" role="button">수정</a>
<!-- enum 타입 뿌리기 -->
<option th:each="status : ${T(jpabook.jpashop.domain.OrderStatus).values()}"
        th:value="${status}"
        th:text="${status}">option

 

 

변경 감지 & 병합

준영속성 엔티티 - 영속성 컨텍스트가 더이상 관리하지 않는 엔티티
(DB에 한 번 저장되었다가, 다시 만들어진 식별자(id)가진 엔티티 포함)

-> 수정 방법 2가지

  • 변경 감지 : @Transactional + find/set ( set보단 의미있는 메서드 사용)
  • 병합(merge) : em.merge()
  • 내부적으론 같은 동작을 하지만, 병합시 null 업데이트 위험이 있으므로
    변경감지로 원하는 필드만 교체하는것을 추천
  • -> Dto는 컨트롤러 단에서만 사용하도록 하며, 트랜잭션이 있는 서비스 계층에서 영속 상태의 엔티티를 조회후, 변경감지 사용

 

Git : https://github.com/kimdongkyu5902/Jpashop-Study

728x90
반응형

'스프링 > JPA' 카테고리의 다른 글

전체 컬럼 매핑은 필수? DB default값  (0) 2021.05.26
JPA 상속 관계 (TABLE_PER_CLASS전략)  (0) 2021.05.21
JPA 활용 2  (0) 2021.04.23
자바 ORM 표준 JPA  (0) 2021.04.16
스프링-입문-스프링부트 + JPA  (0) 2021.04.16

댓글