스프링부트 ~22 | 댓글 CRUD를 위한 Entity, Repository & Test
각 Article에는 Comment가 달리기 마련이다.
이 기능을 추가하기 위해서 Comment Entity와 Repository를 준비해보자.
이전에 Article Entity 작성했던 것을 떠올리면서, 어떤 데이터가 필요할지(:작성한대로 DB table이 생성됨)생각하며 Comment Entity를 구현하면 된다.
아래 내용들을 알고 들어가자
∨ DB의 데이터는 id로 관리된다.
- 데이터 자기 자신을 가리키는 id
- 그리고 대상을 가리키는 id
이것이 즉 PK와 FK다.
- PK : primary key
- FK : foreign key
∨ 추가하려는 기능은 댓글 기능이다.
댓글과 게시글의 관계는?
- 게시글 입장에서는 일대다 관계이고 @OneToMany
- 댓글 입장에서는 다대일 관계이다 @ManyToOne
댓글 Entity 설계
- 하나의 댓글은 어떤 하나의 게시글에 달라는 것이다.
- 각 댓글마다 Article의 id가 부여되겠다.
Article | Comment |
id : Long title : String content : String |
id : Long article : Article nickname : String body : String |
@Entity
@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Comment 입장에서 Article과의 관계는 ManyToOne이다
// FK가 article_id column에 들어간다
// JPA는 자동으로 table 조인을 해주기 때문에 Article을 통으로 넣는다.
// 반면 Mybatis나 JDBC는 조인을 해주지 않아서 article_id를 바로 담는 것이 표준이다.
@ManyToOne
@JoinColumn(name = "article_id")
private Article article;
@Column
private String nickname;
@Column
private String body;
}
참고) 더미데이터 추가
Article에 더미데이터를 추가해놓고 실습했던 것처럼 Comment 더미데이터도 같은 방식으로 추가한다.
INSERT INTO comment(id, article_id, nickname, body) VALUES(2, 5, '예니', '악플금지!!');
참고) SQL 연습
4번 게시글의 모든 댓글
SELECT
*
FROM
comment
WHERE
article_id = 4
;
닉네임이 "예니"인 모든 댓글
SELECT
*
FROM
comment
WHERE
nickname = '예니'
;
댓글 Repository 설계
이전에 Entity Repository는 CrudRepository를 사용했었다.
이번에는 JpaRepository를 사용한다.
* CrudRepository를 확장한 PagindAndSortingRepository를 확장한 것이다. 데이터 crud뿐 아니라 특정 페이지의 데이터 조회, 정렬 등 제공한다.
Repositofy <= CrudRepository <= CrudRepository <= PagindAndSortingRepository <= JpaRepository
지난번에 articleRepository를 구현할 때에는 그냥 extends만 해주고 딱히 구현한 부분은 없었다.
지금 commentRepository에는 메서드를 작성해줄 것이다.
recall) 댓글 Entity를 잘 구현했나 확인하면서 sql 쿼리 연습을 했다.
- n번 게시글의 모든 댓글을 조회하는 쿼리
- 닉네임 '~~'의 모든 댓글을 조회하는 쿼리
DB랑 접촉하는 Repository의 역할을 톡톡히 수행하는 '쿼리 메서드'를 작성해줄 것이다.
쿼리문을 메서드로 수행하게 하는 방식을 두가지 새로 배웠다.
- 메서드에 @Query() 어노테이션을 붙이고, 괄호 안에 수행시킬 쿼리문을 넣어주면 된다.
- 해당 메서드를 통해 수행할 쿼리를 xml로 작성해두는 방법도 있다.
public interface CommentRepository extends JpaRepository<Comment, Long> {
// 네이티브 쿼리문을 직접 작성
@Query(value =
"SELECT * " +
"FROM comment " +
"WHERE article_id = :articleId",
nativeQuery = true) // true로 해주어야 해당 sql문이 직접 동작한다
List<Comment> findByArticleId(Long articleId);
// 네이티브 쿼리문을 xml로 작성
List<Comment> findByNickname(String nickname);
}
* @Query() 내부 변수명과 메서드의 파라미터명을 일치시켜야 인식한다.
* 만약 인식하지 못할 경우 메서드 파라미터에 @Param("articleId") Long articleId라고 명시해준다.
메서드로 수행할 sql을 xml로 작성하는 예시는 접은글에 넣겠다.
작성 위치 ../resources/META-INF/orm.xml (디렉토리명, 파일명 지켜야 한다)
이때 작성하는 xml은 외우지 말고,
구글에 orm native quary 또는 orm.xml example 이런 식으로 검색해서 쓰면 된다.
수행하는 쿼리문은 named-native-query 내부를 보면 된다.
name="Comment.findByNickname" : Comment 리포지토리의 findByNickname 메서드
result-class="com.example.firstproject.entity.Comment" : Comment 엔티티를 반환한다는 것
그리고나서 아래와 같이, 수행할 쿼리문을 작성해주면 된다.
<?xml version="1.0" encoding="utf-8" ?>
<entity-mappings xmlns="https://jakarta.ee/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence/orm
https://jakarta.ee/xml/ns/persistence/orm/orm_3_0.xsd"
version="3.0">
<named-native-query
name="Comment.findByNickname"
result-class="com.example.firstproject.entity.Comment">
<query>
<![CDATA[
SELECT
*
FROM
comment
WHERE
nickname = :nickname
]]>
</query>
</named-native-query>
</entity-mappings>
쿼리가 복잡하거나 최적화가 필요하면 xml, 단순한 쿼리는 @Query 어노테이션을 이용할 수 있겠다.
정해진 좋은 방법은 없다.
Test
리포지토리에 대한 테스트를 진행할 것이기 때문에,
@DataJpaTest를 붙여주자.
@DataJpaTest
class CommentRepositoryTest {
@Autowired
CommentRepository commentRepository;
@Test
@DisplayName("특정 게스글의 모든 댓글 조회")
void findByArticleId() {
/* Case 1: 4번 게시글의 모든 댓글 조회 */
{
// 준비
Long articleId = 4L;
// 수행
List<Comment> comments = commentRepository.findByArticleId(articleId);
// 예상
Article article = new Article(4L, "오늘 모하나요", "댓글부탁");
Comment a = new Comment(1L, article, "예니", "오늘은 좋은날!!");
Comment b = new Comment(2L, article, "죠지", "노래불러요");
Comment c = new Comment(3L, article, "젤리젤리", "나는 콜라 마셔욥");
List<Comment> expected = Arrays.asList(a, b, c);
// 검증
assertEquals(expected.toString(), comments.toString(), "4번 글의 모든 댓글을 출력");
}
/* Case 2: 1번 게시글의 모든 댓글 조회 */
{
// 준비
Long articleId = 1L;
// 수행
List<Comment> comments = commentRepository.findByArticleId(articleId);
// 예상
Article article = new Article(1L, "귀여운거 보고가세요", "그건바로 옌이!!");
List<Comment> expected = Arrays.asList();
// 검증
assertEquals(expected.toString(), comments.toString(), "1번 글은 댓글이 없음");
}
}
@Test
@DisplayName("특정 닉네임의 모든 댓글 조회")
void findByNickname() {
/* Case 1: "예니"의 모든 댓글 조회 */
{
// 준비
String nickname = "예니";
// 수행
List<Comment> comments = commentRepository.findByNickname(nickname);
// 예상
Comment a = new Comment(1L, new Article(4L, "오늘 모하나요", "댓글부탁"), nickname, "오늘은 좋은날!!");
Comment b = new Comment(4L, new Article(5L, "당신의 프뮤는?", "뭔가요?!"), nickname, "오스카임돠");
Comment c = new Comment(7L, new Article(6L, "지금 하고싶은 것", "뭔지알려주셈"), nickname, "비밀~!");
List<Comment> expected = Arrays.asList(a, b, c);
// 검증
assertEquals(expected.toString(), comments.toString(), "예니의 모든 댓글을 출력");
}
}
}
이번에도 역시 스스로하는 도전과제..
이 강의 코드는 안따라하고 있어서 그냥 이런 테케를 만들 수 있구나 정도만 보고 넘어가겠다.
findByArticleId()의 테스트케이스 3, 4, 5번
case 3: 9번 게시글의 모든 댓글 조회
case 4: 9999번 게시글의 모든 댓글 조회
case 5: -1번 게시글의 모든 댓글 조회
findByNickname()의 테스트케이스 2, 3, 4, 5번
case 2: "Kim"의 모든 댓글 조회
case 3: null 의 모든 댓글 조회
case 4: " "의 모든 댓글 조회
case 5: "i"가 들어간 닉네임의 모든 댓글 조회