[SpringBoot]스프링부트 쇼핑몰 프로젝트 with JPA(연관 관계 매핑)

hwa.haha·2022년 11월 21일
0

SpringBoot

목록 보기
4/7
post-thumbnail

연관관계 매핑종류

엔티티들은 대부분 다른 엔티티와 연관 관계를 맺고 있습니다. JPA에서는 엔티티에 연관 관계를 매핑해두고 필요할 때 해당 엔티티와 연관된 엔티티를 사용하여 좀 더 객체지행적으로 프로그래밍 할 수 있도록 도와줍니다. 연관 관꼐 매핑의 기초를 알아보겠습니다.

일대일(1:1) @OneToOne
일대다(1:N) @OneToMany
*다대일(N:1) @ManyToOne
다대다(N:M) @ManyToMany

엔티티 매핑의 방형성은 단방향/ 양방향

Cart

CartTest

package com.shop.shop.entity;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.web.bind.annotation.GetMapping;

import javax.persistence.*;

@Entity
@Table(name = "cart")
@Getter
@Setter
@ToString
public class Cart {

    @Id
    @Column(name = "cart_id")
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @OneToOne
    @JoinColumn(name = "member_id")
    private Member member;

}

@OneToOne 어노테이션을 이용해 회원 엔티티와 일대일로 매핑을합니다.
@JoinColumn 어노테이션을 이용해 매핑할 외래키를 지정합니다. name속성에는 매핑할 외래키릐 이름을 설정합니다. @JoinColumn 의 name을 명시하지 않으면 JPA가 알아서 ID를 찾지만 컬럼명이 원하는 대로 생성되지 않을수 있기 때문에 직접 지정하겠습니다.


회원엔티티에는 장바구니엔티티 관련소스가 전혀없고 장바구니엔티티가 일방적으로 회원 엔티티를 참조하고 있습니다.장바구니와 회원은 일대일로 매핑되어 있으며, 장바구니 엔티티가 회원엔티티를 참조하는 일대일 단방향 매핑입니다.

CartTest

package com.shop.shop.repository;

import com.shop.shop.dto.MemberFormDto;
import com.shop.shop.entity.Cart;
import com.shop.shop.entity.Member;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.TestPropertySource;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import javax.persistence.EntityNotFoundException;
import javax.persistence.PersistenceContext;

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


@SpringBootTest
@Transactional
@TestPropertySource(locations= "classpath:application-test.properties")

class CartTest {

    @Autowired
    CartRepository cartRepository;

    @Autowired
    MemberRepository memberRepository;

    @Autowired
    PasswordEncoder passwordEncoder;

    @PersistenceContext
    EntityManager em;

    public Member createMember(){
        MemberFormDto memberFormDto = new MemberFormDto();
        memberFormDto.setEmail("test@email.com");
        memberFormDto.setName("홍길동");
        memberFormDto.setAddress("가산");
        memberFormDto.setPassword("1234");

        return  Member.createMember(memberFormDto,passwordEncoder);
    }

    @Test
    @DisplayName("장바구니 회원 엔티티 매핑 조회 하기")
    public  void findCartAndMemberTest(){
        Member member = createMember();
        memberRepository.save(member);

        Cart cart = new Cart();
        cart.setMember(member);
        cartRepository.save(cart);

        em.flush();
        em.clear();

        Cart savedCart = cartRepository.findById(cart.getId())
                .orElseThrow(EntityNotFoundException::new);
        assertEquals(savedCart.getMember().getId(),member.getId());
        
    }


}

CatrItem

package com.shop.shop.entity;


import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;

@Entity
@Setter
@Getter
@Table(name = "cart_item")
public class CatrItem {

    @Id
    @GeneratedValue
    @Column(name = "cart_item_id")
    private Long id;


    @ManyToOne //cartItem 입장에선 Many
    @JoinColumn(name = "cart_id") //매핑이 되어진다.
    private  Cart cart;

    @ManyToOne
    @JoinColumn(name = "item_id")
    private  Item item;


    private  int count;


}

