익명 블로그에서 연관 관계가 있는 엔티티 삭제 활용
⇒외래키 제약 조건 때문에 게시글 삭제 불가능한 상태에서 데이터 삭제 방식의 종류 활용
[Reply 클래스 - 엔티티]
- Board 클래스와 Reply 클래스는 N : 1의 관계
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Table(name = "reply_tb")
@Entity
public class Reply {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String comment;
@CreationTimestamp
private Timestamp createdAt;
@ManyToOne(fetch = FetchType.LAZY) // 유저1 : 댓글n
private User user; // 댓글을 쓴 유저
@ManyToOne(fetch = FetchType.LAZY) // 보드1 : 댓글n
private Board board; // 댓글이 달린 게시물
@Builder
public Reply(Integer id, String comment, Timestamp createdAt, User user, Board board) {
this.id = id;
this.comment = comment;
this.createdAt = createdAt;
this.user = user;
this.board = board;
}
}[Board 클래스 - 엔티티]
- Board 클래스와 Reply 클래스는 N : 1의 관계
@NoArgsConstructor(access = AccessLevel.PROTECTED) // 외부에서 빈 생성자 생성하지 못하게 제한(protected)
@Getter
@Table(name = "board_tb") // 테이블 명
@Entity // Board 객체를 테이블로 생성하며, 데이터베이스 테이블과 매핑하여 데이터베이스 작업(insert, update, delete 등)을 수행
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // AutoIncrement
private Integer id;
private String title;
private String content;
// 연관 관계 설정 (User 쪽이 1이니까 ManyToOne)
@ManyToOne(fetch = FetchType.LAZY) // LAZY와 EAGER일때 콘솔 출력되는 쿼리 다름
private User user;
// mappedBy : fk의 변수명이 무엇인가?
@OneToMany(mappedBy = "board", fetch = FetchType.EAGER)
private List<Reply> replies = new ArrayList<>(); // 댓글들을 List로 만듦
@CreationTimestamp
private Timestamp createdAt;
// 풀 생성자 만들고 @Builder 달아서 빌더의 형태 잡음
@Builder
public Board(Integer id, String title, String content, User user, Timestamp createdAt) {
this.id = id;
this.title = title;
this.content = content;
this.user = user;
this.createdAt = createdAt;
}
// update(글 수정)를 setter로 만들어서 활용하기로 함
// setter는 명확한 의도가 있는것만 해놓는게 좋기 때문에
public void update(String title, String content) {
this.title = title;
this.content = content;
}
}[BoardResponse 클래스 - ReplyDTO]
- 세션에 저장된 User 정보와 댓글(Reply)을 작성한 User 정보 비교
- Integer 비교는 ‘==’ 이 아니라 .equals
- int와 달리 Integer은 객체의 참조 비교하기 때문
- 값이 아닌 메모리 주소를 비교하므로, 동일한 값을 가진 다른 객체는 false반환 할 수 있음
// [ReplyDTO 로직의 일부]
if (sessionUser != null) {
// lazy loading
this.isOwner = sessionUser.getId().equals(reply.getUser().getId());
}public class BoardResponse {
// 글 상세보기
@Data
public static class DetailDTO {
private int id;
private String title;
private String content;
private String createdAt;
private Integer userId; // 게시글을 작성한 userId
private String username;
private boolean isOwner = false;
private List<ReplyDTO> replies;
// 내부 DTO(Class)는 public 붙이지 않기 - 외부에서 모르고 꺼내쓸 수 있기때문
@Data
class ReplyDTO {
private int id;
private String comment;
private int userId; // 댓글 작성한 userId
private String username; // 댓글 작성한 username(아이디)
private boolean isOwner = false; // 이너클래스라 변수 이름이 같아도 됨
// 생성자
public ReplyDTO(Reply reply, User sessionUser) {
this.id = reply.getId();
this.comment = reply.getComment();
this.userId = reply.getUser().getId();
this.username = reply.getUser().getUsername();
if (sessionUser != null) {
this.isOwner = sessionUser.getId().equals(reply.getUser().getId());
}
}
}
// 생성자
public DetailDTO(Board board, User sessionUser) { // sessionUser는 권한 확인을 위해
this.id = board.getId();
this.title = board.getTitle();
this.content = board.getContent();
this.createdAt = Encoding.formatToStr(board);
this.userId = board.getUser().getId();
this.username = board.getUser().getUsername();
if (sessionUser != null) {
// lazy loading
this.isOwner = sessionUser.getUsername().equals(board.getUser().getUsername());
}
this.replies = board.getReplies().stream().map(r -> new ReplyDTO(r, sessionUser)).toList();
}
}
}[BoardRepository 클래스]
@RequiredArgsConstructor
@Repository
public class BoardRepository {
private final EntityManager em;
public void delete(int id) {
em.createQuery("delete from Board b where id = :id")
.setParameter("id", id)
.executeUpdate();
}
}[BoardService 클래스]
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
public class BoardService {
private final BoardRepository boardRepository;
@Transactional
public void 게시글삭제(int id, User sessionUser) {
Board boardPS = boardRepository.findById(id)
.orElseThrow(() -> new Exception404("게시글 아이디를 찾을 수 없습니다."));
if (!sessionUser.getId().equals(boardPS.getUser().getId())) {
throw new Exception403("권한이 없습니다.");
}
boardRepository.delete(id);
}
}[BoardController 클래스]
@RequiredArgsConstructor
@Controller
public class BoardController {
private final BoardService boardService;
private final HttpSession session;
// 메인화면 이동
@GetMapping("/")
public String list(Model model) {
List<BoardResponse.DTO> boardList = boardService.게시글목록보기();
model.addAttribute("models", boardList);
return "index";
}
// 글 삭제
@PostMapping("/board/{id}/delete")
public String delete(@PathVariable("id") int id) {
User sessionUser = (User) session.getAttribute("sessionUser");
boardService.게시글삭제(id, sessionUser);
return "redirect:/";
}
}[데이터 삭제 방법]
외래 키(FK)에 걸기
- @OnDelete(action = OnDeleteAction.CASCADE)
- FK에 @OnDelete 설정
- 부모 엔티티(Board) 삭제 시 관련된 자식 엔티티(Reply)도 자동으로 삭제되도록 설정 ⇒ cascade 설정
@OnDelete(action = OnDeleteAction.CASCADE)
@ManyToOne(fetch = FetchType.LAZY)
private Board board;게시글 삭제 시 댓글을 null로 업데이트
- 부모 엔티티 삭제 시 자식 엔티티의 외래 키(FK)를 null로 설정하여 관계 해제
// 게시글 삭제 로직에서 댓글의 board를 null로 업데이트
public void deleteBoard(Board board) {
List<Reply> replies = replyRepository.findByBoard(board);
for (Reply reply : replies) {
reply.setBoard(null);
replyRepository.save(reply);
}
boardRepository.delete(board);
}게시글 삭제 시 댓글을 직접 삭제 (쿼리 튜닝)
- 부모 엔티티 삭제 시 자식 엔티티를 직접 삭제하는 쿼리 실행
// 게시글 삭제 로직에서 댓글을 먼저 삭제
public void deleteBoard(Board board) {
replyRepository.deleteByBoard(board);
boardRepository.delete(board);
}제약 조건 해제 후 삭제
- 외래 키 제약 조건을 해제하여 관계를 자유롭게 관리할 수 있도록 함
@JoinColumn(foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
@ManyToOne(fetch = FetchType.LAZY)
private Board board;논리적 삭제
- 실무에서 주로 사용하는 방법 (가장 중요!!)
- 데이터베이스에서 실제로 데이터를 삭제하지 않고, 특정 칼럼을 사용하여 삭제된 것처럼 처리
- Board에 visible 컬럼 추가하고 삭제 시 false로 update
@ManyToOne(fetch = FetchType.LAZY)
private Board board;
@Column(nullable = false)
private boolean visible = true;
@Column(nullable = true)
private Timestamp updatedAt;
public void delete() {
this.visible = false;
this.updatedAt = new Timestamp(System.currentTimeMillis());
}Share article