하루살이 개발자

[Instagram 클론코딩] 4. 구독 본문

Project/Instagram 클론코딩

[Instagram 클론코딩] 4. 구독

하루살이 2022. 2. 17. 18:58

연관관계 개념

 

1.  N : 1

- N인 테이블이 FK이다.(공식1)

- User기준 1 : N, 게시글 기준 1 : 1

N : 1

 

2. N : N

- 중간 테이블을 만들어야 한다.(공식2)

- User와 영화의 관계에서 "예매"라는 중간 테이블을 만들어야 한다.

- User기준 1 : N , Movie기준 1 : N

N : N
구독 기능 연관관계(N : N)

1. 구독하기 모델 만들기

 

Subscribe 

package com.cos.photogramstart.domain.subscribe;

import java.time.LocalDateTime;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.PrePersist;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;

import com.cos.photogramstart.domain.user.User;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
@Table( // 유니크하게 만들기(1번이 2번 구독, 2번이 1번 구독 하는 중복상황을 막기 위해)
		uniqueConstraints = {
				@UniqueConstraint(
						name="subscribe_uk",
						columnNames = {"fromUserId", "toUserId"} // 2개를 유니크하게 만들기
				)
		}
)
public class Subscribe {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private int id;
	
	@JoinColumn(name = "fromUserId") // 스키마 변경: DB에서 언더바 방식(fromUser_id) 맘에 안들어서 바꿈 (이렇게 컬럼명 만들어! 니 맘대로 만들지 말고!!)
	@ManyToOne // 자동으로 테이블 생성 N : 1 (ORM 방식: entity를 받아 바로 테이블로 만들어줌)
	private User fromUser; // 구독 하는 user
	
	@JoinColumn(name = "toUserId")
	@ManyToOne
	private User toUser; // 구독 받는 user
	
	private LocalDateTime createDate;
	
	@PrePersist
	public void createDate() {
		this.createDate = LocalDateTime.now();
	}

}

 

SubscribeRepository

 

package com.cos.photogramstart.domain.subscribe;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;

public interface SubscribeRepository extends JpaRepository<Subscribe, Integer>{

}

 

DB 확인 

SELECT * FROM subscribe;

INSERT INTO subscribe(fromUserId, toUserId) VALUES(2, 1);

DROP TABLE subscribe;

 

 

2. 구독, 구독취소 API 만들기

 

SubscribeService

package com.cos.photogramstart.service;

import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.Query;

import org.qlrm.mapper.JpaResultMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.cos.photogramstart.domain.subscribe.SubscribeRepository;
import com.cos.photogramstart.handler.ex.CustomApiException;
import com.cos.photogramstart.web.dto.subscribe.SubscribeDto;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class SubscribeService {

	private final SubscribeRepository subscribeRepository;
	private final EntityManager em; // Repository는 EntityManager를 구현해서 만들어져 있는 구현체


	@Transactional // DB에 영향을 주기 때문에
	public void 구독하기(int fromUserId, int toUserId) {
		try {
			subscribeRepository.mSubscribe(fromUserId, toUserId);
		} catch (Exception e) {
			throw new CustomApiException("이미 구독을 하였습니다."); // 핸들러에서 처리
		}
		// save를 이용하려면 객체가 int로 정의되어있어야 하는데,
		// subscribe는 User로(오브젝트로) 정의 되어있으므로 이렇게 하지 말고 직접 네이티브 커리를 짜자(SubscribeRepository에 짜기!)
		//subscribeRepository.save(null);
	}
	
	@Transactional
	public void 구독취소하기(int fromUserId, int toUserId) {
		subscribeRepository.mUnSubscribe(fromUserId, toUserId);
	}
}

 

SubscribeRepository

package com.cos.photogramstart.domain.subscribe;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;

public interface SubscribeRepository extends JpaRepository<Subscribe, Integer>{
	// 구독 쿼리 직접 만들기!
	// Scribe에서 생성했던 date는 작접 작성하는 쿼리에서는 작동 안 함 -> createDate 필요
	@Modifying // INSERT, DELETE, UPDATE 를 네이티브 쿼리로 작성하려면 해당 어노테이션 필요!!
	@Query(value = "INSERT INTO subscribe(fromUserId, toUserId, createDate) VALUES(:fromUserId, :toUserId, now())", nativeQuery = true)
	void mSubscribe(int fromUserId, int toUserId); // 성공하면 1리턴(변경된 행의 개수가 리턴됨, 0이면? 변경된 행이 없다는 것), 실패하면 -1 리턴
	// 내가 만들었다는 의미에서 m붙임(mSubscribe)

	// 구독 취소 쿼리 직접 만들기!
	@Modifying
	@Query(value = "DELETE FROM subscribe WHERE fromUserId = :fromUserId AND toUserId = :toUserId", nativeQuery = true)
	void mUnSubscribe(int fromUserId, int toUserId);

}

 

SubscribeApiController

package com.cos.photogramstart.web.api;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import com.cos.photogramstart.config.auth.PrincipalDetails;
import com.cos.photogramstart.service.SubscribeService;
import com.cos.photogramstart.web.dto.CMRespDto;

import lombok.RequiredArgsConstructor;

// 파일 등이 아닌 데이터만 리턴하는 컨트롤러를 apiController라고 함
@RequiredArgsConstructor
@RestController
public class SubscribeApiController {
	
	private final SubscribeService subscribeService;

	// 현재 로그인 한 사람이 구독함 함
	@PostMapping("/api/subscribe/{toUserId}")
	public ResponseEntity<?> subscribe(@AuthenticationPrincipal PrincipalDetails principalDetails, @PathVariable int toUserId){
		subscribeService.구독하기(principalDetails.getUser().getId(), toUserId);
		return new ResponseEntity<>(new CMRespDto<>(1, "구독하기 성공", null), HttpStatus.OK);
	}

	// 현재 로그인 한 사람이 구독취소 함
	@DeleteMapping("/api/subscribe/{toUserId}")
	public ResponseEntity<?> unSubscribe(@AuthenticationPrincipal PrincipalDetails principalDetails, @PathVariable int toUserId){
		subscribeService.구독취소하기(principalDetails.getUser().getId(), toUserId);
		return new ResponseEntity<>(new CMRespDto<>(1, "구독취소하기 성공", null), HttpStatus.OK);
	}
}