하나의 장바구니에는 여러 개의 상품을 담을 수 있으므로 @ManyToOne 어노테이션을 이용하여 다대일 관계로 매핑합니다.
장바구니에 담을 상품의 정보를 알아야 하므로, 상품 엔티티를 매핑합니다.
하나의 상품은 여러 장바구니의 장바구니 상품으로 담길수 있으므로 마찬가지로 @ManyToOne 어노테이션을 이용하여 다대일 관계로 매핑합니다.
같은 상품을 장바구니에 몇개 담을 지 저장합니다.

@JoinColumn 어노테이션의 name 으로 설장한 값이 foreign key로 추가되며 @JoinColumn 어노테이션을 사용하는 엔티티 컬럼명이 추가된다고 생각하면 됩니다.

OrderStatus


package com.shop.shop.constant;


public enum OrderStatus {
    ORDER, CANCEL
}

Order

package com.shop.shop.entity;

import com.shop.shop.constant.OrderStatus;
import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

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

    @Id
    @Column(name = "order_id")
    private Long id;

    @ManyToOne//주문만 알수있음 멤버도 내용을 알려면 양방향이 되어야한다.
    @JoinColumn(name = "member_id")
    private Member member;

    private LocalDateTime orderDate; //주문일

    @Enumerated(EnumType.STRING)
    private OrderStatus orderStatus; //주문상태
    
    @OneToMany(mappedBy = "order")//주인이 아닌쪽은 연관관계매핑시 mappyBy속성의 값으로 연관관계의 주인설정(변수명)
    private List<OrderItem>  orderItems = new ArrayList<>();
    //객체선언하면 연관관계 매핑해주어야한다. 

    private  LocalDateTime regTime;

    private  LocalDateTime updateTime;


정렬할때 사용하는 'order'키워드가 있기 때문에Order 엔티티에 매핑되는 테이블로 "orders"를 지정합니다. 한명의 회원은 여러번 주문을 할 수 있으므로 주문 엔티티 기준에서 다대일 단방향 매핑을 합니다.

import java.time.LocalDateTime;

@Entity
@Getter
@Setter
public class OrderItem {

    @Id
    @GeneratedValue
    @Column(name = "order_item_id")
    private LocalDateTime id;

    @ManyToOne//어떤 상품인지 알수 있음
    @JoinColumn(name = "item_id")
    private  Item item;

    @ManyToOne//어떤주문인지 알수있음
    @JoinColumn(name = "order_id")
    private Order order;

    private  int orderPrice;

    private  int count;

    private  LocalDateTime regTime;

    private  LocalDateTime updateTime;



}

하나의 상품은 여러 주문 상품으로 들어갈 수 있으므로 주문 상품 기준으로 다대일 단방향 매핑을 설정합니다.
한 번의 주문에 여러개이 상품을 주문 할수 있으므로 주문 상품 엔티티와 주문 엔티티를 다대일 단방향 패일을 먼저 설정합니다.

테이블은 외래키하나로 양방향조회가 가능합니다.
엔티티는 테이블과 달리 둘중 누가 외래키를 관리 할지 정해야 합니다.

  • 연관관계의 주인은 외래키가 있는 곳으로 설정
  • 연관관계의 주인이 외래키를 관리(등록,수정,삭제_
  • 주인이 아닌 쪽은 연관 관계 매핑 시 mappedBy 속성의 값으로 연관 관계의 주인을 설정
  • 주인이 아닌 쪽은 읽기만 가능

무조건적인 양방향매핑보다는 단방향매핑으로 설계 후 나중에 필요한 경우 양방향 매핑을 추가하는 것을 권합니다.

지연로딩


일대일, 다대일로 매핑할 경우 기본 전략인 즉시로딩을 통해 엔티티를 함께 가지고 오며 작성하고 있는 비즈니스 로직에서 사용하지 않을 데이터까지도 한꺼번에 가져오기 때문에 실무에서 사용하기 힘듭니다 , 즉시 로딩을 사용하는 대신에 지연로딩 방식을 사용해야 합니다.
FetchType.LAZY 방식으로 설정해주세요.


import lombok.Setter;

import javax.persistence.*;
import java.time.LocalDateTime;

@Entity
@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;

    private  LocalDateTime regTime;

    private  LocalDateTime updateTime;

}
    @Test
    @DisplayName("지연 로딩 테스트")
    public void lazyLoadingTest(){
        Order order = this.createOrder();
        Long orderItemId = order.getOrderItems().get(0).getId();

        em.flush();
        em.clear();

        OrderItem orderItem = orderItemRepository.findById(orderItemId)
                .orElseThrow(EntityNotFoundException::new);
        System.out.println("Order class:" + orderItem.getOrder().getClass());
        System.out.println("========================");
        orderItem.getOrder().getOrderDate();
        System.out.println("========================");
    }

