JavaRush /Java Blog /Random-TW /我們刪除了「Java 專案從 A 到 Z」群組中的文章訂閱
Roman Beekeeper
等級 35

我們刪除了「Java 專案從 A 到 Z」群組中的文章訂閱

在 Random-TW 群組發布
大家好,親愛的朋友們,未來的資深軟體工程師。我們繼續開發電報機器人。在我們專案的這一步中,我們將研究比軟體價值更明顯的價值的三個任務。我們需要了解如何從特定群組中刪除新文章的訂閱:使用/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就會理解我們想要什麼並刪除記錄。《Java 專案從頭到尾》:刪除群組中文章的訂閱 - 1《Java 專案從頭到尾》:刪除群組中文章的訂閱 - 2為了快速刪除用戶,我為 TelegramUser 添加了 EqualsAndHashCode 註釋,排除了 GroupSub 列表,這樣就不會出現問題。並在用戶集合上呼叫了我們需要的用戶的remove方法。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);
   }
}
在這裡,每個測試都會檢查一個單獨的場景,讓我提醒您,只有其中五個:
  • 當您只是傳遞/deleteGroupSub命令並且沒有群組訂閱時;
  • 當您簡單地傳遞/deleteGroupSub命令並且有群組訂閱時;
  • 當傳遞無效群組 ID 時,例如/deleteGroupSub abc
  • 正如預期的那樣,所有內容都被正確刪除的場景;
  • 組 ID 有效,但資料庫中不存在這樣的組的情況。
正如您所看到的,所有這些場景都需要透過測試來覆蓋。當我寫作時,我意識到為了編寫更好的測試,值得參加一些測試課程。我認為這將有助於正確尋找不同的選擇。沒錯,就是對未來的想法。接下來,您需要向/help命令新增說明,表明您現在可以刪除訂閱。讓我們將其放置在用於處理訂閱的部分中。《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。我將徹底清除與機器人的通訊並重新開始。我將詳細介紹與該團隊合作時可能出現的所有選項。《Java 專案從頭到尾》:刪除群組中文章的訂閱 - 4從螢幕截圖中可以看出,所有選項均已通過並且成功。
朋友們!您想立即知道專案的新程式碼何時發布嗎?新文章什麼時候出來?加入我的Telegram 頻道。我在那裡收集我的文章、想法和開源開發。
我們將專案的版本更新為 0.6.0-SNAPSHOT 我們更新 RELEASE_NOTES.md,新增版本中所做操作的描述:
## 0.6.0-SNAPSHOT * JRTB-7:新增了刪除群組訂閱的功能。
程式碼可以工作,已經為其編寫了測試:是時候將任務推送到儲存庫並建立拉取請求了。《Java 專案從頭到尾》:刪除群組中文章的訂閱 - 5

而不是結束

我們已經很久沒有查看我們的專案板了,但是已經發生了很大的變化:《Java 專案從頭到尾》:從群組中刪除文章訂閱 - 6只剩下 5 個任務了。也就是說,你我已經走到了路的盡頭。留下了一點。特別有趣的是,這個系列文章從9月中旬開始,也就是7個月了!!!當我想到這個想法時,我沒想到會花這麼久。同時,我對結果非常滿意!朋友們,如果不清楚文章中發生的事情,請在評論中提問。這樣我就會知道有些東西需要更好地描述,有些東西需要進一步解釋。 好吧,像往常一樣,喜歡 - 訂閱 - 按響鈴,我們的項目一顆星,寫評論並評論文章! 謝謝大家。直到下一部分。我們很快就會討論如何透過/stop/start命令添加機器人停用和啟動以及如何最好地使用它們。回頭見!

此系列所有資料的清單位於本文開頭。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION