JavaRush /مدونة جافا /Random-AR /نقوم بإزالة الاشتراك في المقالات من المجموعة - "مشروع Jav...
Roman Beekeeper
مستوى

نقوم بإزالة الاشتراك في المقالات من المجموعة - "مشروع Java من الألف إلى الياء"

نشرت في المجموعة
مرحبًا بالجميع، أصدقائي الأعزاء، كبار مهندسي البرمجيات في المستقبل. نواصل تطوير روبوت برقية. في هذه الخطوة من مشروعنا، سننظر في ثلاث مهام لها قيمة أكثر وضوحًا من قيمة البرنامج. نحتاج إلى معرفة كيفية إزالة الاشتراك في مقالات جديدة من مجموعة معينة: استخدم الأمر /stop لإلغاء تنشيط الروبوت، واستخدم الأمر /start لتنشيطه. علاوة على ذلك، فإن جميع الطلبات والتحديثات تتعلق فقط بالمستخدمين النشطين للروبوت. كالعادة، سنقوم بتحديث الفرع الرئيسي للحصول على كافة التغييرات وإنشاء فرع جديد: STEP_7_JRTB-7. في هذا الجزء، سنتحدث عن حذف الاشتراك وننظر في 5 خيارات للأحداث - سيكون الأمر مثيرًا للاهتمام.

JRTB-7: إزالة الاشتراك في مقالات جديدة من المجموعة

من الواضح أن جميع المستخدمين سيرغبون في أن يتمكنوا من حذف اشتراكهم حتى لا يتلقوا إشعارات حول المقالات الجديدة. سيكون منطقه مشابهًا جدًا لمنطق إضافة الاشتراك. إذا أرسلنا أمرًا واحدًا فقط، فسنتلقى ردًا على ذلك قائمة بالمجموعات ومعرفاتها التي اشترك فيها المستخدم بالفعل، حتى نتمكن من فهم ما يجب حذفه بالضبط. وإذا أرسل المستخدم معرف المجموعة مع الفريق، فسنحذف الاشتراك. لذلك، دعنا نطور هذا الأمر من جانب روبوت التليجرام.
  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 - الاسترجاع من قاعدة البيانات عن طريق المعرف وحفظ الكيان نفسه. كل هذه الطرق تستدعي ببساطة طرق المستودع الجاهزة. سأخبرك بشكل منفصل عن حذف الاشتراك. في مخطط قاعدة البيانات، هذا هو الجدول المسؤول عن عملية متعدد إلى متعدد، ولحذف هذه العلاقة، تحتاج إلى حذف السجل الموجود فيه. هذا إذا استخدمنا الفهم العام من جانب قاعدة البيانات. لكننا نستخدم Spring Data ويوجد Hibernate افتراضيًا، والذي يمكنه القيام بذلك بشكل مختلف. نحصل على كيان GroupSub، الذي سيتم جذب جميع المستخدمين المرتبطين به. من هذه المجموعة من المستخدمين، سنزيل المستخدم الذي نحتاجه ونحفظ groupSub مرة أخرى في قاعدة البيانات، ولكن بدون هذا المستخدم. بهذه الطريقة سوف تفهم Spring Data ما أردناه وتحذف السجل. "مشروع Java من الألف إلى الياء": إزالة الاشتراك في المقالات من المجموعة - 1"مشروع Java من الألف إلى الياء": إزالة الاشتراك في المقالات من المجموعة - 2لإزالة مستخدم بسرعة، أضفت التعليق التوضيحي EqualsAndHashCode لـ TelegramUser، باستثناء قائمة GroupSub حتى لا تكون هناك مشاكل. وتسمى طريقة الإزالة على جمع المستخدمين مع المستخدم الذي نحتاجه. هذا ما يبدو عليه مستخدم Telegram:
@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;
}
ونتيجة لذلك، سار كل شيء كما أردنا. هناك العديد من الأحداث المحتملة في الفريق، لذا فإن كتابة اختبار جيد لكل منها يعد فكرة رائعة. بالحديث عن الاختبارات: أثناء كتابتي لها، وجدت خللًا في المنطق وقمت بتصحيحه قبل طرحه في الإنتاج. ولو لم يكن هناك اختبار، فمن غير الواضح مدى سرعة اكتشافه. حذف اختبار GroupSubCommandTest:
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 وتكون هناك اشتراكات في المجموعات؛
  • عند تمرير معرف مجموعة غير صالح، على سبيل المثال، /deleteGroupSub abc ؛
  • السيناريو الذي يتم فيه حذف كل شيء بشكل صحيح، كما هو متوقع؛
  • سيناريو عندما يكون معرف المجموعة صالحًا، ولكن هذه المجموعة غير موجودة في قاعدة البيانات.
كما ترون، كل هذه السيناريوهات تحتاج إلى أن تكون مغطاة بالاختبارات. بينما كنت أكتب، أدركت أنه من أجل كتابة اختبارات أفضل، فإن الأمر يستحق أخذ بعض الدورات التدريبية للاختبار. أعتقد أن هذا سيساعد في البحث بشكل صحيح عن خيارات مختلفة. هذا صحيح، أفكار للمستقبل. بعد ذلك، تحتاج إلى إضافة وصف إلى الأمر /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 وقم بتشغيل SpringBoot عبر IDEA. سأقوم بمسح المراسلات مع الروبوت تمامًا والبدء من جديد. سأستعرض جميع الخيارات التي قد تنشأ عند العمل مع هذا الفريق. "مشروع Java من الألف إلى الياء": إزالة الاشتراك في المقالات من المجموعة - 4كما ترون من لقطة الشاشة، تم تنفيذ جميع الخيارات وكانت ناجحة.
أصدقاء! هل تريد أن تعرف فورًا متى سيتم إصدار الكود الجديد للمشروع؟ متى يخرج مقال جديد؟ انضم إلى قناتي على التليجرام . هناك أقوم بجمع مقالاتي وأفكاري وتطوير المصادر المفتوحة معًا.
نقوم بتحديث إصدار مشروعنا إلى 0.6.0-SNAPSHOT نقوم بتحديث RELEASE_NOTES.md، مع إضافة وصف لما تم إنجازه في الإصدار الجديد:
## 0.6.0-SNAPSHOT * JRTB-7: إضافة القدرة على حذف اشتراك المجموعة.
الكود يعمل، وتمت كتابة الاختبارات له: حان الوقت لدفع المهمة إلى المستودع وإنشاء طلب سحب."مشروع Java من الألف إلى الياء": إزالة الاشتراك في المقالات من المجموعة - 5

بدلا من الانتهاء

لم ننظر إلى لوحة مشروعنا لفترة طويلة، ولكن كانت هناك تغييرات كبيرة: "مشروع Java من الألف إلى الياء": إزالة الاشتراك في المقالات من المجموعة - 6لم يتبق سوى 5 مهام. وهذا يعني أننا وصلنا بالفعل إلى نهاية الطريق. غادر قليلا. ومن المثير للاهتمام بشكل خاص ملاحظة أن هذه السلسلة من المقالات تم نشرها منذ منتصف سبتمبر، أي لمدة 7 أشهر!!! عندما خطرت ببالي هذه الفكرة، لم أتوقع أن تستغرق وقتًا طويلاً. وفي الوقت نفسه، أنا أكثر من مسرور بالنتيجة! أيها الأصدقاء، إذا لم يكن من الواضح ما يحدث في المقال، اطرحوا الأسئلة في التعليقات. بهذه الطريقة سأعرف أن شيئًا ما يحتاج إلى وصف أفضل، وأن شيئًا يحتاج إلى مزيد من التوضيح. حسنًا، كالعادة، قم بالإعجاب - الاشتراك - دق الجرس، قم بتمييز مشروعنا بنجمة ، واكتب التعليقات وقم بتقييم المقالة! شكرا للجميع. حتى الجزء التالي. سنتحدث قريبًا عن كيفية إضافة إلغاء تنشيط الروبوت وتنشيطه عبر أوامر /stop & /start وكيفية استخدامها بشكل أفضل. أراك لاحقًا!

توجد قائمة بجميع المواد الموجودة في السلسلة في بداية هذه المقالة.

تعليقات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION