Spring Projcect/[갠플] Online-mall

[5일차] 연관 관계 매핑 - 장바구니, 쿠폰

계란💕 2022. 9. 27. 21:14

1. 고객 - 장바구니 매핑하기 (일대일)

  • Member
<hide/>
@OneToOne
@JoinColumn(name = "CART_ID")   // 카트의 변수명과 달라도되나???
private Cart cart;

 

  • Cart 엔티티 추가
<hide/>
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Cart {

    @Id  @GeneratedValue()
    private Long id;

    private String name;   

    @OneToOne(mappedBy = "cart")
    @JoinColumn
    private Member member;  // mappedBy를 조인컬럼으로 바꾸면? mamberId  컬럼 생긴다.
}

 

  • MemberServiceImpl - 이메일 인증 후
    • 새로운 카트를 만들어서 추가해준다.
<hide/>
Cart cart = new Cart();
member.setCart(cart);
cartRepository.save(cart);
memberRepository.save(member);

 

  Note) 실행 결과

  • 회원 가입하면 다음과 같이 장바구니 번호가 생성된다.

member
cart

 

 

 

2. 고객 -  쿠폰 매핑하기 (일대일)

  • coupon 엔티티
<hide/>
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Coupon {

    @Id  @GeneratedValue
    private Long id;

    @OneToOne(mappedBy = "coupon")
    @JoinColumn
    private Member member;
    
//    @Enumerated(EnumType.STRING)
//    String으로 하면 SQL 오류가 난다.   ==> 해결하기
    private CouponType couponType;  // 쿠폰 종류

    private LocalDateTime couponRegDt;  // 쿠폰 등록일
    private LocalDateTime couponExpirationDt; // 쿠폰 만료일
    
}

 

  • ServiceImpl
<hide/>
@Override
public boolean emailAuth(String uuid) { // 이메일 인증

    Optional<Member> optionalMember = memberRepository.findByEmailAuthKey(uuid);    // 있으면 Optional<Member> 가 리턴된다.
    if(!optionalMember.isPresent()){
        return false;
    }
    Member member = optionalMember.get();

    // 이미 활성화됐기 때문에 또 활성화할 필요없다.
    if(member.isEmailAuthYn()){
        return false;
    }

    // 장바구니 추가
    Cart cart = new Cart();
    LocalDateTime now =  LocalDateTime.now();

    // 쿠폰 추가
    Coupon coupon = Coupon.builder()
        .couponType(CouponType.COUPON_A)
        .couponRegDt(now)
        .couponExpirationDt(now.plusMonths(3))
//            .member(member)  => TransientPropertyValueException 발생
        .build();

    // member에만 set()하고 반대편은 저장 x
    member.setCart(cart);
    member.setCoupon(coupon);
    member.setUserStatus(MemberCode.MEMBER_STATUS_ING);
    member.setEmailAuthYn(true);
    member.setEmailAuthDt(LocalDateTime.now());

//        couponRepository.save(coupon);
//        cartRepository.save(cart);
    memberRepository.save(member);  // 연관관계의 주인만 저장해도 된다.
    return true;
}

 

 

 

3. 회원 가입 후 - 이메일 인증

  • ServiceImpl
    • 이메일 인증 
    • 가입시 생성한 uuid로 회원을 찾는다.
<hide/>
@Override
public boolean emailAuth(String uuid) { // 이메일 인증

    Optional<Member> optionalMember = memberRepository.findByEmailAuthKey(uuid);    // 있으면 Optional<Member> 가 리턴된다.
    if(!optionalMember.isPresent()){
        return false;
    }
    Member member = optionalMember.get();

    // 이미 활성화됐기 때문에 또 활성화할 필요없다.
    if(member.isEmailAuthYn()){
        return false;
    }
    member.setUserStatus(MemberCode.MEMBER_STATUS_ING);
    member.setEmailAuthYn(true);
    member.setEmailAuthDt(LocalDateTime.now());
    memberRepository.save(member);
    return true;
}

 

  • 컨트롤러
    • 회원 가입시 uuid를 생성해서 member의 이메일 인증키에 넣었다.
    • 그러고나서 회원가입 이메일을 보낼 때 링크에 아래와 같은 링크를 넣었다,
    • localhost:8080/member/email-auth?id= {uuid}
    • 따라서, GetMapping이 실행되면 위 URL의 id인 uuid가 컨트롤러로 넘어온다.
    • 인증을 받으면 result에 true를 저장해서 html로 result 값을 보낸다.
