블로그 만들기 3번째 시간이다. 이번시간에는 post에 대한 카테고리 와 댓글을 만들어보자.
post에는 여러개의 category를 넣을 수도 있겠지만 여기서는 한개의 category만 선택할 수 있고 댓글 경우에는 여러개의 입력 할 수 있게 만들어 보았다.

3.blog1

물론 컬럼들은 바뀔 수 있으나 큰 흐름은 대충 위와 같다. 만약 카테고리도 여러개를 넣고 싶다면 매핑테이블을 만들어서 하면 될 듯하다.

post와 category 와의 관계는 다대일 관계이다. 반대로도 생각할 수 도 있는데 category 와 post의 관계는 일대다 다.
보통의 db의 경우에는 양방향으로 검색이 가능하다. 예를들어 category기준으로 post를 검색할 수도 있고 post기준으로 category를 검색 할 수 도 있다. 하지만 객체의 경우에는 양방향이라는게 없다. 왜냐하면 객체는 항상 단방향이기 때문이다. 단방향이지만 양방향으로 만들 수 있는데 참조하는 쪽에 연관관계를 한개 더 만들면 가능하다. 하지만 이것은 양방향이라기보다 서로 다른 단방향 관계 2개다. 그래서 양방향이 없다고 이야기 한것이다. 하지만 여기에서는 서로다른 단반향 관계를 양뱡향이라고 하겠다.
category 엔티티를 보자.

@Entity
public class Category {

  @Id
  @GeneratedValue
  private Long id;

  private String name;

  private LocalDateTime regDate;

  @OneToMany(mappedBy = "category", fetch = FetchType.LAZY)
  private List<Post> post = new ArrayList<>();
  //기타 생성자 getter setter
}

@OneToMany 어노테이션으로 우리는 일대다 관계를 만들 수 있다. 속성 중 mappedBy는 설명할게 좀 있으니 그냥 연관관계의 주인이 아니라고만 알고 있자. fetch의 경우에는 즉시로딩 과 지연로딩을 설정할 수 있다. 즉시로딩일 경우에는 Category를 가져올때 무조건 post도 가져온다는 의미이고 지연로딩일 경우에는 Category를 가져올때 post는 프록시 객체로 가져오고 만약 post를 사용한다면 그때 쿼리해서 가져온다. 말은 쉽지 실제 모르고 개발하면 쉽지 않다. 영속성과 관련도 많이 있어서.. 특별한 경우가 아니라면 지연로딩을 추천하고 있다.

@Entity
public class Post {
  //기타 프로퍼티

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "CATEGORY_ID")
  private Category category;

  //기타 생략
}

우리는 양뱡향으로 설정하기 위해서 위와 같이 post에도 category를 넣었다. @ManyToOne은 다대일이라는 표현이다. 속성은 많지만 다 설명하면 jpa시간이 되버리니 생략한다. @JoinColumn은 외래키를 매핑할 때 사용한다. name이라는 속성에 매핑할 외래 키 이름을 지정해주면 된다. 하지만 이 이노테이션은 생략 가능하다.

@Entity
public class Comment {
    @Id @GeneratedValue
    private Long id;

    private String content;

    private LocalDateTime regDate;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "POST_ID")
    private Post post;
    //기타 생성자 getter setter
}

Comment 경우에는 post 한개당 여러개를 달 수 있으므로 다대일 관계가 된다. 그리고 post 입장에서는 일대다가 된다. 아래와 같이 post에도 추가 하자.

public class Post {
  //...
  @OneToMany(mappedBy = "post", fetch = FetchType.LAZY)
  private List<Comment> comments;

  //...
}

그럼 얼추 도메인 모델은 끝이 났다.
category의 경우는 CRUD 모두 있어야 하지만 comment 경우에는 단건 조회와 변경은 굳이 만들지 필요 없을 듯 하다.

@Controller
@RequiredArgsConstructor
@RequestMapping("/categories")
public class CategoryController {

  private final CategoryService categoryService;

  @GetMapping
  public String categories(Pageable pageable, Model model) {
    model.addAttribute("categories", categoryService.findAll(pageable));
    return "category/list";
  }

  @GetMapping("/new")
  public String newCategory(@ModelAttribute CategoryDto categoryDto) {
    return "category/new";
  }

  @GetMapping("/{id}/edit")
  public String edit(@PathVariable Long id, Model model) {
    model.addAttribute("categoryDto", categoryService.findOne(id));
    return "category/edit";
  }

  @PostMapping
  public String createCategory(@ModelAttribute @Valid CategoryDto categoryDto, BindingResult bindingResult) {
    if(bindingResult.hasErrors()){
      return "category/new";
    }
    categoryService.createCategory(new Category(categoryDto.getId(), categoryDto.getName()));
    return "redirect:/categories";
  }

  @PostMapping("/{id}/edit")
  public String modifyCategory(@PathVariable Long id, @ModelAttribute @Valid CategoryDto categoryDto, BindingResult bindingResult) {
    if(bindingResult.hasErrors()){
      return "category/edit";
    }
    categoryService.updateCategory(new Category(id, categoryDto.getName()));
    return "redirect:/categories";
  }

  @PostMapping("/{id}/delete")
  public String deleteCategory(@PathVariable Long id) {
    categoryService.delete(id);
    return "redirect:/categories";
  }
}
@Service
@Transactional
@RequiredArgsConstructor
public class CategoryService {

  private final CategoryRepository categoryRepository;

  public Category createCategory(Category category){
    category.setRegDate(LocalDateTime.now());
    return categoryRepository.save(category);
  }

  public void delete(Long id) {
    categoryRepository.delete(id);
  }

