inblog logo
|
keepgoing
    SpringBoot

    [Springboot] 19. 댓글 시작 ( 양방향 매핑, 인라인뷰 )

    김호정's avatar
    김호정
    Sep 13, 2024
    [Springboot] 19. 댓글 시작 ( 양방향 매핑, 인라인뷰 )
    Contents
    양방향 매핑
     
     
    댓글은 어디에 저장되어야 할까 ?
     
    댓글은 어디에 속할까 ? 유저 ? 보드 ?
     
    유저 / 보드 →
     
    댓글을 어디다 쓰느냐에 따라서 다르다!
     
    지금 게시글 안에 쓸거니까 (게시글이랑 독립적이지 않으니까 ) → 보드에 속한다.
     
    💡
    최소 3개의 데이터가 필요하면 스칼라가 아니라 오브젝트! 댓글 작성자명, 댓글, 댓글작성일… 등 최소 3개의 데이터 필요 → 1개의 데이터가 아니니까 무조건 테이블을 따로 만들어야한다. → 오브젝트니까! 1개의 데이터만 필요하면
     
     
    1 제목1 내용1 3(love) 글잘적었네, 못적었네, 굳(→댓글)
     
    1정규화 → 컬럼을 , 해서 적을 수 없다는 거. (원자성)
     
    1정규화 , 2 정규화, 3 정규화 하는 것보다 아래걸로 하는게 쉬움!
     
    1. 테이블을 쪼개는 근거
      1. (1) 내가 필드를 추가하고 싶은데, 오브젝트로 표현해야 할떄(스칼라로 표현이 안될때)
        → 테이블 쪼개기
        예) 댓글번호, 댓글내용, 댓글시간, 댓글주인, 댓글게시글번호 → 오브젝트
        Reply {
        r_num
        content
        created_at
        username
        board_id
        }
        → 이렇게 Reply 오브젝트 로 표현해야할 때 테이블을 쪼갠다.
         
        (2) 내가 필드를 추가하고 싶은데, 컬렉션으로 표현해야할 때 → 테이블 쪼개기
        즉, 같은 타입인데 여러개를 적어야할 때 !
        → 예 ) 1번 게시글에 댓글1 말고도 다른 댓글2, 3, 4 …. 도 들어와야 한다.
         
        1번 게시글 - 댓글1, 댓글2, 댓글3
        → 이렇게 컬렉션으로 표현해야 할때 테이블을 쪼갠다.
         
        ⇒ 지금 만드는 댓글은 오브젝트로 표현해야 하는 경우 + 컬렉션으로 표현해야 하는경우 둘다에 해당한다 → 무조건 테이블 쪼개야함
         
    1. 연관관계
      1. 지금 유저, 게시글, 댓글이 있는데 서로의 관계를 봐야한다.
         
        1대 N 이런건 “다른 테이블에 자기가 행을 몇개나 만들 수 있느냐”에 따라 정해진다.
        유저(1)(1) 게시글(N)(1) → 1 대 N ( 유저는 게시글 테이블에 행을 여러개 만들 수 있으니 1대 N이고
        게시글은 유저 테이블에 자기 행을 1개밖에 못만드니 1대 1이다 → 1 : N )
        유저(1)(1) 댓글(N)(1) → 1대 N
        게시글(1)(1) 댓글(N)(1) → 1대 N ( 게시글은 댓글 테이블에 여러행을 만들 수 있으니 1대N이고
        댓글은 게시글 테이블에 댓글행을 1개씩 밖에 못만드니 1대1 → 1 : N )
         
         
        N쪽에 fk랑 driving table이 있어야 하니,
        댓글(N)
         
        ⇒ PK, user_id, board_id, comment(댓글내용), created_at(등록일)
         
        이런식으로 테이블을 설계한다!
        notion image
         
        우리 프로젝트로 생각하면
         
        영화
        영화관(CGV_부산대연)
        상영관(1관, 2관, 3관)
        상영시간(10시, 11시, 12시)
        고객
         
        영화(1)(N) 영화관(N)(1) → N대 N
         
        영화를 배급한다. (행위) → N : N은 중간에 무조건 행위테이블이 나온다.
         
        어떤 영화관이 어떤 영화를 들고있는지 배급 테이블이 필요해진다.
         
        배급 테이블
        1 영화_ID, 영화관_ID
    영화관(1)(1) 상영관(N)(1) → 1대N
     
     

     
    Board 테이블안에 reply 패키지를 만듦
    → board를 조회하면 join해서 댓글을 상세페이지에 뿌리면 되니까 따로 C S R 이 필요없음
     
    /board/1/reply/2
    게시글 몇번에 댓글 몇번 삭제가 더 쉽게 와닿는다!
     
    그래서 BOARD안에 REPLY 패키지를 만들어놓고
    REPLY 엔티티, 레파지토리만 만들고 서비스랑 컨트롤러 안 만드는 회사가 있다.
    게시글 상세보기 하는 비즈니스 안에서 댓글이 필요한 거니까!
    (boardController랑 Service에서 사용)
     
    notion image
     
    위처럼 만드는데 우리는 일단 이렇게 안하고 ( → 어려울 수 있으니) 기존 방식대로 한다.
     
    shop.mtcoding.springv3 패키지 아래에 reply 패키지를 생성하고
    Reply 엔티티를 만들어준다.
    notion image
    notion image
    reply 엔티티
    @NoArgsConstructor @Entity // DB에서 조회하면 자동 매핑이됨 @Getter @Setter @Table(name = "reply_tb") // 테이블 명 설정해주기 public class Reply { @GeneratedValue(strategy = GenerationType.IDENTITY) @Id // PK 설정 private Integer id; private String comment; // 댓글 내용 @ManyToOne(fetch = FetchType.LAZY) private Board board; @JsonIgnoreProperties({"password"}) @ManyToOne(fetch = FetchType.LAZY) // 이걸 안적으면 하이버네이트가 오브젝트로 인식. 이걸 적어줘야 아 fk구나 하고 이해한다. private User user; @CreationTimestamp // em.persist 할때만 발동. 네이티브 쿼리 쓰면 발동안한다. 네이티브 쿼리쓰면 now()라고 내가 넣어줘야한다. private Timestamp createdAt; }
     
    ++
    name = “user_tb” 테이블 이름에 _tb 왠만하면 붙여주기.
    _tb 안붙이면 안만들어질 때가 있다.
    그렇다고 user 테이블 member 라고 만들지 말기
    user_tb 로 만들고 users 이렇게 임의로 수정해서 만들지 말기!
    컨벤션을 지키자 : )
     
     
    notion image
    이렇게 만들어짐!
     
    • fk 설정 → 마이바티스에서는 board_id 이렇게 넣는데 JPA는 객체를 넣는다
    → Board, User
    • @ManyToOne 안 걸어주면 하이버네이트가 오브젝트로 인식해서 어떻게 테이블 만들어라는 거야 하는데 걸어주면 fk구나 하고 알게된다.
     

    양방향 매핑

    상세보기를 할때 게시글 , 유저, 댓글이 같이 나와야하는데
    셋을 조인하기위해 만들 mFindByIdWithReply 매서드를 사용해서 값을 다 들고와도ㅛ
    같이 담길 오브젝트가 없음
     
    → 비즈니스적으로 필요한 데이터가 있을때는 반대방향으로 한번 걸어줘야 한다.
     
    → 양방향 매핑!
     
    notion image
     
    Board 에 replies를 추가하고 관계설정하고 mappedBy도 써준다.
    → mappedBy 안해주면 이걸 fk로 인식하기 때문에 mappedBy 까지 적어줘야 한다.
    → mappedBy 를 적어줌으로써 List<Reply> replies는 “ 난 fk가 아니야. Reply의 “board”가 fk 야” 라고 알려줄 수 있음
     
     
    만약 내가 조회를 2번 실행할꺼면 이렇게 할 필요는 없다.
    board & user 조인해서 1번 들고오고 (select), reply 1번 들고오면 되는데 (select)
    양방향매핑으로 한꺼번에 (1번만 select) 들고오려면 이렇게 설정해줘야 한다.
     
    서버 재실행해주면 Reply 테이블이 잘 만들어지는 걸 확인할 수 있다.

     
    @Builder 어노테이션은 왜 Class 위에 안달고 생성자에 달까?
     
    builder를 클래스위에 달려면 @allArgsContructor 가 필요하다
     
    근데 빌더패턴은 컬렉션을 포함시키지 못한다.
    → class에 @Builder 를 달 경우에 지역필드에 있는 List<Reply> 에서 에러남.
    notion image
    List 컬렉션
    List 컬렉션
    그래서 클래스에 빌더 어노테이션을 붙이면 돌다가 터질수있다.
     
    빌더패턴안에 컬렉션 빼주기 !
     
    양방향 매핑때문에 컬렉션이 생성자 안에 들어가서 에러가나는데 이럼 디버깅이 안된다 ㅎ
     
    → 양방향 매핑을 하려면 Board에 List<Reply> replies를 추가해 줘야 하는데
    ( fk 아님 . 양방향 매핑 해줘서 테이블 생성 시 column 으로 생성되지도 않음. )
    class Board 위에 빌더 어노테이션 붙이면 매개변수로 저 컬렉션도 들어가니까
    따로 생성자 만들어서 붙여주기 ;)
     
    이안에 위의 REPLY 컬렉션이 들어가면 안돼!
    이안에 위의 REPLY 컬렉션이 들어가면 안돼!
    • 컬렉션 들어가면 안돼
    • User는 fk
     
    이제 댓글 추가해보자!
     
    제일 먼저 화면 만들고 더미 만들기! 화면이 없으면 아무것도 못함
     
     
    detail.mustache 에 댓글 추가
    <!-- 댓글 --> <div class="card mt-3"> <!-- 댓글등록 --> <div class="card-body"> <form action="/reply/save" method="post"> <textarea class="form-control" rows="2" name="comment"></textarea> <div class="d-flex justify-content-end"> <button type="submit" class="btn btn-outline-primary mt-1">댓글등록</button> </div> </form> </div> <!-- 댓글목록 --> <div class="card-footer"> <b>댓글리스트</b> </div> <div class="list-group"> <!-- 댓글아이템 --> <div class="list-group-item d-flex justify-content-between align-items-center"> <div class="d-flex"> <div class="px-1 me-1 bg-primary text-white rounded">ssar</div> <div>댓글1</div> </div> <form action="#" method="post"> <button class="btn">🗑</button> </form> </div> </div> </div>
     
    detail.mustache 전체코드
    {{>layout/header}} <div class="container p-5"> <!-- 수정삭제버튼 --> {{#model.isOwner}} <div class="d-flex justify-content-end"> <a href="/api/board/{{model.id}}/update-form" class="btn btn-warning me-1">수정</a> <form action="/api/board/{{model.id}}/delete" method="post"> <button class="btn btn-danger">삭제</button> </form> </div> {{/model.isOwner}} <div class="d-flex justify-content-end"> <b>작성자</b> : {{model.username}} </div> <!-- 게시글내용 --> <div> <h2><b>{{model.title}}</b></h2> <hr/> <div class="m-4 p-2"> {{{model.content}}} </div> </div> <!-- 댓글 --> <div class="card mt-3"> <!-- 댓글등록 --> <div class="card-body"> <form action="/reply/save" method="post"> <textarea class="form-control" rows="2" name="comment"></textarea> <div class="d-flex justify-content-end"> <button type="submit" class="btn btn-outline-primary mt-1">댓글등록</button> </div> </form> </div> <!-- 댓글목록 --> <div class="card-footer"> <b>댓글리스트</b> </div> <div class="list-group"> <!-- 댓글아이템 --> <div class="list-group-item d-flex justify-content-between align-items-center"> <div class="d-flex"> <div class="px-1 me-1 bg-primary text-white rounded">ssar</div> <div>댓글1</div> </div> <form action="#" method="post"> <button class="btn">🗑</button> </form> </div> </div> </div> </div> {{>layout/footer}}
     
     
    insert into reply_tb(comment, board_id, user_id, created_at) values('댓글1', 5, 1, now()); insert into reply_tb(comment, board_id, user_id, created_at) values('댓글2', 5, 1, now()); insert into reply_tb(comment, board_id, user_id, created_at) values('댓글3', 5, 2, now()); insert into reply_tb(comment, board_id, user_id, created_at) values('댓글4', 4, 2, now()); insert into reply_tb(comment, board_id, user_id, created_at) values('댓글5', 3, 2, now());
     
    댓글 더미 데이터 추가
    게시글 5 에 댓글 3개, 게시글 4에 댓글 1개, 게시글 3에 댓글 1개
     
    insert into user_tb(username, password, email, created_at) values ('ssar', '1234', 'ssar@nate.com', now()); insert into user_tb(username, password, email, created_at) values ('cos', '1234', 'cos@nate.com', now()); insert into user_tb(username, password, email, created_at) values ('love', '1234', 'love@nate.com', now()); insert into board_tb(title, content, created_at, user_id) values ('제목1', '내용1', now(), 1); insert into board_tb(title, content, created_at, user_id) values ('제목2', '내용2', now(), 1); insert into board_tb(title, content, created_at, user_id) values ('제목3', '내용3', now(), 2); insert into board_tb(title, content, created_at, user_id) values ('제목4', '내용4', now(), 2); insert into board_tb(title, content, created_at, user_id) values ('제목5', '내용5', now(), 2); insert into reply_tb(comment, board_id, user_id, created_at) values('댓글1', 5, 1, now()); insert into reply_tb(comment, board_id, user_id, created_at) values('댓글2', 5, 1, now()); insert into reply_tb(comment, board_id, user_id, created_at) values('댓글3', 5, 2, now()); insert into reply_tb(comment, board_id, user_id, created_at) values('댓글4', 4, 2, now()); insert into reply_tb(comment, board_id, user_id, created_at) values('댓글5', 3, 2, now());
     
    엔포드는 테이블이 2개일때만 !
    → 지금 만드는 매서드처럼
    신경써야하는 테이블이 3개 ( User, reply, board )일때는 신경 쓰지 않아도 된다.
     
    notion image
    SELECT * FROM board_tb bt INNER JOIN user_tb ut ON bt.user_id = ut.id;
     
    각각의 게시글에 user를 옆에다 하나씩 붙여줌
     
    여기서 게시글 id 가 5인걸 찾자
     
    notion image
    SELECT * FROM board_tb bt INNER JOIN user_tb ut ON bt.user_id = ut.id where bt.id = 5;
     
    마이바티스와 다르게 하이버네이트에서는 이 필드를 받는 dto를 안만들어도 됨
     
    board객체안에 reply객체가 들어가 있으니까 !
     
    위의 조인된 결과를 하나의 테이블로 보고! 여기다가 reply도 조인
     
     

     
    인라인뷰 라는 서브쿼리를 써보자
    ( → from 절에 결과를 집어넣어서 테이블화 시키는 것 )
    notion image
    SELECT * FROM ( SELECT bt.id, bt.title, bt.content, ut.username FROM board_tb bt INNER JOIN user_tb ut ON bt.user_id = ut.id where bt.id = 5 );
     
    인라인 뷰는 from 뒤의 괄호 안에 결과를 집어넣어서
    그거 자체를 테이블로 만드는 것!
     
    notion image
    SELECT bt.id, bt.title, bt.content, ut.id user_id, ut.username FROM board_tb bt INNER JOIN user_tb ut ON bt.user_id = ut.id where bt.id = 5
     
    ut.id 에 user_id 라고 별칭을 주고 조회해도 잘 된다.
     
    notion image
     
    where 조건에 위에서 적은 별칭을 사용하니 “별칭이름 not found”라고 뜬다.
     
    왜 별칭을 인식하지 못할까?
     
    쿼리를 제일 먼저 실행하는게 from이다.
    하드에서 퍼올려서 메모리에 올림.
    그리고 나서 하는게 where 절이 실행된다.
    select 절은 마지막에 실행된다.( 프로젝션 )
    (→ select 절이 먼저 실행되면 쓸데없는 프로젝션을 많이 해야한다… )
    → 그래서 select 절에 u_id 라고 별칭을 줬는데
    몰라서 에러가 남.
     
    이걸 해결할 수 있는게 인라인 뷰
     
    notion image
    select * from ( SELECT bt.id, bt.title, bt.content, ut.id u_id, ut.username FROM board_tb bt INNER JOIN user_tb ut ON bt.user_id = ut.id );
     
    from이 실행될때 연산이 메모리에 있기 때문에 u_id가 이미
    실행이 되어서
    select * 여기서 프로젝션 할 때 u_id를 알고 있다.
     
    이럴때 인라인 뷰를 쓴다.
     
    where 절 붙여주기
     
    notion image
    select * from ( SELECT bt.id, bt.title, bt.content, ut.id u_id, ut.username FROM board_tb bt INNER JOIN user_tb ut ON bt.user_id = ut.id ) where u_id=2;
     
    인라인 뷰로 바꾸니까 별칭 u_id로 조회된다.
     
    notion image
    SELECT * FROM ( SELECT bt.id, bt.title, bt.content, ut.username FROM board_tb bt INNER JOIN user_tb ut ON bt.user_id = ut.id where bt.id = 5 );
    아까 작성한 board & user 조인한 쿼리에다가 reply를 조인 한 번 해보자!
     
    • 한번 조인한거에 또 조인하면 인라인뷰를 쓴다고 생각하면 됨
     
    notion image
    SELECT * FROM ( SELECT bt.id, bt.title, bt.content, ut.username FROM board_tb bt INNER JOIN user_tb ut ON bt.user_id = ut.id where bt.id = 5 ) t1 inner join reply_tb t2 on t1.id = t2.board_id;
     
    t1 이라는 별칭을 주고, t1.id 와 t2.board_id 를 on 뒤에 적어준다.
     
    댓글 조인 완료!
     
    이거 자체를 테이블로 보고 또 조인하면 된다.
     
     
    위의 user_id 가 댓글의 user_id인데 헷갈리면 별칭 주기!
     
    이렇게 별칭을 줄 수 있다.
    이렇게 별칭을 줄 수 있다.
     
    notion image
    네모를 하나의 테이블로 본다.
     
    이 테이블에 h1이라는 별칭을 주자.
     
    조인할 user_tb에는 h2라는 별칭을 주자.
     
    h1의 user_id 와 h2의 id가 같은걸 뽑자!
    notion image
    근데 * 쓰면 id 를 못찾아서…. 에러남
     
    원래 이전 select * 에서 * 쓰지말고 bt.title 이런식으로
    잡고 왔어야 함.
     
    ++
    복습할때 * 안쓰고 별칭으로 가져와봄
    notion image
     
    SELECT h1.id, h1.title, h1.content, h1.u_id b_u_id, h1.email, h1.password, h1.username b_username, h1.r_id, h1.r_user_id, h1.created_at, h1.comment, h2.id, h2.username, h2.password, h2.email FROM ( SELECT t1.id, t1.user_id, t1.title, t1.content, t1.u_id, t1.email, t1.password, t1.username, t2.id r_id, t2.user_id r_user_id, t2.created_at, t2.comment FROM ( SELECT bt.id, bt.user_id, bt.title, bt.content, ut.id u_id, ut.email, ut.password, ut.username FROM board_tb bt INNER JOIN user_tb ut ON bt.user_id = ut.id where bt.id = 5 ) t1 inner join reply_tb t2 on t1.id = t2.board_id ) h1 inner join user_tb h2 on h1.r_user_id = h2.id;
     
    인라인 뷰 안쓰는 방법으로 다시 써보자
     
     
    notion image
     
    인라인 뷰 안쓰고 하니 잘 나온다.
     
    SELECT * FROM board_tb bt INNER JOIN user_tb ut ON bt.user_id = ut.id INNER JOIN reply_tb rt on bt.id = rt.board_id INNER JOIN user_tb rut on rut.id = rt.user_id where bt.id = 5;
    notion image
     
    여기서 화면에 필요한 것만 골라보자
     
    notion image
    게시글 3줄이 중복된다. 1줄만 있으면 된다
    notion image
    유저도 3줄이 중복된다. 1줄만 있으면 된다
    notion image
    댓글은 3줄이 다 필요하다.
     
     
    notion image
    게시글 2번을 조회하면 결과가 안나온다.
     
    게시글 2에는 댓글이 없는데 이너조인해서 그럼
     
    이럴땐 아우터 조인으로 바꾼다.
     
    notion image
    SELECT * FROM board_tb bt INNER JOIN user_tb ut ON bt.user_id = ut.id LEFT OUTER JOIN reply_tb rt on bt.id = rt.board_id INNER JOIN user_tb rut on rut.id = rt.user_id where bt.id = 2;
    이렇게 했는데 결과가 안나오네 ?
     
    이럴땐 한줄씩 실행해보기
    notion image
    여기는 되고
     
    notion image
    여기까지 되고
    → left outer join 했으니까 댓글이 없는 게시글 1, 2, 도 조회된다.
     
    if ) 만약 댓글을 inner join으로 하면 댓글이 없는 게시물은 조회되지 않는다 :)
    notion image
     
    notion image
    여까지 되는데
     
    왜 안되는 거지
     
     
    notion image
    마지막줄을 left outer join으로 바꾸니까 된다.
     
    중간에 reply_tb가 없을 수 잇는데 LEFT OUTER JOIN 을 했으니
    뒤에 줄에도 LEFT OUTER JOIN 을 해줘야 한다.
    SELECT * FROM board_tb bt INNER JOIN user_tb ut ON bt.user_id = ut.id LEFT OUTER JOIN reply_tb rt on bt.id = rt.board_id LEFT OUTER JOIN user_tb rut on rut.id = rt.user_id where bt.id = 2;
    notion image
     
    게시글 5번으로 where 조건을 줘서 보면
     
    1. board_tb
    1. user_tb
    1. reply_tb
    1. user_tb
     
    이렇게 4개가 join 되어서 나온 것을 볼 수 있다.
     
    중간에 user_tb가 있는데 왜 또 user_tb를 join 했는가 ?
     
    위에 inner join 해서 가져온 user_tb는 게시글 작성자에 해당하는 user 정보만 가지고 온 것이고,
    아래에 left outer join 해서 가져온 user_tb 는 reply 테이블의 user, 즉 reply 작성한 user 정보를
    가지고 오기 위해서 한 번 더 join 한 것이다. !
     
    결국 게시글 5번의 게시글 정보, 게시글작성자 정보, 게시글 댓글 정보, 댓글작성자 정보 를
    한꺼번에 JOIN으로 가져온 것 !
     
    네이티브 쿼리를 사용하면 이렇게 가져와야 하고
     
    notion image
     
    JPQL로 작성하면 이렇게 간단하다!
     
    근데 이렇게 쿼리를 짜면 네이티브 쿼리 짜는 실력이 안는다.
     
    그래서 신입일때는 네이티브 쿼리로 짜는게 좋을 수 있다 !
     
     
     
    Share article
    Contents
    양방향 매핑

    keepgoing

    RSS·Powered by Inblog