JavaRush /Java Blog /Random-KO /"A부터 Z까지의 Java 프로젝트" 그룹에서 기사 구독을 제거합니다.
Roman Beekeeper
레벨 35

"A부터 Z까지의 Java 프로젝트" 그룹에서 기사 구독을 제거합니다.

Random-KO 그룹에 게시되었습니다
사랑하는 친구 여러분, 미래의 수석 소프트웨어 엔지니어 여러분, 안녕하세요. 우리는 계속해서 텔레그램 봇을 개발하고 있습니다. 이번 프로젝트 단계에서는 소프트웨어 가치보다 가시적인 가치가 더 높은 세 가지 작업을 살펴보겠습니다. 특정 그룹에서 새 기사에 대한 구독을 제거하는 방법을 배워야 합니다. /stop 명령을 사용하여 봇을 비활성화하고 /start 명령을 사용하여 활성화합니다. 모든 요청과 업데이트는 봇의 활성 사용자에게만 적용됩니다. 평소와 같이 메인 브랜치를 업데이트하여 모든 변경 사항을 적용하고 새 브랜치인 STEP_7_JRTB-7을 생성합니다. 이 부분에서는 구독 삭제에 대해 이야기하고 이벤트에 대한 5가지 옵션을 고려하겠습니다. 흥미로울 것입니다.

JRTB-7: 그룹에서 새 기사 구독 제거

모든 사용자는 새 기사에 대한 알림을 받지 않기 위해 구독을 삭제할 수 있기를 원할 것입니다. 해당 논리는 구독을 추가하는 논리와 매우 유사합니다. 하나의 명령만 보내면 이에 대한 응답으로 사용자가 이미 구독하고 있는 그룹 및 해당 ID 목록을 수신하므로 삭제해야 할 것이 정확히 무엇인지 이해할 수 있습니다. 그리고 사용자가 팀과 함께 그룹 ID를 보내면 구독이 삭제됩니다. 따라서 텔레그램 봇 측에서 이 명령을 개발해 보겠습니다.
  1. 새 명령의 이름( /deleteGroupSub ) 을 추가하고 CommandName 에 다음 줄을 추가해 보겠습니다.

    DELETE_GROUP_SUB("/deleteGroupSub")

  2. 다음으로, DeleteGroupSubCommand 명령을 생성해 보겠습니다 .

    package com.github.javarushcommunity.jrtb.command;
    
    import com.github.javarushcommunity.jrtb.repository.entity.GroupSub;
    import com.github.javarushcommunity.jrtb.repository.entity.TelegramUser;
    import com.github.javarushcommunity.jrtb.service.GroupSubService;
    import com.github.javarushcommunity.jrtb.service.SendBotMessageService;
    import com.github.javarushcommunity.jrtb.service.TelegramUserService;
    import org.springframework.util.CollectionUtils;
    import org.telegram.telegrambots.meta.api.objects.Update;
    
    import javax.ws.rs.NotFoundException;
    import java.util.List;
    import java.util.Optional;
    import java.util.stream.Collectors;
    
    import static com.github.javarushcommunity.jrtb.command.CommandName.DELETE_GROUP_SUB;
    import static com.github.javarushcommunity.jrtb.command.CommandUtils.getChatId;
    import static com.github.javarushcommunity.jrtb.command.CommandUtils.getMessage;
    import static java.lang.String.format;
    import static org.apache.commons.lang3.StringUtils.SPACE;
    import static org.apache.commons.lang3.StringUtils.isNumeric;
    
    /**
    * Delete Group subscription {@link Command}.
    */
    public class DeleteGroupSubCommand implements Command {
    
       private final SendBotMessageService sendBotMessageService;
       private final TelegramUserService telegramUserService;
       private final GroupSubService groupSubService;
    
       public DeleteGroupSubCommand(SendBotMessageService sendBotMessageService, GroupSubService groupSubService,
                                    TelegramUserService telegramUserService) {
           this.sendBotMessageService = sendBotMessageService;
           this.groupSubService = groupSubService;
           this.telegramUserService = telegramUserService;
       }
    
       @Override
       public void execute(Update update) {
           if (getMessage(update).equalsIgnoreCase(DELETE_GROUP_SUB.getCommandName())) {
               sendGroupIdList(getChatId(update));
               return;
           }
           String groupId = getMessage(update).split(SPACE)[1];
           String chatId = getChatId(update);
           if (isNumeric(groupId)) {
               Optional<GroupSub> optionalGroupSub = groupSubService.findById(Integer.valueOf(groupId));
               if (optionalGroupSub.isPresent()) {
                   GroupSub groupSub = optionalGroupSub.get();
                   TelegramUser telegramUser = telegramUserService.findByChatId(chatId).orElseThrow(NotFoundException::new);
                   groupSub.getUsers().remove(telegramUser);
                   groupSubService.save(groupSub);
                   sendBotMessageService.sendMessage(chatId, format("Удалил подписку на группу: %s", groupSub.getTitle()));
               } else {
                   sendBotMessageService.sendMessage(chatId, "Не нашел такой группы =/");
               }
           } else {
               sendBotMessageService.sendMessage(chatId, "неправильный формат ID группы.\n " +
                       "ID должно быть целым положительным числом");
           }
       }
    
       private void sendGroupIdList(String chatId) {
           String message;
           List<GroupSub> groupSubs = telegramUserService.findByChatId(chatId)
                   .orElseThrow(NotFoundException::new)
                   .getGroupSubs();
           if (CollectionUtils.isEmpty(groupSubs)) {
               message = "Пока нет подписок на группы. Whatбы добавить подписку напиши /addGroupSub";
           } else {
               message = "Whatбы удалить подписку на группу - передай комадну вместе с ID группы. \n" +
                       "Например: /deleteGroupSub 16 \n\n" +
                       "я подготовил список всех групп, на которые ты подписан) \n\n" +
                       "Name группы - ID группы \n\n" +
                       "%s";
    
           }
           String userGroupSubData = groupSubs.stream()
                   .map(group -> format("%s - %s \n", group.getTitle(), group.getId()))
                   .collect(Collectors.joining());
    
           sendBotMessageService.sendMessage(chatId, format(message, userGroupSubData));
       }
    }

이를 위해 GroupSub 엔터티 작업을 위해 ID로 데이터베이스에서 검색하고 엔터티 자체를 저장하는 두 가지 방법을 더 추가해야 했습니다. 이러한 모든 메소드는 이미 만들어진 저장소 메소드를 호출합니다. 구독 삭제에 대해서는 별도로 안내해 드리겠습니다. 데이터베이스 스키마에서는 다대다 프로세스를 담당하는 테이블로, 이 관계를 삭제하려면 그 안의 레코드를 삭제해야 한다. 이는 데이터베이스 부분의 일반적인 이해를 사용하는 경우입니다. 그러나 우리는 Spring Data를 사용하고 기본적으로 이를 다르게 수행할 수 있는 Hibernate가 있습니다. 우리는 관련된 모든 사용자가 그려지는 GroupSub 엔터티를 얻습니다. 이 사용자 컬렉션에서 필요한 사용자를 제거하고 이 사용자 없이 groupSub를 데이터베이스에 다시 저장합니다. 이런 식으로 Spring Data는 우리가 원하는 것을 이해하고 레코드를 삭제합니다. "A부터 Z까지의 Java 프로젝트": 그룹에서 기사 구독 제거 - 1"A부터 Z까지의 Java 프로젝트": 그룹에서 기사 구독 제거 - 2사용자를 빠르게 제거하기 위해 문제가 없도록 GroupSub 목록을 제외하고 TelegramUser에 EqualsAndHashCode 주석을 추가했습니다. 그리고 필요한 사용자와 함께 사용자 컬렉션에 대해 제거 메소드를 호출했습니다. TelegramUser의 모습은 다음과 같습니다.
@Data
@Entity
@Table(name = "tg_user")
@EqualsAndHashCode(exclude = "groupSubs")
public class TelegramUser {

   @Id
   @Column(name = "chat_id")
   private String chatId;

   @Column(name = "active")
   private boolean active;

   @ManyToMany(mappedBy = "users", fetch = FetchType.EAGER)
   private List<GroupSub> groupSubs;
}
결과적으로 모든 것이 우리가 원하는 대로 이루어졌습니다. 팀에는 여러 가지 가능한 이벤트가 있으므로 각 이벤트에 대해 좋은 테스트를 작성하는 것이 좋습니다. 테스트에 대해 말하자면, 테스트를 작성하는 동안 로직의 결함을 발견하고 프로덕션에 출시되기 전에 수정했습니다. 테스트가 없었다면 얼마나 빨리 발견됐을지는 불분명합니다. 삭제그룹하위명령테스트:
package com.github.javarushcommunity.jrtb.command;

import com.github.javarushcommunity.jrtb.repository.entity.GroupSub;
import com.github.javarushcommunity.jrtb.repository.entity.TelegramUser;
import com.github.javarushcommunity.jrtb.service.GroupSubService;
import com.github.javarushcommunity.jrtb.service.SendBotMessageService;
import com.github.javarushcommunity.jrtb.service.TelegramUserService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.telegram.telegrambots.meta.api.objects.Update;

import java.util.ArrayList;
import java.util.Optional;

import static com.github.javarushcommunity.jrtb.command.AbstractCommandTest.prepareUpdate;
import static com.github.javarushcommunity.jrtb.command.CommandName.DELETE_GROUP_SUB;
import static java.util.Collections.singletonList;

