JavaRush /Blog Java /Random-PL /Usuwamy subskrypcję artykułów z grupy - „Projekt Java od ...
Roman Beekeeper
Poziom 35

Usuwamy subskrypcję artykułów z grupy - „Projekt Java od A do Z”

Opublikowano w grupie Random-PL
Witam wszystkich, drodzy przyjaciele, przyszli Starsi Inżynierowie Oprogramowania. Kontynuujemy rozwój bota telegramowego. Na tym etapie naszego projektu przyjrzymy się trzem zadaniom, które mają bardziej widoczną wartość niż wartość oprogramowania. Musimy dowiedzieć się, jak usunąć subskrypcję nowych artykułów z określonej grupy: użyj polecenia /stop , aby dezaktywować bota, a poleceniem /start, aby go aktywować. I tak, aby wszystkie żądania i aktualizacje dotyczyły tylko aktywnych użytkowników bota. Jak zwykle zaktualizujemy główną gałąź, aby pobrać wszystkie zmiany i utworzymy nową: STEP_7_JRTB-7. W tej części porozmawiamy o usunięciu subskrypcji i rozważymy 5 opcji wydarzeń - będzie ciekawie.

JRTB-7: usuwanie subskrypcji nowych artykułów z grupy

Oczywiste jest, że wszyscy użytkownicy będą chcieli mieć możliwość usunięcia swojej subskrypcji, aby nie otrzymywać powiadomień o nowych artykułach. Jego logika będzie bardzo podobna do logiki dodawania abonamentu. Jeśli wyślemy tylko jedno polecenie, w odpowiedzi otrzymamy listę grup i ich identyfikatory, do których użytkownik jest już zapisany, abyśmy mogli zrozumieć, co dokładnie należy usunąć. A jeśli użytkownik wyśle ​​ID grupy wraz z zespołem, usuniemy subskrypcję. Dlatego rozwińmy to polecenie od strony bota telegramu.
  1. Dodajmy nazwę nowego polecenia - /deleteGroupSub , a w CommandName - linię:

    DELETE_GROUP_SUB("/deleteGroupSub")

  2. Następnie utwórzmy polecenie 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 = "Пока нет подписок на группы. Coбы добавить подписку напиши /addGroupSub";
           } else {
               message = "Coбы удалить подписку на группу - передай комадну вместе с ID группы. \n" +
                       "Например: /deleteGroupSub 16 \n\n" +
                       "я подготовил список всех групп, на которые ты подписан) \n\n" +
                       "Nazwa группы - 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));
       }
    }

Aby to zrobić musieliśmy dodać jeszcze dwie metody pracy z encją GroupSub - pobieranie z bazy po ID i zapisywanie samej encji. Wszystkie te metody po prostu wywołują gotowe metody repozytorium. O usunięciu subskrypcji opowiem Ci osobno. W schemacie bazy danych jest to tabela odpowiedzialna za proces wiele do wielu i aby usunąć tę relację należy usunąć znajdujący się w niej rekord. Dzieje się tak, jeśli korzystamy z ogólnego zrozumienia ze strony bazy danych. Ale używamy Spring Data i domyślnie jest Hibernacja, która może to zrobić inaczej. Otrzymujemy encję GroupSub, do której zostaną przeciągnięci wszyscy związani z nią użytkownicy. Z tej kolekcji użytkowników usuniemy tego, którego potrzebujemy i zapiszemy groupSub z powrotem w bazie danych, ale bez tego użytkownika. W ten sposób Spring Data zrozumie, czego chcieliśmy i usunie rekord. „Projekt Java od A do Z”: Usuwanie subskrypcji artykułów z grupy - 1„Projekt Java od A do Z”: Usuwanie subskrypcji artykułów z grupy - 2Aby szybko usunąć użytkownika, dodałem adnotację EqualsAndHashCode do TelegramUser, wyłączając listę GroupSub, aby nie było problemów. I wywołał metodę usuwania w przypadku zbierania użytkowników z użytkownikiem, którego potrzebujemy. Tak to wygląda dla 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;
}
Dzięki temu wszystko poszło tak jak chcieliśmy. Możliwych zdarzeń w zespole jest kilka, dlatego napisanie dobrego testu dla każdego z nich jest świetnym pomysłem. A skoro mowa o testach: podczas ich pisania znalazłem błąd w logice i poprawiłem go zanim trafił do produkcji. Gdyby nie przeprowadzono testu, nie jest jasne, jak szybko zostałby wykryty. Usuń grupęSubCommandTest:
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 = "Пока нет подписок на группы. Coбы добавить подписку напиши /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 = "Coбы удалить подписку на группу - передай комадну вместе с ID группы. \n" +
               "Например: /deleteGroupSub 16 \n\n" +
               "я подготовил список всех групп, на которые ты подписан) \n\n" +
               "Nazwa группы - 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);
   }
}
Tutaj każdy test sprawdza osobny scenariusz, a przypomnę, że jest ich tylko pięć:
  • kiedy po prostu przekazałeś polecenie /deleteGroupSub i nie ma żadnych subskrypcji grupowych;
  • kiedy po prostu przekazałeś polecenie /deleteGroupSub i istnieją subskrypcje grup;
  • gdy przekazany został nieprawidłowy identyfikator grupy, na przykład /deleteGroupSub abc ;
  • scenariusz, w którym wszystko zostanie poprawnie usunięte, zgodnie z oczekiwaniami;
  • scenariusz, gdy identyfikator grupy jest ważny, ale takiej grupy nie ma w bazie.
Jak widać wszystkie te scenariusze trzeba poprzeć testami. Podczas pisania zdałem sobie sprawę, że aby pisać lepsze testy, warto wziąć udział w kursach testerskich. Myślę, że pomoże to we właściwym szukaniu różnych opcji. Zgadza się, przemyślenia na przyszłość. Następnie musisz dodać opis do polecenia /help , że możesz teraz usunąć subskrypcję. Umieśćmy go w sekcji poświęconej pracy z subskrypcjami. „Projekt Java od A do Z”: Usuwanie subskrypcji artykułów z grupy - 3Oczywiście, aby to polecenie zadziałało, musisz dodać jego inicjalizację do CommandContainer :
.put(DELETE_GROUP_SUB.getCommandName(),
       new DeleteGroupSubCommand(sendBotMessageService, groupSubService, telegramUserService))
Teraz możesz przetestować funkcjonalność na bocie testowym. Uruchamiamy naszą bazę danych za pomocą docker-compose-test.yml: docker-compose -f docker-compose-test.yml up I uruchamiamy SpringBoot poprzez IDEA. Całkowicie wyczyszczę korespondencję z botem i zacznę od nowa. Przeanalizuję wszystkie opcje, które mogą się pojawić podczas pracy z tym zespołem. „Projekt Java od A do Z”: Usuwanie subskrypcji artykułów z grupy - 4Jak widać na zrzucie ekranu, wszystkie opcje zostały pomyślnie zrealizowane.
Przyjaciele! Chcesz wiedzieć od razu, kiedy zostanie wydany nowy kod projektu? Kiedy pojawi się nowy artykuł? Dołącz do mojego kanału na Telegramie . Gromadzę tam swoje artykuły, przemyślenia i rozwój open source.
Aktualizujemy wersję naszego projektu do 0.6.0-SNAPSHOT Aktualizujemy RELEASE_NOTES.md, dodając opis co zostało zrobione w nowej wersji:
## 0.6.0-SNAPSHOT * JRTB-7: dodano możliwość usuwania subskrypcji grupowej.
Kod działa, napisano dla niego testy: czas wypchnąć zadanie do repozytorium i utworzyć pull request.„Projekt Java od A do Z”: Usuwanie subskrypcji artykułów z grupy - 5

Zamiast kończyć

Długo nie zaglądaliśmy na naszą tablicę projektową, ale zaszły duże zmiany: „Projekt Java od A do Z”: Usuwanie subskrypcji artykułów z grupy - 6Zostało już tylko 5 zadań. Oznacza to, że ty i ja jesteśmy już na samym końcu drogi. Zostawiłem trochę. Co szczególnie ciekawe, cykl artykułów trwa od połowy września, czyli już 7 miesięcy!!! Kiedy wpadłem na ten pomysł, nie spodziewałem się, że zajmie to tak dużo czasu. Jednocześnie jestem więcej niż zadowolony z wyniku! Przyjaciele, jeśli nie jest jasne, co dzieje się w artykule, zadawajcie pytania w komentarzach. Dzięki temu będę wiedział, że coś trzeba lepiej opisać, a coś wymaga doprecyzowania. No cóż, jak zwykle lajkuj - subskrybuj - zadzwoń, daj gwiazdkę naszemu projektowi , napisz komentarz i oceń artykuł! Dziękuje za wszystko. Do następnej części. Wkrótce porozmawiamy o tym, jak dodać dezaktywację i aktywację bota za pomocą poleceń /stop i /start oraz jak najlepiej z nich korzystać. Do zobaczenia później!

Lista wszystkich materiałów wchodzących w skład serii znajduje się na początku artykułu.

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