댓글 엔터티, 리파지터리.

하쮸·2024년 12월 2일
0

1. 댓글과 게시글의 관계.

  • 웹 사이트를 통해 게시글을 보면 하나의 게시글에 다수의 댓글이 달림.
    • 이러한 관계를 one-to-many = 일대다(1 : n)관계라고 함.
    • 반대로 댓글 입장에서 본다면 many-to-one = 다대일(n : 1)관계임.
  • DB 테이블로 생각해본다면, article 테이블, comment 테이블이 있고 각 테이블마다 PK가 있음.
    • comment테이블에 article_id속성을 추가해서 article 테이블과 조인을 걸면 됨.
    • 즉, FK키를 통해 one-to-many관계를 설정.

1-1. 이전 내용 복습, JpaRepository 계층구조.

  • JPA
    • Java로 DB에 명령을 하는 도구.
    • 데이터를 객체 지향적으로 다루는 기술.
  • Entity
    • DB 데이터를 담는 Java 객체.
    • Entity를 기반으로 테이블이 생성됨.
  • Repository
    • Entity를 관리하는 인터페이스.
    • 데이터 CRUD 등의 기능 제공.
  • JpaRepository
    • ListCrudRepositoryListPagingAndSortingRepository를 상속받는 인터페이스.
    • CRUD뿐만 아니라 엔터티페이지 단위로 조회 및 정렬하는 기능과 JPA에 특화된 여러 기능 등을 제공함.

JpaRepository 인터페이스의 계층 구조. (출처)

  • Repository
    • 최상위 리파티터리 인터페이스.
  • CrudRepository, ListCrudRepository
    • 엔터티의 CRUD 기능 제공.
  • ListPagingAndSortingRepository, PagingAndSortingRepository
    • 엔터티의 페이징 및 정렬 기능 제공.
  • JpaRepository
    • 엔터티의 CRUD 기능과 페이징 및 정렬 기능 뿐만 아니라 JPA에 특화된 기능을 추가로 제공.

2. 댓글 Entity

@Entity
@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Comment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "article_id")
    private Article article;

    @Column
    private String nickname;

    @Column
    private String body;
}
  • @Entity
    • 해당 클래스가 엔터티임을 선언. 이 클래스의 필드를 바탕으로 DB 테이블 생성.
  • @Getter
    • 각 필드 값을 조회할 수 있는 getter메서드 생성.
  • @ToString
    • toString메서드 생성.
  • @AllArgsConstructor
    • 모든 필드를 매개변수로 갖는 생성자 자동 생성.
  • @NoArgsConstructor
    • 매개변수가 아예 없는 기본 생성자 자동 생성.
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
  • @Id
    • 해당 필드가 PK임을 선언.
  • @GeneratedValue(strategy = GenerationType.IDENTITY)
    • @GeneratedValue : PK를 자동으로 1씩 증가.
    • (strategy = GenerationType.IDENTITY) : DB에서 auto_increment로 1씩 증가.(PK생성을 DB에 위임)
  • @ManyToOne
    • Comment 엔터티와 Article 엔터티를 N : 1 관계로 설정.
  • @JoinColumn(name = "article_id")
    • FK 매핑. (Article 엔터티의 PK와 매핑)
  • @Column
    • 해당 필드르르 테이블의 속성으로 매핑.

H2 DB에서 확인.

더미 데이터 추가.

ArticleComment
  • PK는 각 테이블의 id, FK는 article 테이블의 대표키를 참조하는 article_id

3. 댓글 Repository.

public interface CommentRepository extends JpaRepository<Comment, Long> {
}
  • JpaRepository<Comment, Long>
    • <>안에 대상 엔터티, PK 값의 타입을 적어주면 됨.
    • 단순 CRUD작업만 한다면 CrudRepository를 사용해도 충분.
      CRUD + 페이징 처리, 정렬 작업을 해야 한다면 JpaRepository를 사용하는 것이 좋음.

3-1. 네이티브 쿼리(Native Query).

  • 쿼리(Query)를 Java 인터페이스에 메서드로 작성하기.
    • 이러한 메서드를 네이티브 쿼리 메서드(Native query method)라고 함.
  • 네이티브 쿼리 메서드는 작성한 SQL쿼리를 Repository 메서드로 실행할 수 있게 해줌.
    • 네이티브 쿼리 메서드 만드는 방법.
      1. @Query 어노테이션을 이용.
      2. orm.xml 파일을 이용.

3-1-1. @Query를 이용.

    @Query(value = "SELECT * FROM comment WHERE article_id = :articleId", nativeQuery = true)
    List<Comment> findByArticleId(Long ArticleId);
  • findByArticleId메서드로 원하는 쿼리를 실행하기 위해 @Query어노테이션을 붙임.
    • 형식 : @Query(value = "SQL Query", nativeQuery = true)
  • @Query어노테이션은 SQL과 유사한 JPQL(Java Persistence Query Language)이라는 객체 지향 쿼리 언어를 통해 복잡한 쿼리 처리하는 걸 지원해줌.
    • 여기서 nativeQuery 속성을 true로 하면 기존 SQL문을 그대로 사용할 수 있음.
    • (주의) SQL WHERE절에 조건을 쓸 때 매개변수 앞에 콜론(:)을 붙여줘야 메서드에서 넘긴 매개변수매칭됨.

3-1-2. orm.xml을 이용.

List<Comment> findByNickname(String nickname);
  • 해당 메서드르르 수행할 쿼리를 XML로 작성.
    • 이러한 XML을 네이티브 쿼리 XML(Native query XML)이라고 함.
    • 네이티브 쿼리 XML의 기본 경로파일 이름resources -> META-INF -> orm.xml

엔터티 매핑을 위한 XML.

<?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">
</entity-mappings>
  • <entity-mappings>태그 안에 <named-native-query><query>태그를 이용해서 쿼리를 입력.
    <named-native-query name="쿼리를 수행하는 대상 엔터티.메서드 이름"
                        result-class="쿼리 수행 결과 반환하는 타입의 전체 패키지 경로">
        <query>
            <![CDATA[
            		<!-- 쿼리 입력 -->
            ]]>
        </query>
    </named-native-query>
  • <![CDATA[ ]]> 구문은 Character DATAA, 즉 파싱(구문 분석)되지 않은 문자 데이터를 쓸 때 사용함.
    • 이 구문을 사용해야 SQL 문의 값의 대소 비교 연산등에 문제가 발생하지 않음.

전체 코드.

<?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="findByNickname"
                        result-class="com.project.articlecomment.entity.Comment">
        <query>
            <![CDATA[
            SELECT * FROM comment WHERE nickname = :nickname
            ]]>
        </query>
    </named-native-query>
</entity-mappings>

4. 테스트 코드 작성.

@DataJpaTest
class CommentRepositoryTest {
    @Autowired
    CommentRepository commentRepository;
}
  • @DataJpaTest
    • 해당 클래스를 JPA와 연동해서 테스트하겠다는 선언.
  • @Autowired
    • 외부에서 객체를 주입.(DI)

4-1. 특정 게시글의 모든 댓글 조회.

    @Test
    @DisplayName("특정 게시글의 모든 댓글 조회")
    void findByArticleId() {
    }
  • @DisplayName()
    • 메서드 이름은 그대로 둔 채 테스트 이름을 바꾸고 싶을 때 사용하는 어노테이션.
    • ()안에는 문자열 형식으로 ""를 사용해서 작성.
  • 하나의 메서드로 테스트 케이스여러 개작성할 때는 테스트마다 중괄호({})로 묶어서하면 됨.
@Test
    @DisplayName("특정 게시글의 모든 댓글 조회")
    void findByArticleId() {
        {
            Long articleId = 4L;
            List<Comment> comments = commentRepository.findByArticleId(articleId);

            Article article = new Article(4L, "연말에 뭐하세요?", "댓글로 알려주세요");
            Comment a = new Comment(1L, article, "Park", "집돌이");
				// 코드 생략.
            List<Comment> expected = Arrays.asList(a, b, c);

            assertEquals(expected.toString(), comments.toString(), "4번 글의 모든 댓글 출력.");
        }
        // case 2 : 2번 게시글.
        {
            Long articleId = 1L;
            		// 코드 생략.

            assertEquals(expected.toString(), comments.toString(), "1번 글에는 댓글 없음.");
        }
    }
  • assertEquals(expected.toString(), comments.toString(), "4번 글의 모든 댓글 출력.");
    • 예상 데이터의 문자열과 실제 데이터의 문자열이 같은 지 비교함.
    • 메서드의 마지막 전달값으로 검증이 실패했을 때 출력할 메시지를 넣음.
  • List<Comment> expected = Arrays.asList(a, b, c);
    • 3개의 객체를 하나의 리스트로 합침.

4-2. 특정 닉네임의 모든 댓글 조회

@Test
    @DisplayName("특정 닉네임의 모든 댓글 조회")
    void findByNickname() {
        // case 1 : "Park"의 모든 댓글 조회.
        {
            String nickname = "Park";
            List<Comment> comments = commentRepository.findByNickname(nickname);

            Comment a =  new Comment(1L, new Article(4L, "연말에 뭐하세요?", "댓글로 알려주세요"), nickname, "집돌이");
            				// 코드 생략.
            List<Comment> expected = Arrays.asList(a, b, c);

            assertEquals(expected.toString(), comments.toString());
        }
    }
  • Park가 작성한 댓글 데이터를 Comment a, b, c 객체에 저장.
    • 여기서 1번 댓글의 부모(article)은 4번, 4번 댓글은 5번, 7번 댓글은 6번으로 부모(article)게시글이 전부 다름.
    • 즉, 하나의 article 객체만을 이용해서 참조할 수는 없으므로 a, b, c 객체 생성시 article 필드에 각각 객체생성함.

5. JpaRepository

package org.springframework.data.jpa.repository;

import java.util.List;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.ListCrudRepository;
import org.springframework.data.repository.ListPagingAndSortingRepository;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.query.QueryByExampleExecutor;

@NoRepositoryBean
public interface JpaRepository<T, ID> extends ListCrudRepository<T, ID>, ListPagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
    void flush();

    <S extends T> S saveAndFlush(S entity);

    <S extends T> List<S> saveAllAndFlush(Iterable<S> entities);

    /** @deprecated */
    @Deprecated
    default void deleteInBatch(Iterable<T> entities) {
        this.deleteAllInBatch(entities);
    }

    void deleteAllInBatch(Iterable<T> entities);

    void deleteAllByIdInBatch(Iterable<ID> ids);

    void deleteAllInBatch();

    /** @deprecated */
    @Deprecated
    T getOne(ID id);

    /** @deprecated */
    @Deprecated
    T getById(ID id);

    T getReferenceById(ID id);

    <S extends T> List<S> findAll(Example<S> example);

    <S extends T> List<S> findAll(Example<S> example, Sort sort);
}
profile
Every cloud has a silver lining.

0개의 댓글