Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: message entity, repository 구현 #424

Open
wants to merge 3 commits into
base: BE/develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.yigongil.backend.domain.message;

import com.yigongil.backend.domain.BaseEntity;
import com.yigongil.backend.domain.member.Member;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import lombok.Builder;
import lombok.Getter;

@Getter
@Entity
public class Message extends BaseEntity {

@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
private Long id;

@ManyToOne
@JoinColumn(nullable = false, updatable = false)
private Member sender;

@ManyToOne
@JoinColumn(nullable = false, updatable = false)
private Member receiver;

@Column(nullable = false)
private String content;

protected Message() {
}

@Builder
public Message(Long id, Member sender, Member receiver, String content) {
this.id = id;
this.sender = sender;
this.receiver = receiver;
this.content = content;
}

@Override
public String toString() {
return "Message{" +
"id=" + id +
", sender=" + sender +
", receiver=" + receiver +
", content='" + content + '\'' +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.yigongil.backend.domain.message;

public interface MessageListDto {

String getContent();

String getCreatedAt();

boolean getIsMine();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.yigongil.backend.domain.message;

import com.yigongil.backend.domain.member.Member;
import java.util.List;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;

public interface MessageRepository extends Repository<Message, Long> {

Message save(Message message);

@Query("""
select m.sender as sender, m.receiver as receiver
from Message m
group by m.sender, m.receiver
having m.sender = :member or m.receiver = :member
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금 쿼리는 다음과 같이 message 테이블을 풀스캔하네요.(type = all)

image

where 절을 사용해 수정하면 type = index merge 가 되어 필요한 컬럼만 효율적으로 스캔할 수 있습니다! 👍

image

추가로 이 정보도 페이징 할 것일면 order 절을 넣어주면 좋겠네요! (메서드 이름에 있는데 쿼리엔 없어요)

Suggested change
select m.sender as sender, m.receiver as receiver
from Message m
group by m.sender, m.receiver
having m.sender = :member or m.receiver = :member
SELECT m.sender AS sender, m.receiver AS receiver
FROM Message m
WHERE m.sender = :member OR m.receiver = :member
GROUP BY m.sender, m.receiver

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋은데요? 그리고 unique 값을 뽑아내기 위해 group by를 쓰셨다면 distinct를 쓰시는 걸 추천드립니다.
성능도 더 좋고 의미적으로도 더 잘 전달될 것 같아요

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추가로 인덱스 머지 최적화에 대해 가볍게 정리해봤는데 혹시 도움이 될까 싶어 링크 추가합니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋습니다

""")
List<MessageSenderReceiverDto> findBySenderOrReceiverOrderByIdDesc(Member member);

@Query("""
select m.content as content, m.createdAt as createdAt,
case when m.sender.id = :memberId then true else false end as isMine
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

별게 다있네요ㅋㅋㅋ 배워갑니다

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우리가 예전에 우려했던 대로 Pageable을 사용하면 새로운 dm을 보낼 때 과거의 메시지가 중복돼서 보이는 문제가 있을 수 있겠네요..
하지만 이런 앱의 dm에서 중복된 메시지가 등장하는 것이 유저에게 얼마나 불편함을 불러일으킬지도 미지수긴 합니다ㅎㅎ
이건 다른 분들의 의견도 들어보고 싶네요!

from Message m
where (m.sender.id = :memberId and m.receiver.id = :opponentId)
or (m.sender.id = :opponentId and m.receiver.id = :memberId)
order by m.id desc
""")
Slice<MessageListDto> findAllMessageByMemberAndPaging(Long memberId, Long opponentId, Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.yigongil.backend.domain.message;

import com.yigongil.backend.domain.member.Member;

public interface MessageSenderReceiverDto {

Member getSender();

Member getReceiver();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package com.yigongil.backend.domain.message;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;

import com.yigongil.backend.config.JpaConfig;
import com.yigongil.backend.domain.member.Member;
import com.yigongil.backend.domain.member.MemberRepository;
import com.yigongil.backend.fixture.MemberFixture;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.Import;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;

@Import(JpaConfig.class)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 안쓰면 어떻게 되나요?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 부분과 아래 Application.yml을 변경한 부분과 연관관계가 있는 것 같은데 저도 궁금하네요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

DataJpaTest 내부에 있는 AutoConfigureTestDatabase 어노테이션의 기본 설정입니다
replace.any로 DataJpaTest가 테스트 시에 임의의 인메모리 데이터베이스를 사용합니다
Replace.None으로 두지 않으면 로컬의 설정과 맞지 않아 ddl이 뜨지 않습니다

@DataJpaTest
class MessageRepositoryTest {

@Autowired
private MessageRepository messageRepository;

@Autowired
private MemberRepository memberRepository;

private Member 김진우;
private Member 노이만;
private Pageable pageable;

@BeforeEach
void setUp() {
김진우 = memberRepository.save(MemberFixture.김진우.toMember());
노이만 = memberRepository.save(MemberFixture.폰노이만.toMember());
pageable = PageRequest.of(0, 10);
}

@Test
void 메시지를_주고받은_상대들을_조회하는_쿼리() {
messageRepository.save(new Message(null, 김진우, 노이만, "Hello, world!"));
messageRepository.save(new Message(null, 노이만, 김진우, "Hello, world!"));

List<MessageSenderReceiverDto> result = messageRepository.findBySenderOrReceiverOrderByIdDesc(김진우);

assertThat(result).hasSize(2);
}

@Test
void 메시지를_주고받은_상대들을_조회하는_쿼리2() {
messageRepository.save(new Message(null, 김진우, 노이만, "Hello, world!"));
messageRepository.save(new Message(null, 김진우, 노이만, "Hello, world!"));
messageRepository.save(new Message(null, 노이만, 김진우, "Hello, world!"));

List<MessageSenderReceiverDto> result = messageRepository.findBySenderOrReceiverOrderByIdDesc(김진우);

assertThat(result).hasSize(2);
}

@Test
void 메시지를_주고받은_상대들을_조회하는_쿼리3() {
Member 파울러 = memberRepository.save(MemberFixture.마틴파울러.toMemberWithoutId());

messageRepository.save(new Message(null, 김진우, 노이만, "Hello, world!"));
messageRepository.save(new Message(null, 김진우, 노이만, "Hello, world!"));
messageRepository.save(new Message(null, 노이만, 김진우, "Hello, world!"));
messageRepository.save(new Message(null, 파울러, 김진우, "Hello, world!"));

List<MessageSenderReceiverDto> result = messageRepository.findBySenderOrReceiverOrderByIdDesc(김진우);

assertThat(result).hasSize(3);
}

@Test
void 메시지를_주고받은_상대들을_조회하는_쿼리4() {
Member 파울러 = memberRepository.save(MemberFixture.마틴파울러.toMemberWithoutId());

messageRepository.save(new Message(null, 김진우, 노이만, "Hello, world!"));
messageRepository.save(new Message(null, 김진우, 노이만, "Hello, world!"));
messageRepository.save(new Message(null, 노이만, 김진우, "Hello, world!"));
messageRepository.save(new Message(null, 파울러, 김진우, "Hello, world!"));
messageRepository.save(new Message(null, 노이만, 파울러, "Hello, world!"));

List<MessageSenderReceiverDto> result = messageRepository.findBySenderOrReceiverOrderByIdDesc(김진우);

assertThat(result).hasSize(3);
}

@Test
void 주고받은_쪽지를_정렬한다() {
messageRepository.save(new Message(null, 김진우, 노이만, "Hello, world!"));
messageRepository.save(new Message(null, 김진우, 노이만, "Hello, world!"));
messageRepository.save(new Message(null, 노이만, 김진우, "Hello, world!"));

Slice<MessageListDto> result = messageRepository.findAllMessageByMemberAndPaging(김진우.getId(), 노이만.getId(), pageable);

assertAll(
() -> assertThat(result).hasSize(3),
() -> assertThat(result.getContent().get(0).getIsMine()).isFalse(),
() -> assertThat(result.getContent().get(1).getIsMine()).isTrue(),
() -> assertThat(result.getContent().get(2).getIsMine()).isTrue()
);
}

@Test
void 주고받은_쪽지를_페이징한다() {
for (int i = 0; i < 11; i++) {
messageRepository.save(new Message(null, 김진우, 노이만, "Hello, world!"));
}

Slice<MessageListDto> result = messageRepository.findAllMessageByMemberAndPaging(김진우.getId(), 노이만.getId(), pageable);

assertAll(
() -> assertThat(result).hasSize(pageable.getPageSize()),
() -> assertThat(result.hasNext()).isTrue()
);
}

@Test
void 주고받은_쪽지를_페이징한다2() {
for (int i = 0; i < 9; i++) {
messageRepository.save(new Message(null, 김진우, 노이만, "Hello, world!"));
}

Slice<MessageListDto> result = messageRepository.findAllMessageByMemberAndPaging(김진우.getId(), 노이만.getId(), pageable);

assertAll(
() -> assertThat(result).hasSize(9),
() -> assertThat(result.hasNext()).isFalse(),
() -> assertThat(result).map(MessageListDto::getIsMine).doesNotContain(false)
);
}

@Test
void 파라미터_순서를_바꿔서_주고받은_쪽지를_페이징한다() {
for (int i = 0; i < 9; i++) {
messageRepository.save(new Message(null, 김진우, 노이만, "Hello, world!"));
}

Slice<MessageListDto> result = messageRepository.findAllMessageByMemberAndPaging(노이만.getId(), 김진우.getId(), pageable);

assertAll(
() -> assertThat(result).hasSize(9),
() -> assertThat(result.hasNext()).isFalse(),
() -> assertThat(result).map(MessageListDto::getIsMine).doesNotContain(true)
);
}
}
5 changes: 4 additions & 1 deletion backend/src/test/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ spring:
format_sql: true
show_sql: true
hibernate:
ddl-auto: create
ddl-auto: update
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
generate-ddl: true


flyway:
enabled: false

Expand Down