Auditing을 이용한 엔티티 공통 속성 공통화

AuditorAwareImpl


package com.shop.shop.config;


import org.springframework.data.domain.AuditorAware;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

import java.util.Optional;

public class AuditorAwareImpl implements AuditorAware<String> {

    @Override
    public Optional<String> getCurrentAuditor() {
        Authentication authentication =
                SecurityContextHolder.getContext().getAuthentication();

        String userId = "";
        if (authentication != null) {
            userId = authentication.getName();
        }

        return  Optional.of(userId);
    }

}

AuditConfig


package com.shop.shop.config;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@Configuration
@EnableJpaAuditing
public class AuditConfig {


    @Bean
    public AuditorAware auditorProvider(){
        return  new AuditorAwareImpl();
    }
}

BaseTimeEntity

package com.shop.shop.entity;


import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@EntityListeners(value = {AuditingEntityListener.class})
@MappedSuperclass
@Getter
@Setter
public  abstract class BaseTimeEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime regTime;

    @LastModifiedDate
    private  LocalDateTime updateTime;



}

BaseEntity

package com.shop.shop.entity;


import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@EntityListeners(value = {AuditingEntityListener.class})
@MappedSuperclass
@Getter

public  abstract class BaseEntity extends BaseTimeEntity {

    @CreatedBy
    @Column(updatable = false)
    private  String createBy;

    @LastModifiedBy
    private  String modifiedBy;



}

Member 엔티티 BaseEntity 클래스 상속받기

Member

package com.shop.shop.entity;

import com.shop.shop.constant.Role;
import com.shop.shop.dto.MemberFormDto;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.persistence.*;

@Entity
@Table(name = "member")
@Getter
@Setter
@ToString

public class Member extends  BaseEntity{

    @Id
    @Column(name ="member_id")
    @GeneratedValue(strategy = GenerationType.AUTO)
    private  Long id;

    private String name;

    @Column(unique = true)
    private String email;

    private  String password;

    private  String address;

    @Enumerated(EnumType.STRING)
    private  Role role;

    public  static  Member createMember(MemberFormDto memberFormDto, PasswordEncoder passwordEncoder){

        Member member = new Member();
        member.setName(memberFormDto.getName());
        member.setEmail(memberFormDto.getEmail());
        member.setAddress(memberFormDto.getAddress());
        String password = passwordEncoder.encode(memberFormDto.getPassword());
        member.setPassword(password);
        member.setRole(Role.ADMIN);

        return  member;
    }


}

MemberTest

package com.shop.shop.entity;

import com.shop.shop.repository.MemberRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.TestPropertySource;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import javax.persistence.EntityNotFoundException;
import javax.persistence.PersistenceContext;

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


@SpringBootTest
@Transactional
@TestPropertySource(locations= "classpath:application-test.properties")
class MemberTest {

    @Autowired
    MemberRepository memberRepository;

    @PersistenceContext
    EntityManager em;

    @Test
    @DisplayName("Auditing 테스트")
    @WithMockUser(username = "gilgong", roles = "USER")
    public void auditingTest(){
        Member newMember = new Member();
        memberRepository.save(newMember);

        em.flush();
        em.clear();

        Member member = memberRepository.findById(newMember.getId())
                .orElseThrow(EntityNotFoundException::new);

        System.out.println("register time:" + member.getRegTime());
        System.out.println("update time:" + member.getUpdateTime());
        System.out.println("create member:" + member.getCreateBy());
        System.out.println("modify member:" + member.getModifiedBy());
    }

}
profile
개발자로 차근차근

0개의 댓글