  public void updateCategory(Category category) {
    Category oldCategory = categoryRepository.findOne(category.getId());
    if(oldCategory != null){
      oldCategory.setName(category.getName());
    }
  }

  @Transactional(readOnly = true)
  public Page<Category> findAll(Pageable pageable) {
    return categoryRepository.findAll(pageable);
  }

  @Transactional(readOnly = true)
  public List<Category> findAll() {
    return categoryRepository.findAll();
  }

  public Category findOne(Long id) {
    return categoryRepository.findOne(id);
  }
}
public interface CategoryRepository extends JpaRepository<Category, Long> {
}

기본적인 읽기와 쓰기이다. 굳이 설명은 이 전시간에 했기 때문에 생략하고 소스만 보자.

다음은 Comment의 기본적인 컨트롤러 서비스 레파지토리다.


@Controller @RequestMapping("/comments") @RequiredArgsConstructor public class CommentController { private final CommentService commentService; @PostMapping public String createComment(@ModelAttribute @Valid CommentDto commentDto, BindingResult bindingResult, Model model){ if(bindingResult.hasErrors()){ return "post/post"; } model.addAttribute("comment",commentService.createComment( new Comment(commentDto.getContent(), new Post(commentDto.getPostId())))); return "redirect:/posts/"+commentDto.getPostId(); } @PostMapping("/{postId}/{commentId}") public String deleteComment(@PathVariable Long postId, @PathVariable Long commentId){ commentService.deleteComment(commentId); return "redirect:/posts/"+postId; } }

여기서 중요한것은 comment를 생성할 때 해당하는 연관관계의 post를 넣어 줘야 한다. comment에는 post와의 연관관계가 설정 되어 있다. 그래야 comment의 외래키에 postId가 들어간다. 원래는 postId가 있는지 검증도 해야하고 영속 객체를 넣는게 더 좋은 방법인듯 하다. 하지만 여기서는 무조건 있는 값이 넘어 온다고 가정했다. 그런 로직은 service에 있는게 맞을 듯 하다.
그러면 post의 카테고리도 마찬가지로 동일하게 넣어줘야 한다.

@PostMapping
public String createPost(@ModelAttribute @Valid PostDto createPost, BindingResult bindingResult, Model model) {
  if(bindingResult.hasErrors()){
    return "post/new";
  }
  Post post = new Post(createPost.getTitle(),
    createPost.getContent(),
    createPost.getCode(),
    PostStatus.Y,
    new Category(createPost.getCategoryId()));
  Post newPost = postService.createPost(post);
  model.addAttribute("post", newPost);
  return "redirect:/posts/" +  newPost.getId();
}

@PostMapping("/{id}/edit")
public String modifyPost(@PathVariable Long id, @ModelAttribute("editPost") @Valid PostDto createPost, BindingResult bindingResult) {
  if(bindingResult.hasErrors()){
    return "post/edit";
  }
  postService.updatePost(id, new Post(
    createPost.getTitle(),
    createPost.getContent(),
    createPost.getCode(),
    PostStatus.Y,
    new Category(createPost.getCategoryId())
  ));
  return "redirect:/posts/" +  id;
}

post도 comment와 마찬가지로 생성시에 연관관계의 category를 넣어 줬다. 연관관계의 주인만이 읽기, 쓰기가 모두 가능하다. 주인이 아닌 곳에서는 읽기만 가능하다.
아까 위에서 mappedBy를 잠깐 언급했는데 mappedBy가 있는 곳이 연관관계의 주인이 아니다.

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CATEGORY_ID")
private Category category;

위와 같이 Post에 category는 mappedBy가 없으므로 post가 연관관계의 주인이 된다. 물론 연관관계의 주인을 바꿀 수는 있으나 성능과 관리 측면에서 권장하지는 않는다.

계속해서 comment의 service 와 repository 클래스를 보자.

@Service
@Transactional
@RequiredArgsConstructor
public class CommentService {

    private final CommentRepository commentRepository;

    public Comment createComment(Comment comment){
        comment.setRegDate(LocalDateTime.now());
        return commentRepository.save(comment);
    }

    public void deleteComment(Long commentId) {
        commentRepository.delete(commentId);
    }
}

public interface CommentRepository extends JpaRepository<Comment, Long> {
}

여기서 읽기가 없는 이유는 나중에 post에서 바로 불러온다. 그래서 굳이 여기서는 필요 없어서 지웠다.
일단 기본적인 crud는 완성 되었다. 기본적인 것만 추가를 했으니 나중에 더 추가할 내용이 조금 있다. 아직 다 완성 된게 아니다보니 하다가 수정사항이 더 있을 수 있다.

오늘 중요한거는 객체의 일대다 다대일 관계였다.

다음 시간에는 아마도 view templates 인 thymeleaf를 붙어서 뷰를 만들어 보자. 뷰가 나와야 뭔가 조금은 한듯 싶으니..

현재까지의 소스는 여기에 올라가 있다.

  1. [spring-boot] 블로그를 만들자 (1)

  2. [spring-boot] 블로그를 만들자. (2) JPA

  3. [spring-boot] 블로그를 만들자. (3) Category 와 Comment

  4. [spring-boot] 블로그를 만들자. (4) thymeleaf

  5. [spring-boot] 블로그를 만들자. (5) markdown, catetory

  6. [spring-boot] 블로그를 만들자. (6) 댓글과 Navigation

  7. [spring-boot] 블로그를 만들자. (7) 캐시와 에러페이지

  8. [spring-boot] 블로그를 만들자. (8) GitHub login

  9. [spring-boot] 블로그를 만들자. (9) CI 와 배포