@DisplayName("Unit-level testing for DeleteGroupSubCommand")
class DeleteGroupSubCommandTest {

   private Command command;
   private SendBotMessageService sendBotMessageService;
   GroupSubService groupSubService;
   TelegramUserService telegramUserService;


   @BeforeEach
   public void init() {
       sendBotMessageService = Mockito.mock(SendBotMessageService.class);
       groupSubService = Mockito.mock(GroupSubService.class);
       telegramUserService = Mockito.mock(TelegramUserService.class);

       command = new DeleteGroupSubCommand(sendBotMessageService, groupSubService, telegramUserService);
   }

   @Test
   public void shouldProperlyReturnEmptySubscriptionList() {
       //given
       Long chatId = 23456L;
       Update update = prepareUpdate(chatId, DELETE_GROUP_SUB.getCommandName());

       Mockito.when(telegramUserService.findByChatId(String.valueOf(chatId)))
               .thenReturn(Optional.of(new TelegramUser()));

       String expectedMessage = "Пока нет подписок на группы. Whatбы добавить подписку напиши /addGroupSub";

       //when
       command.execute(update);

       //then
       Mockito.verify(sendBotMessageService).sendMessage(chatId.toString(), expectedMessage);
   }

   @Test
   public void shouldProperlyReturnSubscriptionLit() {
       //given
       Long chatId = 23456L;
       Update update = prepareUpdate(chatId, DELETE_GROUP_SUB.getCommandName());
       TelegramUser telegramUser = new TelegramUser();
       GroupSub gs1 = new GroupSub();
       gs1.setId(123);
       gs1.setTitle("GS1 Title");
       telegramUser.setGroupSubs(singletonList(gs1));
       Mockito.when(telegramUserService.findByChatId(String.valueOf(chatId)))
               .thenReturn(Optional.of(telegramUser));

       String expectedMessage = "Whatбы удалить подписку на группу - передай комадну вместе с ID группы. \n" +
               "Например: /deleteGroupSub 16 \n\n" +
               "я подготовил список всех групп, на которые ты подписан) \n\n" +
               "Name группы - ID группы \n\n" +
               "GS1 Title - 123 \n";

       //when
       command.execute(update);

       //then
       Mockito.verify(sendBotMessageService).sendMessage(chatId.toString(), expectedMessage);
   }

   @Test
   public void shouldRejectByInvalidGroupId() {
       //given
       Long chatId = 23456L;
       Update update = prepareUpdate(chatId, String.format("%s %s", DELETE_GROUP_SUB.getCommandName(), "groupSubId"));
       TelegramUser telegramUser = new TelegramUser();
       GroupSub gs1 = new GroupSub();
       gs1.setId(123);
       gs1.setTitle("GS1 Title");
       telegramUser.setGroupSubs(singletonList(gs1));
       Mockito.when(telegramUserService.findByChatId(String.valueOf(chatId)))
               .thenReturn(Optional.of(telegramUser));

       String expectedMessage = "неправильный формат ID группы.\n " +
               "ID должно быть целым положительным числом";

       //when
       command.execute(update);

       //then
       Mockito.verify(sendBotMessageService).sendMessage(chatId.toString(), expectedMessage);
   }

   @Test
   public void shouldProperlyDeleteByGroupId() {
       //given

       /// prepare update object
       Long chatId = 23456L;
       Integer groupId = 1234;
       Update update = prepareUpdate(chatId, String.format("%s %s", DELETE_GROUP_SUB.getCommandName(), groupId));


       GroupSub gs1 = new GroupSub();
       gs1.setId(123);
       gs1.setTitle("GS1 Title");
       TelegramUser telegramUser = new TelegramUser();
       telegramUser.setChatId(chatId.toString());
       telegramUser.setGroupSubs(singletonList(gs1));
       ArrayList<TelegramUser> users = new ArrayList<>();
       users.add(telegramUser);
       gs1.setUsers(users);
       Mockito.when(groupSubService.findById(groupId)).thenReturn(Optional.of(gs1));
       Mockito.when(telegramUserService.findByChatId(String.valueOf(chatId)))
               .thenReturn(Optional.of(telegramUser));

       String expectedMessage = "Удалил подписку на группу: GS1 Title";

       //when
       command.execute(update);

       //then
       users.remove(telegramUser);
       Mockito.verify(groupSubService).save(gs1);
       Mockito.verify(sendBotMessageService).sendMessage(chatId.toString(), expectedMessage);
   }

   @Test
   public void shouldDoesNotExistByGroupId() {
       //given
       Long chatId = 23456L;
       Integer groupId = 1234;
       Update update = prepareUpdate(chatId, String.format("%s %s", DELETE_GROUP_SUB.getCommandName(), groupId));


       Mockito.when(groupSubService.findById(groupId)).thenReturn(Optional.empty());

       String expectedMessage = "Не нашел такой группы =/";

       //when
       command.execute(update);

       //then
       Mockito.verify(groupSubService).findById(groupId);
       Mockito.verify(sendBotMessageService).sendMessage(chatId.toString(), expectedMessage);
   }
}
여기서 각 테스트는 별도의 시나리오를 확인하며 그 중 5개만 있음을 상기시켜 드립니다.
  • 단순히 /deleteGroupSub 명령을 전달했고 그룹 구독이 없는 경우
  • 단순히 /deleteGroupSub 명령을 전달 하고 그룹에 대한 구독이 있는 경우
  • 잘못된 그룹 ID가 전달된 경우(예: /deleteGroupSub abc )
  • 예상대로 모든 것이 올바르게 삭제되는 시나리오;
  • 그룹 ID는 유효하지만 해당 그룹이 데이터베이스에 없는 경우의 시나리오입니다.
보시다시피 이러한 모든 시나리오는 테스트로 다루어져야 합니다. 글을 쓰면서 더 나은 테스트를 작성하려면 몇 가지 테스트 과정을 수강할 가치가 있다는 것을 깨달았습니다. 나는 이것이 다른 옵션을 적절하게 찾는 데 도움이 될 것이라고 생각합니다. 맞습니다, 미래에 대한 생각입니다. 다음으로 이제 구독을 삭제할 수 있다는 설명을 /help 명령 에 추가해야 합니다 . 구독 작업을 위한 섹션에 배치하겠습니다. "A부터 Z까지의 Java 프로젝트": 그룹에서 기사 구독 제거 - 3물론 이 명령이 작동하려면 CommandContainer 에 초기화를 추가해야 합니다 .
.put(DELETE_GROUP_SUB.getCommandName(),
       new DeleteGroupSubCommand(sendBotMessageService, groupSubService, telegramUserService))
이제 테스트 봇에서 기능을 테스트할 수 있습니다. docker-compose-test.yml을 사용하여 데이터베이스를 시작합니다. docker-compose -f docker-compose-test.yml up 그리고 IDEA를 통해 SpringBoot를 시작합니다. 봇과의 서신을 완전히 삭제하고 다시 시작하겠습니다. 이 팀과 협력할 때 발생할 수 있는 모든 옵션을 살펴보겠습니다. "A부터 Z까지의 Java 프로젝트": 그룹에서 기사 구독 제거 - 4스크린샷에서 볼 수 있듯이 모든 옵션이 통과되어 성공했습니다.
친구! 프로젝트의 새 코드가 출시되면 즉시 알고 싶으십니까? 새 기사는 언제 나오나요? 내 텔레그램 채널에 가입하세요 . 그곳에서 나는 내 기사, 생각, 오픈 소스 개발을 함께 수집합니다.
프로젝트 버전을 0.6.0-SNAPSHOT으로 업데이트합니다. RELEASE_NOTES.md를 업데이트하고 새 버전에서 수행된 작업에 대한 설명을 추가합니다.
## 0.6.0-SNAPSHOT * JRTB-7: 그룹 구독 삭제 기능이 추가되었습니다.
코드가 작동하고 이에 대한 테스트가 작성되었습니다. 이제 작업을 저장소에 푸시하고 풀 요청을 생성할 시간입니다."A부터 Z까지의 Java 프로젝트": 그룹에서 기사 구독 제거 - 5

끝나는 대신

오랫동안 프로젝트 게시판을 살펴보지 않았는데 큰 변화가 있었습니다. 이제 "A부터 Z까지의 Java 프로젝트": 그룹에서 기사 구독 제거 - 6작업이 5개밖에 남지 않았습니다. 즉, 당신과 나는 이미 길의 끝에 와 있는 것입니다. 조금 남았습니다. 특히 이 시리즈의 기사가 9월 중순부터, 즉 7개월 동안 연재되고 있다는 점이 흥미롭습니다!!! 이 아이디어를 떠올렸을 때 이렇게 오랜 시간이 걸릴 거라고는 예상하지 못했습니다. 동시에 결과도 매우 만족스럽습니다! 친구 여러분, 기사에서 무슨 일이 일어나고 있는지 확실하지 않다면 댓글로 질문하세요. 이렇게 하면 어떤 것은 더 잘 설명해야 하고 어떤 것은 추가 설명이 필요하다는 것을 알게 될 것입니다. 음, 평소처럼 - 구독 - 벨을 울리고, 프로젝트에 별점을 주고 , 댓글을 쓰고, 기사를 평가해 주세요! 모두에게 감사드립니다. 다음 부분까지. /stop/start 명령을 통해 봇 비활성화 및 활성화를 추가하는 방법 과 이를 가장 잘 사용하는 방법에 대해 곧 설명하겠습니다 . 나중에 봐요!

시리즈의 모든 자료 목록은 이 기사의 시작 부분에 있습니다.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION