JavaRush /Java Blog /Random-JA /「Java プロジェクト A to Z」グループから記事の購読を削除します。
Roman Beekeeper
レベル 35

「Java プロジェクト A to Z」グループから記事の購読を削除します。

Random-JA グループに公開済み
親愛なる友人、将来のシニア ソフトウェア エンジニアの皆さん、こんにちは。私たちは電報ボットの開発を続けています。プロジェクトのこのステップでは、プログラムの価値よりも目に見える価値がある 3 つのタスクを検討します。特定のグループから新しい記事の購読を削除する方法を学習する必要があります。ボットを非アクティブ化するには/stopコマンドを使用し、アクティブ化するには/startコマンドを使用します。さらに、すべてのリクエストと更新は、ボットのアクティブなユーザーのみに関係します。いつものように、メインブランチを更新してすべての変更を取得し、新しいブランチ STEP_7_JRTB-7 を作成します。このパートでは、サブスクリプションの削除について説明し、イベントの 5 つのオプションを検討します。これは興味深いものになるでしょう。

JRTB-7: グループから新しい記事の購読を削除する

すべてのユーザーが、新しい記事に関する通知を受け取らないように購読を削除できることを望んでいることは明らかです。そのロジックは、サブスクリプションを追加するロジックと非常に似ています。コマンドを 1 つだけ送信すると、その応答として、ユーザーがすでに登録しているグループとその 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 エンティティを操作するための 2 つのメソッドを追加する必要がありました。ID によるデータベースからの取得と、エンティティ自体の保存です。これらのメソッドはすべて、既製のリポジトリ メソッドを呼び出すだけです。サブスクリプションの削除については、別途説明します。データベース スキーマでは、これは多対多のプロセスを担当するテーブルであり、この関係を削除するには、その中のレコードを削除する必要があります。これは、データベース側の一般的な理解を使用した場合です。ただし、Spring Data を使用しており、デフォルトで Hibernate があり、これを別の方法で行うことができます。GroupSub エンティティを取得します。これに関連付けられたすべてのユーザーがそこに描画されます。このユーザーのコレクションから必要なユーザーを削除し、このユーザーを除いて groupSub をデータベースに保存し直します。このようにして、Spring Data は私たちが望んでいることを理解し、レコードを削除します。「Java プロジェクトの A から Z まで」: グループから記事へのサブスクリプションを削除 - 1「Java プロジェクトの A から Z まで」: グループからの記事への購読の削除 - 2ユーザーをすばやく削除するために、問題が起こらないように GroupSub リストを除外して、TelegramUser に EqualsAndHashCode アノテーションを追加しました。そして、必要なユーザーを含むユーザーのコレクションに対して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);
   }
}
ここで、各テストは個別のシナリオをチェックしますが、そのうちの 5 つだけがあることを思い出してください。
  • /deleteGroupSubコマンドを渡しただけで、グループ サブスクリプションがない場合。
  • /deleteGroupSubコマンドを渡しただけで、グループへのサブスクリプションがある場合。
  • 無効なグループ ID が渡された場合 (例: /deleteGroupSub abc )
  • 期待どおり、すべてが正しく削除されるシナリオ。
  • これは、グループ ID は有効ですが、そのようなグループがデータベースに存在しない場合のシナリオです。
ご覧のとおり、これらのシナリオはすべてテストでカバーする必要があります。書いているうちに、より良いテストを書くためには、いくつかのテストコースを受講する価値があることに気付きました。これは、さまざまなオプションを適切に探すのに役立つと思います。そう、未来への想い。次に、サブスクリプションを削除できるようになったという説明を/helpコマンドに追加する必要があります。これをサブスクリプションを操作するセクションに配置しましょう。「Java プロジェクトの A から Z まで」: グループからの記事へのサブスクリプションの削除 - 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 プロジェクトの A から Z まで」: グループからの記事への購読の削除 - 4スクリーンショットからわかるように、すべてのオプションが実行され、成功しました。
友達!プロジェクトの新しいコードがリリースされるとすぐに知りたいですか? 新しい記事はいつ公開されますか? 私のTelegram チャンネルに参加してください。そこでは、私の記事、考え、オープンソース開発をまとめています。
プロジェクトのバージョンを 0.6.0-SNAPSHOT に更新します。 RELEASE_NOTES.md を更新し、新しいバージョンで行われた内容の説明を追加します。
## 0.6.0-SNAPSHOT * JRTB-7: グループ サブスクリプションを削除する機能を追加しました。
コードは機能し、そのためのテストが作成されています。タスクをリポジトリにプッシュしてプル リクエストを作成します。「Java プロジェクトの A から Z まで」: グループからの記事へのサブスクリプションの削除 - 5

終わる代わりに

長い間プロジェクト ボードを見ていませんでしたが、大きな変化がありました。「Java プロジェクトの A から Z まで」: グループからの記事への購読の削除 - 6残っているタスクは 5 つだけです。つまり、あなたも私もすでに道の終点にいるのです。少し残った。特に興味深いのは、この一連の記事が 9 月中旬から、つまり 7 か月間連載されているということです。このアイデアを思いついたとき、これほど長い時間がかかるとは予想していませんでした。同時に、その結​​果にはとても満足しています! 記事の中で何が起こっているのか不明な場合は、コメント欄で質問してください。こうすることで、何かをもっと詳しく説明する必要があることや、さらに説明する必要があることがわかります。 さて、いつものように、購読、ベルを鳴らし、プロジェクトに星を付け、コメントを書き、記事を評価してください。 ありがとうございます。次のパートまで。/stopおよび/startコマンドを使用してボットの非アクティブ化とアクティブ化を追加する方法と、それらを最適に使用する方法については、すぐに説明します。また後で!

シリーズのすべてのマテリアルのリストは、この記事の冒頭にあります。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION