@3/22/2023
A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance: 오류 발생
•
상황
◦
글 수정 시 첨부파일을 업데이트 하는 로직에서 발생
@Transactional
public void updatePost(Long postId, PostUpdateDTO postUpdateDTO, User user) throws IOException {
Post post = findPostById(postId);
if (post.getUser().getId() == user.getId()) {
List<MultipartFile> multipartFiles = postUpdateDTO.getUploadFiles();
List<UploadFile> uploadFiles = fileManager.storeFiles(multipartFiles);
post.setUploadFiles(uploadFiles);
post.setTitle(postUpdateDTO.getTitle());
post.setContent(postUpdateDTO.getContent());
} else {
throw new IllegalStateException("게시글을 수정할 권한이 없습니다.");
}
}
Java
복사
◦
예상키로는 post의 setUploadFiles를 통해 새로운 컬렉션으로 통으로 갈아 끼운 것이 원인듯함. 따라서 기존 컬렉션을 통해 cascade와 orphan removal을 하려던 차에 주인으로부터 벗어나버리니까 오류가 발생하는 것 같음
◦
아래처럼 바꾸니 해결됨
@Transactional
public void updatePost(Long postId, PostUpdateDTO postUpdateDTO, User user) throws IOException {
Post post = findPostById(postId);
if (post.getUser().getId() == user.getId()) {
List<MultipartFile> multipartFiles = postUpdateDTO.getUploadFiles();
List<UploadFile> uploadFiles = post.getUploadFiles();
for (UploadFile newUploadFile : fileManager.storeFiles(multipartFiles)) {
uploadFiles.add(newUploadFile);
}
post.setUploadFiles(uploadFiles);
post.setTitle(postUpdateDTO.getTitle());
post.setContent(postUpdateDTO.getContent());
} else {
throw new IllegalStateException("게시글을 수정할 권한이 없습니다.");
}
}
Java
복사
연관관계 매핑이 안 되는 문제
•
위를 해결하니 또 문제가 발생함
•
영속성 전이만을 통해 파일을 등록했더니 uploadFile에는 post 참조가 걸리지 않아 FK가 빈 채로 들어가는 문제가 발생함
•
UploadFile에 만들어뒀던 addToPost를 통해 양방향으로 참조를 걸어줌
public void addToPost(Post post) {
this.post = post;
post.getUploadFiles().add(this);
}
Java
복사
◦
해결됨
@Transactional
public void updatePost(Long postId, PostUpdateDTO postUpdateDTO, User user) throws IOException {
Post post = findPostById(postId);
if (post.getUser().getId() == user.getId()) {
List<MultipartFile> multipartFiles = postUpdateDTO.getUploadFiles();
if (multipartFiles != null) {
List<UploadFile> uploadFiles = post.getUploadFiles();
for (UploadFile newUploadFile : fileManager.storeFiles(multipartFiles)) {
newUploadFile.addToPost(post);
}
post.setUploadFiles(uploadFiles);
}
post.setTitle(postUpdateDTO.getTitle());
post.setContent(postUpdateDTO.getContent());
} else {
throw new IllegalStateException("게시글을 수정할 권한이 없습니다.");
}
}
Java
복사
글 수정 시 기존에 첨부한 파일 표시
•
뷰
<ul class="list-group me-auto col-sm-5 col-md-4 mb-1">
<li th:each="uploadFile : ${uploadFiles}" class="list-group-item d-flex justify-content-between">
<i class="bi bi-file-earmark me-2"></i>
<span class="me-auto" th:text="${uploadFile.originalFileName}"></span>
<button type="button" class="btn-close" th:id="${uploadFile.id}" onclick="uploadFileDeleteAjax(this)"></button>
</li>
</ul>
HTML
복사
•
컨트롤러
@GetMapping("/attachment/{uploadFileId}/delete")
public ResponseEntity deleteAttachment(
@PathVariable Long uploadFileId,
@SessionAttribute User loginUser
) throws IOException {
UploadFile uploadFile = uploadFileRepository.findOne(uploadFileId).orElseThrow(() -> {
throw new IllegalStateException("권한이 없습니다");
});
Long userId = uploadFile.getPost().getUser().getId();
if (userId == loginUser.getId()) {
uploadFileRepository.delete(uploadFileId);
fileManager.deleteFile(uploadFile);
return ResponseEntity.ok().build();
} else {
return ResponseEntity.badRequest().build();
}
}
Java
복사
•
JS
function uploadFileDeleteAjax(button) {
let uploadFileId = button.id;
let li = button.closest("li");
let request = new XMLHttpRequest();
request.onload = () => {
li.remove();
};
request.open("GET", "/files/attachment/" + uploadFileId + "/delete");
request.send();
}
JavaScript
복사
결과 화면
→ 이렇게 하니 문제점이 파일을 삭제하고 취소를 눌러도 파일 삭제된 것은 복구가 안 됨
hidden input으로 삭제 처리
•
PostController
@PostMapping("/{postId}/edit")
public String updatePost(
@PathVariable Long postId,
@Validated @ModelAttribute PostUpdateDTO postUpdateDTO,
BindingResult bindingResult,
@SessionAttribute User loginUser
) throws IOException {
if (bindingResult.hasErrors()) {
return "posts/postUpdateForm";
}
postService.updatePost(postId, postUpdateDTO, loginUser);
return "redirect:/posts/" + postId;
}
Java
복사
•
PostService
@Transactional
public void updatePost(Long postId, PostUpdateDTO postUpdateDTO, User user) throws IOException {
Post post = findPostById(postId);
if (post.getUser().getId() != user.getId()) {
throw new IllegalStateException("게시글을 수정할 권한이 없습니다.");
}
// 첨부파일 삭제
if (postUpdateDTO.getDeleteFileIds() != null) {
for (Long deleteFileId : postUpdateDTO.getDeleteFileIds()) {
UploadFile uploadFile = uploadFileRepository.findOne(deleteFileId).orElseThrow(() -> {
throw new IllegalStateException("존재하지 않는 파일입니다.");
});
Long userId = uploadFile.getPost().getUser().getId();
if (userId == user.getId()) {
uploadFileRepository.delete(deleteFileId);
fileManager.deleteFile(uploadFile);
}
}
}
// 첨부파일 추가
List<MultipartFile> multipartFiles = postUpdateDTO.getUploadFiles();
if (multipartFiles != null) {
List<UploadFile> uploadFiles = post.getUploadFiles();
for (UploadFile newUploadFile : fileManager.storeFiles(multipartFiles)) {
newUploadFile.addToPost(post);
}
post.setUploadFiles(uploadFiles);
}
post.setTitle(postUpdateDTO.getTitle());
post.setContent(postUpdateDTO.getContent());
}
Java
복사
•
postUpdateDTO에 deleteFileIds 필드 추가
@Getter @Setter
public class PostUpdateDTO {
private Long postId;
@NotBlank
private String title;
@NotBlank
private String content;
private List<MultipartFile> uploadFiles;
private List<Long> deleteFileIds;
}
Java
복사
•
HTML
<ul class="list-group me-auto col-sm-5 col-md-4 mb-1">
<li th:each="uploadFile : ${uploadFiles}" class="list-group-item d-flex justify-content-between">
<i class="bi bi-file-earmark me-2"></i>
<span class="me-auto overflow-hidden" th:text="${uploadFile.originalFileName}"></span>
<button type="button" class="btn-close" th:id="${uploadFile.id}" onclick="uploadFileDelete(this)"></button>
</li>
</ul>
HTML
복사
•
JS
function uploadFileDelete(button) {
let uploadFileId = button.id;
let form = button.closest("form");
let li = button.closest("li");
let input = document.createElement("input");
input.setAttribute("type", "hidden");
input.setAttribute("deleteFileIds", uploadFileId);
form.appendChild(input);
li.remove();
}
JavaScript
복사
•
핵심은 JS를 통해 hidden input으로 삭제할 파일들의 item을 모으는 것