<hide/>
@GetMapping("/member/email-auth")
public String emailAuth(Model model, HttpServletRequest request){

    String uuid = request.getParameter("id"); // 주소창에 보이는 변수
    System.out.println(uuid);
    boolean result = memberService.emailAuth(uuid);
    model.addAttribute("result", result);   // 현재 결과 result를 html로 result라는 변수로 넘긴다.
    return "member/email-auth";
}

 

  • html
<hide/>
<!DOCTYPE html>
<html lang="ko"  xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>회원 활성화</title>
</head>
<body>
<h1>회원 활성화</h1>
<!--    <div th:if="${result}">-->

<div th:if="${result}">
  <p>회원님의 계정이 활성화 되었습니다.</p>
</div>

<div th:if="${result eq false}">
  <p>회원님의 계정 활성화에 실패했습니다.</p>
</div>

</body>
</html>

 

  Note) 실행 결과

  • localhost:8080/member/email-auth?id = {}
  • {인증을 위한 uuid}

활성화 성공 페이지

 

활성화를 이미 한 경우의 실패 페이지

 

 

 

 

  오류) TransientPropertyValueException

object references an unsaved transient instance - save the transient instance before flushing : com.example.mall.entity.Coupon.member -> com.example.mall.entity.Member

  • 오류: TransientPropertyValueException
  • 원인: 저장되지 않은 객체를 참고했다.
  • 해결: 연관 관계 매핑한 부분에 대해서 member 쪽에만 setCart(), setCoupon()을 해주고 반대는 설정하지 말야야 한다.
    • 멤버 - 장바구니
    • 멤버 - 쿠폰
<hide/>
org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : com.example.mall.entity.Coupon.member -> com.example.mall.entity.Member
	at org.hibernate.engine.spi.CascadingActions$8.noCascade(CascadingActions.java:379) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
	at org.hibernate.engine.internal.Cascade.cascade(Cascade.java:169) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
	at org.hibernate.event.internal.AbstractFlushingEventListener.cascadeOnFlush(AbstractFlushingEventListener.java:159) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
	at org.hibernate.event.internal.AbstractFlushingEventListener.prepareEntityFlushes(AbstractFlushingEventListener.java:149) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
	at org.hibernate.event.internal.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:82) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
	at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:39) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
	at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:107) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
	at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1407) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
	at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:489) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
	at org.hibernate.internal.SessionImpl.flushBeforeTransactionCompletion(SessionImpl.java:3290) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
	at org.hibernate.internal.SessionImpl.beforeTransactionCompletion(SessionImpl.java:2425) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
	at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.beforeTransactionCompletion(JdbcCoordinatorImpl.java:449) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
	at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.beforeCompletionCallback(JdbcResourceLocalTransactionCoordinatorImpl.java:183) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
	at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.access$300(JdbcResourceLocalTransactionCoordinatorImpl.java:40) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
	at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:281) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
	at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:101) ~[hibernate-core-5.6.10.Final.jar:5.6.10.Final]
	at org.springframework.orm.jpa.JpaTransactionManager.doCommit(JpaTransactionManager.java:562) ~[spring-orm-5.3.22.jar:5.3.22]

  

 

  오류)

  •  couponType을 넣는데 @EnumType을 String으로 정해도 데이터베이스에 쿠폰 타입이 들어가지 않는다.
    • IllegalArgumentException 아니면 SQL 오류가 난다.
    • 이메일 인증 하는 마지막 부분에 연관관계의 주인인 member만 repository에 저장하면 cart, coupon에 자동으로 id가 들어간다.
    • string이 들어가도록 구현해야한다.
  • EnumType.String을 넣었을 때 에러 화면 - 회원 가입 까지는 되는데 이메일의 링크를 누르면 다음과 같이 오류난다.

  • 에러 코드
    java.sql.SQLException: (conn=475) Incorrect integer value: 'COUPON_C' for column 'coupon_type' at row 1
  • 원인: 데이터베이스에 타입이 int로 설정되어 있음
  • 해결: 다음과 같이 varchar() 형태로 바꾼다.