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

نضيف كل ما يتعلق بقاعدة البيانات. (الجزء الثاني) - "مشروع جافا من الألف إلى الياء"

نشرت في المجموعة
أهلاً بكم. اسمحوا لي أن أذكركم: في الجزء الأول أضفنا Flyway. فلنكمل.

إضافة قاعدة بيانات إلى docker-compose.yml

المرحلة التالية هي إعداد العمل مع قاعدة البيانات في ملف docker-compose.yml الرئيسي. لنضيف قاعدة البيانات إلى ملف إنشاء عامل الإرساء:
version: '3.1'

services:
 jrtb-bot:
   depends_on:
     - jrtb-db
   build:
     context: .
   environment:
     BOT_NAME: ${BOT_NAME}
     BOT_TOKEN: ${BOT_TOKEN}
     BOT_DB_USERNAME: ${BOT_DB_USERNAME}
     BOT_DB_PASSWORD: ${BOT_DB_PASSWORD}
   restart: always
 jrtb-db:
   image: mysql:5.7
   restart: always
   environment:
     MYSQL_USER: ${BOT_DB_USERNAME}
     MYSQL_PASSWORD: ${BOT_DB_PASSWORD}
     MYSQL_DATABASE: 'jrtb_db'
     MYSQL_ROOT_PASSWORD: 'root'
   ports:
     - '3306:3306'
   expose:
     - '3306'
أضفت أيضًا هذا السطر إلى تطبيقنا:
depends_on:
 - jrtb-db
هذا يعني أننا ننتظر حتى تبدأ قاعدة البيانات قبل بدء التطبيق. بعد ذلك، يمكنك ملاحظة إضافة متغيرين آخرين نحتاجهما للعمل مع قاعدة البيانات:
${BOT_DB_USERNAME}
${BOT_DB_PASSWORD}
سنحصل عليها في docker-compose بنفس الطريقة المتبعة مع روبوت telegram - من خلال متغيرات البيئة. لقد فعلت ذلك بحيث يكون لدينا مكان واحد فقط نقوم فيه بتعيين قيم اسم مستخدم قاعدة البيانات وكلمة المرور الخاصة بها. نقوم بتمريرها إلى صورة عامل الإرساء لتطبيقنا وإلى حاوية عامل الإرساء في قاعدة بياناتنا. بعد ذلك، نحتاج إلى تحديث ملف Dockerfile لتعليم SpringBoot الخاص بنا قبول المتغيرات في قاعدة البيانات.
FROM adoptopenjdk/openjdk11:ubi
ARG JAR_FILE=target/*.jar
ENV BOT_NAME=test.javarush_community_bot
ENV BOT_TOKEN=1375780501:AAE4A6Rz0BSnIGzeu896OjQnjzsMEG6_uso
ENV BOT_DB_USERNAME=jrtb_db_user
ENV BOT_DB_PASSWORD=jrtb_db_password
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-Dspring.datasource.password=${BOT_DB_PASSWORD}", "-Dbot.username=${BOT_NAME}", "-Dbot.token=${BOT_TOKEN}", "-Dspring.datasource.username=${BOT_DB_USERNAME}", "-jar", "app.jar"]
نضيف الآن متغيرات قاعدة البيانات إلى ملف Dockerfile:
ENV BOT_DB_USERNAME=jrtb_db_user
ENV BOT_DB_PASSWORD=jrtb_db_password
القيم المتغيرة ستكون مختلفة. ومع ذلك، فإن تلك التي سنمررها إلى ملف Dockerfile تتطلب قيمًا افتراضية، لذلك أدخلت بعضًا منها. نقوم بتوسيع السطر الأخير بعنصرين، بمساعدتهما سنمرر اسم مستخدم قاعدة البيانات وكلمة المرور لبدء تشغيل التطبيق:
"-Dspring.datasource.password=${BOT_DB_PASSWORD}", "-Dbot.username=${BOT_NAME}"
يجب أن يكون السطر الأخير في ملف Dockerfile (الذي يبدأ بـ ENTRYPOINT) بدون تغليف. إذا قمت بإجراء تحويل، فلن يعمل هذا الرمز. الخطوة الأخيرة هي تحديث ملف start.sh لتمرير المتغيرات إلى قاعدة البيانات.
#!/bin/bash

# Pull new changes
git pull

# Prepare Jar
mvn clean
mvn package

# Ensure, that docker-compose stopped
docker-compose stop

# Add environment variables
export BOT_NAME=$1
export BOT_TOKEN=$2
export BOT_DB_USERNAME='prod_jrtb_db_user'
export BOT_DB_PASSWORD='Pap9L9VVUkNYj99GCUCC3mJkb'

# Start new deployment
docker-compose up --build -d
نحن نعرف بالفعل كيفية إضافة متغيرات البيئة قبل تشغيل docker-compose. للقيام بذلك، تحتاج فقط إلى تنفيذ التصدير var_name=var_value. ولذلك، نضيف سطرين فقط:
export BOT_DB_USERNAME='prod_jrtb_db_user'
export BOT_DB_PASSWORD='Pap9L9VVUkNYj99GCUCC3mJkb'
هذا هو المكان الذي قمنا فيه بتعيين اسم المستخدم وكلمة المرور لقاعدة البيانات. بالطبع، سيكون من الممكن تمرير هذه المتغيرات عند تشغيل البرنامج النصي bash، كما نفعل مع اسم الروبوت ورمزه المميز. ولكن يبدو لي أن هذا غير ضروري. للوصول فعليًا إلى قاعدة البيانات، تحتاج إلى معرفة عنوان IP الخاص بالخادم الذي سيتم نشر قاعدة البيانات عليه، وأن تكون مدرجًا في قائمة عناوين IP المسموح بها للطلب. أما بالنسبة لي، فهذا يكفي بالفعل. لقد تم وضع الأساس: الآن يمكنك القيام بأشياء أكثر قابلية للفهم بالنسبة للمطور - كتابة التعليمات البرمجية. قبل ذلك، كنا نفعل ما يفعله مهندسو DevOps - تهيئة البيئة.

إضافة طبقة المستودع

عادةً ما يتكون التطبيق من ثلاث طبقات:
  1. وحدات التحكم هي نقاط الدخول إلى التطبيق.
  2. الخدمات هي المكان الذي يعمل فيه منطق الأعمال. لدينا بالفعل هذا جزئيًا: SendMessageService هو ممثل صريح لمنطق الأعمال.
  3. المستودعات هي مكان للعمل مع قاعدة البيانات. في حالتنا، هذا هو بوت برقية.
الآن سنضيف الطبقة الثالثة - المستودعات. سنستخدم هنا مشروعًا من نظام Spring البيئي - Spring Data. يمكنك أن تقرأ عن ماهيته في هذه المقالة على حبري . علينا أن نعرف ونفهم عدة نقاط:
  1. لن نضطر إلى العمل مع JDBC: سنعمل مباشرة مع تجريدات أعلى. أي تخزين POJOs التي تتوافق مع الجداول في قاعدة البيانات. سوف نسمي هذه الفئات كيانًا ، كما يتم استدعاؤها رسميًا في Java Persistence API (هذه مجموعة شائعة من الواجهات للعمل مع قاعدة بيانات من خلال ORM، أي تجريد للعمل مع JDBC). سيكون لدينا فئة كيان سنحفظها في قاعدة البيانات، وسيتم كتابتها في الجدول الذي نحتاجه بالضبط. سوف نتلقى نفس الكائنات عند البحث في قاعدة البيانات.
  2. تعرض Spring Data استخدام مجموعة الواجهات الخاصة بها: JpaRepository و CrudRepository وما إلى ذلك... هناك واجهات أخرى: يمكن العثور على القائمة الكاملة هنا . الجميل في الأمر هو أنه يمكنك استخدام أساليبهم دون تنفيذها (!). علاوة على ذلك، هناك قالب معين يمكنك من خلاله كتابة أساليب جديدة في الواجهة، وسيتم تنفيذها تلقائيًا.
  3. الربيع يبسط تطورنا قدر الإمكان. للقيام بذلك، نحتاج إلى إنشاء واجهة خاصة بنا ونرث تلك المذكورة أعلاه. ولكي يعرف Spring أنه يحتاج إلى استخدام هذه الواجهة، أضف تعليقًا توضيحيًا للمستودع.
  4. إذا كنا بحاجة إلى كتابة طريقة للعمل مع قاعدة بيانات غير موجودة، فهذه ليست مشكلة أيضا - سنكتبها. سأريكم ماذا وكيف تفعلون هناك.
في هذه المقالة، سنعمل على إضافة مسار TelegramUser بالكامل وإظهار هذا الجزء كمثال. وسوف نقوم بتوسيع الباقي في مهام أخرى. أي أنه عندما نقوم بتنفيذ الأمر /start، سنكتب active = true في قاعدة بيانات المستخدم الخاص بنا. وهذا يعني أن المستخدم يستخدم الروبوت. إذا كان المستخدم موجودًا بالفعل في قاعدة البيانات، فسنقوم بتحديث الحقل active = true. عند تنفيذ الأمر /stop، لن نقوم بحذف المستخدم، ولكننا سنقوم فقط بتحديث الحقل النشط إلى خطأ، بحيث إذا أراد المستخدم استخدام الروبوت مرة أخرى، فيمكنه تشغيله والمتابعة من حيث توقف. ولذا، عند الاختبار، يمكننا أن نرى أن شيئًا ما يحدث، سنقوم بإنشاء أمر /stat: سيعرض عدد المستخدمين النشطين. نقوم بإنشاء حزمة مستودع بجوار حزم الروبوت والأوامر والخدمات. في هذه الحزمة نقوم بإنشاء كيان واحد آخر . في حزمة الكيان نقوم بإنشاء فئة TelegramUser:
package com.github.javarushcommunity.jrtb.repository.entity;

import lombok.Data;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;

/**
* Telegram User entity.
*/
@Data
@Entity
@Table(name = "tg_user")
public class TelegramUser {

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

   @Column(name = "active")
   private boolean active;
}
هنا يمكنك أن ترى أن لدينا جميع التعليقات التوضيحية من الحزمة javax.persistence. هذه هي التعليقات التوضيحية الشائعة التي يتم استخدامها لجميع تطبيقات ORM. بشكل افتراضي، يستخدم Spring Data Jpa وضع Hibernate، على الرغم من إمكانية استخدام تطبيقات أخرى. فيما يلي قائمة بالتعليقات التوضيحية التي نستخدمها:
  • الكيان - يشير إلى أن هذا كيان للعمل مع قاعدة البيانات؛
  • الجدول - هنا نحدد اسم الجدول؛
  • المعرف - يشير التعليق التوضيحي إلى الحقل الذي سيكون المفتاح الأساسي في الجدول؛
  • العمود - حدد اسم الحقل من الجدول.
بعد ذلك، نقوم بإنشاء واجهة للعمل مع قاعدة البيانات. عادةً ما تتم كتابة أسماء هذه الواجهات باستخدام القالب - EntiryNameRepository. سيكون لدينا مستودع Telegramuser:
package com.github.javarushcommunity.jrtb.repository;

import com.github.javarushcommunity.jrtb.repository.entity.TelegramUser;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
* {@link Repository} for handling with {@link TelegramUser} entity.
*/
@Repository
public interface TelegramUserRepository extends JpaRepository<TelegramUser, String> {
   List<TelegramUser> findAllByActiveTrue();
}
هنا يمكنك أن ترى كيف أضفت طريقة findAllByActiveTrue() ، والتي لا أقوم بتنفيذها في أي مكان. لكن ذلك لن يمنعه من العمل. سوف تفهم Spring Data أنها تحتاج إلى الحصول على كافة السجلات من جدول tg_user الذي يكون حقله النشط = true . نضيف خدمة للعمل مع كيان TelegramUser (نستخدم انعكاس التبعية من SOLID في سياق أن خدمات الكيانات الأخرى لا يمكنها الاتصال مباشرة بمستودع كيان آخر - فقط من خلال خدمة ذلك الكيان). قمنا بإنشاء خدمة TelegramUserService في الحزمة، والتي سيكون لها في الوقت الحالي عدة طرق: حفظ المستخدم، والحصول على المستخدم من خلال معرفه وعرض قائمة المستخدمين النشطين. أولاً نقوم بإنشاء واجهة TelegramUserService:
package com.github.javarushcommunity.jrtb.service;

import com.github.javarushcommunity.jrtb.repository.entity.TelegramUser;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

/**
* {@link Service} for handling {@link TelegramUser} entity.
*/
public interface TelegramUserService {

   /**
    * Save provided {@link TelegramUser} entity.
    *
    * @param  telegramUser provided telegram user.
    */
   void save(TelegramUser telegramUser);

   /**
    * Retrieve all active {@link TelegramUser}.
    *
    * @return the collection of the active {@link TelegramUser} objects.
    */
   List<TelegramUser> retrieveAllActiveUsers();

   /**
    * Find {@link TelegramUser} by chatId.
    *
    * @param chatId provided Chat ID
    * @return {@link TelegramUser} with provided chat ID or null otherwise.
    */
   Optional<TelegramUser> findByChatId(String chatId);
}
وفي الواقع، تنفيذ TelegramUserServiceImpl:
package com.github.javarushcommunity.jrtb.service;

import com.github.javarushcommunity.jrtb.repository.TelegramUserRepository;
import com.github.javarushcommunity.jrtb.repository.entity.TelegramUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

/**
* Implementation of {@link TelegramUserService}.
*/
@Service
public class TelegramUserServiceImpl implements TelegramUserService {

   private final TelegramUserRepository telegramUserRepository;

   @Autowired
   public TelegramUserServiceImpl(TelegramUserRepository telegramUserRepository) {
       this.telegramUserRepository = telegramUserRepository;
   }

   @Override
   public void save(TelegramUser telegramUser) {
       telegramUserRepository.save(telegramUser);
   }

   @Override
   public List<TelegramUser> retrieveAllActiveUsers() {
       return telegramUserRepository.findAllByActiveTrue();
   }

   @Override
   public Optional<TelegramUser> findByChatId(String chatId) {
       return telegramUserRepository.findById(chatId);
   }
}
تجدر الإشارة هنا إلى أننا نستخدم حقن التبعية (تقديم مثيل فئة) لكائن TelegramuserRepository باستخدام التعليق التوضيحي Autowired وعلى المُنشئ. يمكنك القيام بذلك لمتغير، ولكن هذا هو النهج الذي يوصي به فريق Spring Framework لنا.

إضافة إحصائيات للبوت

بعد ذلك، ستحتاج إلى تحديث أوامر /start و/stop. عند استخدام الأمر /start، فإنك تحتاج إلى حفظ المستخدم الجديد في قاعدة البيانات وتعيينه إلى active = true. وعندما يكون هناك /stop، قم بتحديث بيانات المستخدم: set active = false. دعونا نصلح فئة StartCommand :
package com.github.javarushcommunity.jrtb.command;

import com.github.javarushcommunity.jrtb.repository.entity.TelegramUser;
import com.github.javarushcommunity.jrtb.service.SendBotMessageService;
import com.github.javarushcommunity.jrtb.service.TelegramUserService;
import org.telegram.telegrambots.meta.api.objects.Update;

/**
* Start {@link Command}.
*/
public class StartCommand implements Command {

   private final SendBotMessageService sendBotMessageService;
   private final TelegramUserService telegramUserService;

   public final static String START_MESSAGE = "Привет. Я Javarush Telegram Bot. Я помогу тебе быть в курсе последних " +
           "статей тех авторов, котрые тебе интересны. Я еще маленький и только учусь.";

   public StartCommand(SendBotMessageService sendBotMessageService, TelegramUserService telegramUserService) {
       this.sendBotMessageService = sendBotMessageService;
       this.telegramUserService = telegramUserService;
   }

   @Override
   public void execute(Update update) {
       String chatId = update.getMessage().getChatId().toString();

       telegramUserService.findByChatId(chatId).ifPresentOrElse(
               user -> {
                   user.setActive(true);
                   telegramUserService.save(user);
               },
               () -> {
                   TelegramUser telegramUser = new TelegramUser();
                   telegramUser.setActive(true);
                   telegramUser.setChatId(chatId);
                   telegramUserService.save(telegramUser);
               });

       sendBotMessageService.sendMessage(chatId, START_MESSAGE);
   }
}
نقوم هنا أيضًا بتمرير كائن TelegramuserService إلى المنشئ، والذي من خلاله سنحفظ المستخدم الجديد. علاوة على ذلك، باستخدام المسرات الاختيارية في Java، يعمل المنطق التالي: إذا كان لدينا مستخدم في قاعدة البيانات، فإننا ببساطة نجعله نشطًا، وإذا لم يكن كذلك، فإننا ننشئ مستخدمًا نشطًا جديدًا. أمر الإيقاف:
package com.github.javarushcommunity.jrtb.command;

import com.github.javarushcommunity.jrtb.repository.entity.TelegramUser;
import com.github.javarushcommunity.jrtb.service.SendBotMessageService;
import com.github.javarushcommunity.jrtb.service.TelegramUserService;
import org.telegram.telegrambots.meta.api.objects.Update;

import java.util.Optional;

/**
* Stop {@link Command}.
*/
public class StopCommand implements Command {

   private final SendBotMessageService sendBotMessageService;
   private final TelegramUserService telegramUserService;

   public static final String STOP_MESSAGE = "Деактивировал все ваши подписки \uD83D\uDE1F.";

   public StopCommand(SendBotMessageService sendBotMessageService, TelegramUserService telegramUserService) {
       this.sendBotMessageService = sendBotMessageService;
       this.telegramUserService = telegramUserService;
   }

   @Override
   public void execute(Update update) {
       sendBotMessageService.sendMessage(update.getMessage().getChatId().toString(), STOP_MESSAGE);
       telegramUserService.findByChatId(update.getMessage().getChatId().toString())
               .ifPresent(it -> {
                   it.setActive(false);
                   telegramUserService.save(it);
               });
   }
}
نقوم بتمرير TelegramServiceTest إلى StopCommand بنفس الطريقة. المنطق الإضافي هو كما يلي: إذا كان لدينا مستخدم لديه معرف الدردشة هذا، فإننا نقوم بإلغاء تنشيطه، أي أننا نقوم بتعيين نشط = خطأ. كيف يمكنك أن ترى هذا بأم عينيك؟ لنقم بإنشاء أمر /stat جديد، والذي سيعرض إحصائيات الروبوت. في هذه المرحلة، ستكون هذه إحصائيات بسيطة متاحة لجميع المستخدمين. في المستقبل، سوف نقوم بتقييده وجعل الوصول للمسؤولين فقط. سيكون هناك إدخال واحد في الإحصائيات: عدد مستخدمي الروبوت النشطين. للقيام بذلك، قم بإضافة القيمة STAT("/stat") إلى CommandName. بعد ذلك، قم بإنشاء فئة StatCommand :
package com.github.javarushcommunity.jrtb.command;

import com.github.javarushcommunity.jrtb.service.SendBotMessageService;
import com.github.javarushcommunity.jrtb.service.TelegramUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.telegram.telegrambots.meta.api.objects.Update;

/**
* Statistics {@link Command}.
*/
public class StatCommand implements Command {

   private final TelegramUserService telegramUserService;
   private final SendBotMessageService sendBotMessageService;

   public final static String STAT_MESSAGE = "Javarush Telegram Bot использует %s человек.";

   @Autowired
   public StatCommand(SendBotMessageService sendBotMessageService, TelegramUserService telegramUserService) {
       this.sendBotMessageService = sendBotMessageService;
       this.telegramUserService = telegramUserService;
   }

   @Override
   public void execute(Update update) {
       int activeUserCount = telegramUserService.retrieveAllActiveUsers().size();
       sendBotMessageService.sendMessage(update.getMessage().getChatId().toString(), String.format(STAT_MESSAGE, activeUserCount));
   }
}
كل شيء بسيط هنا: نحصل على قائمة بجميع المستخدمين النشطين باستخدام طريقة استرداد AllActiveUsers ونحصل على حجم المجموعة. نحتاج الآن أيضًا إلى تحديث الفئات الصاعدة: CommandContainer و JavarushTelegramBot حتى يتعلموا نقل الخدمة الجديدة التي نحتاجها. حاوية الأوامر:
package com.github.javarushcommunity.jrtb.command;

import com.github.javarushcommunity.jrtb.service.SendBotMessageService;
import com.github.javarushcommunity.jrtb.service.TelegramUserService;
import com.google.common.collect.ImmutableMap;

import static com.github.javarushcommunity.jrtb.command.CommandName.*;

/**
* Container of the {@link Command}s, which are using for handling telegram commands.
*/
public class CommandContainer {

   private final ImmutableMap<String, Command> commandMap;
   private final Command unknownCommand;

   public CommandContainer(SendBotMessageService sendBotMessageService, TelegramUserService telegramUserService) {

       commandMap = ImmutableMap.<String, Command>builder()
               .put(START.getCommandName(), new StartCommand(sendBotMessageService, telegramUserService))
               .put(STOP.getCommandName(), new StopCommand(sendBotMessageService, telegramUserService))
               .put(HELP.getCommandName(), new HelpCommand(sendBotMessageService))
               .put(NO.getCommandName(), new NoCommand(sendBotMessageService))
               .put(STAT.getCommandName(), new StatCommand(sendBotMessageService, telegramUserService))
               .build();

       unknownCommand = new UnknownCommand(sendBotMessageService);
   }

   public Command retrieveCommand(String commandIdentifier) {
       return commandMap.getOrDefault(commandIdentifier, unknownCommand);
   }

}
أضفنا هنا أمرًا جديدًا إلى الخريطة وقمنا بتمريره عبر مُنشئ TelegramUserService. لكن في الروبوت نفسه، لن يتغير سوى المنشئ:
@Autowired
public JavarushTelegramBot(TelegramUserService telegramUserService) {
   this.commandContainer = new CommandContainer(new SendBotMessageServiceImpl(this), telegramUserService);
}
نقوم الآن بتمرير TelegramUserService كوسيطة، مع إضافة التعليق التوضيحي Autowired. هذا يعني أننا سوف نتلقاها من سياق التطبيق. سنقوم أيضًا بتحديث فئة HelpCommand بحيث يظهر أمر إحصائيات جديد في الوصف.

الاختبار اليدوي

لنقم بتشغيل قاعدة البيانات من docker-compose-test.yml والطريقة الرئيسية في فئة JavarushTelegramBotApplication. بعد ذلك نكتب مجموعة من الأوامر:
  • /stat - نتوقع أنه إذا كانت قاعدة البيانات فارغة، فلن يكون هناك أي شخص يستخدم هذا الروبوت؛
  • /بدء - بدء تشغيل الروبوت؛
  • /stat - نتوقع الآن أن يتم استخدام الروبوت بواسطة شخص واحد؛
  • / توقف - إيقاف الروبوت؛
  • /stat - نتوقع أنه لن يكون هناك أي شخص يستخدمه مرة أخرى.
"مشروع جافا من الألف إلى الياء": إضافة كل ما يتعلق بقاعدة البيانات.  الجزء 2 - 2إذا كانت النتيجة هي نفسها بالنسبة لك، فيمكننا القول أن الوظيفة تعمل بشكل صحيح وأن الروبوت يعمل بشكل صحيح. إذا حدث خطأ ما، فلا يهم: نعيد تشغيل الطريقة الرئيسية في وضع التصحيح وننتقل بوضوح عبر المسار بأكمله للعثور على الخطأ.

نكتب ونحدث الاختبارات

وبما أننا قمنا بتغيير المنشئات، فسنحتاج إلى تحديث فئات الاختبار أيضًا. في فئة AbstractCommandTest ، نحتاج إلى إضافة حقل آخر - فئة TelegramUserService ، وهو مطلوب لثلاثة أوامر:
protected TelegramUserService telegramUserService = Mockito.mock(TelegramUserService.class);
بعد ذلك، لنقم بتحديث طريقة init() في CommandContainer :
@BeforeEach
public void init() {
   SendBotMessageService sendBotMessageService = Mockito.mock(SendBotMessageService.class);
   TelegramUserService telegramUserService = Mockito.mock(TelegramUserService.class);
   commandContainer = new CommandContainer(sendBotMessageService, telegramUserService);
}
في StartCommand تحتاج إلى تحديث طريقة getCommand() :
@Override
Command getCommand() {
   return new StartCommand(sendBotMessageService, telegramUserService);
}
أيضًا في StopCommand:
@Override
Command getCommand() {
   return new StopCommand(sendBotMessageService, telegramUserService);
}
بعد ذلك، دعونا نلقي نظرة على الاختبارات الجديدة. لنقم بإنشاء اختبار نموذجي لـ StatCommand :
package com.github.javarushcommunity.jrtb.command;

import static com.github.javarushcommunity.jrtb.command.CommandName.STAT;
import static com.github.javarushcommunity.jrtb.command.StatCommand.STAT_MESSAGE;

public class StatCommandTest extends AbstractCommandTest {
   @Override
   String getCommandName() {
       return STAT.getCommandName();
   }

   @Override
   String getCommandMessage() {
       return String.format(STAT_MESSAGE, 0);
   }

   @Override
   Command getCommand() {
       return new StatCommand(sendBotMessageService, telegramUserService);
   }
}
هذا بسيط. الآن دعونا نتحدث عن كيفية اختبار العمل مع قاعدة البيانات. كل ما فعلناه من قبل كان اختبارات الوحدة. يختبر اختبار التكامل التكامل بين أجزاء متعددة من التطبيق. على سبيل المثال، التطبيقات وقواعد البيانات. هنا سيكون كل شيء أكثر تعقيدا، لأننا نحتاج إلى قاعدة بيانات منتشرة للاختبار. لذلك، عندما نقوم بإجراء اختباراتنا محليًا، يجب أن تكون قاعدة البيانات قيد التشغيل من docker-compose-test.yml. لإجراء هذا الاختبار، تحتاج إلى تشغيل تطبيق SpringBoot بأكمله. تحتوي فئة الاختبار على تعليق توضيحي SpringBootTest الذي سيبدأ التطبيق. لكن هذا النهج لن ينجح معنا، لأنه عند تشغيل التطبيق، سيتم تشغيل روبوت التليجرام أيضًا. ولكن هناك تناقض هنا. سيتم إجراء الاختبارات محليًا على أجهزتنا وبشكل عام عبر GitHub Actions. لكي تنجح الاختبارات مع إطلاق التطبيق بأكمله، يجب علينا تشغيلها ببيانات صالحة على بوت التليجرام: أي باسمه ورمزه المميز... لذلك أمامنا خياران:
  1. لذا اجعل اسم الروبوت ورمزه علنيًا ونأمل أن يكون كل شيء على ما يرام، ولن يستخدمه أحد ويتدخل معنا.
  2. الخروج بطريقة أخرى.
اخترت الخيار الثاني. يحتوي اختبار SpringBoot على تعليق توضيحي DataJpaTest ، والذي تم إنشاؤه بحيث نستخدم فقط الفئات التي نحتاجها عند اختبار قاعدة بيانات ونترك الآخرين بمفردهم. ولكن هذا يناسبنا، لأن بوت Telegram لن يبدأ على الإطلاق. هذا يعني أنه ليست هناك حاجة لتمرير اسم ورمز صالحين له!))) سنحصل على اختبار نتحقق فيه من أن الأساليب التي تنفذها Spring Data لنا تعمل كما نتوقع. من المهم أن نلاحظ هنا أننا نستخدم التعليق التوضيحي @ActiveProfiles("test") لتحديد استخدام ملف تعريف الاختبار. وهذا هو بالضبط ما نحتاجه حتى نتمكن من حساب الخصائص الصحيحة لقاعدة البيانات الخاصة بنا. سيكون من الجيد أن تكون لدينا قاعدة بيانات جاهزة قبل إجراء اختباراتنا. يوجد مثل هذا النهج لهذه المسألة: قم بإضافة تعليق توضيحي Sql إلى الاختبار وتمرير مجموعة من أسماء البرامج النصية التي يجب تشغيلها قبل بدء الاختبار:
@Sql(scripts = {"/sql/clearDbs.sql", "/sql/telegram_users.sql"})
بالنسبة لنا، سيتم تحديد موقعهم على طول المسار ./src/test/resources/ + المسار المحدد في التعليق التوضيحي. وهنا ما تبدو عليه:
clearDbs.sql:
DELETE FROM tg_user;

telegram_users.sql:
INSERT INTO tg_user VALUES ("123456789", 1);
INSERT INTO tg_user VALUES ("123456788", 1);
INSERT INTO tg_user VALUES ("123456787", 1);
INSERT INTO tg_user VALUES ("123456786", 1);
INSERT INTO tg_user VALUES ("123456785", 1);
INSERT INTO tg_user VALUES ("123456784", 0);
INSERT INTO tg_user VALUES ("123456782", 0);
INSERT INTO tg_user VALUES ("123456781", 0);
هذا ما سيبدو عليه نتيجة اختبار TelegramUserRepositoryIT (كما ترون، اسم اختبار التكامل سيكون مختلفًا - نضيف تكنولوجيا المعلومات، وليس اختبار):
package com.github.javarushcommunity.jrtb.repository;

import com.github.javarushcommunity.jrtb.repository.entity.TelegramUser;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;

import java.util.List;
import java.util.Optional;

import static org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace.NONE;

/**
* Integration-level testing for {@link TelegramUserRepository}.
*/
@ActiveProfiles("test")
@DataJpaTest
@AutoConfigureTestDatabase(replace = NONE)
public class TelegramUserRepositoryIT {

   @Autowired
   private TelegramUserRepository telegramUserRepository;

   @Sql(scripts = {"/sql/clearDbs.sql", "/sql/telegram_users.sql"})
   @Test
   public void shouldProperlyFindAllActiveUsers() {
       //when
       List<TelegramUser> users = telegramUserRepository.findAllByActiveTrue();

       //then
       Assertions.assertEquals(5, users.size());
   }

   @Sql(scripts = {"/sql/clearDbs.sql"})
   @Test
   public void shouldProperlySaveTelegramUser() {
       //given
       TelegramUser telegramUser = new TelegramUser();
       telegramUser.setChatId("1234567890");
       telegramUser.setActive(false);
       telegramUserRepository.save(telegramUser);

       //when
       Optional<TelegramUser> saved = telegramUserRepository.findById(telegramUser.getChatId());

       //then
       Assertions.assertTrue(saved.isPresent());
       Assertions.assertEquals(telegramUser, saved.get());
   }
}
لقد كتبنا الاختبارات، ولكن السؤال الذي يطرح نفسه: ماذا سيحدث عند إطلاق عملية CI الخاصة بنا على GitHub؟ لن يكون لها قاعدة بيانات. في الوقت الحالي سيكون هناك فقط بناء أحمر. للقيام بذلك، لدينا إجراءات GitHub، والتي يمكننا من خلالها تكوين إطلاق الإصدار الخاص بنا. قبل إجراء الاختبارات، تحتاج إلى إضافة تشغيل قاعدة البيانات بالإعدادات اللازمة. كما اتضح، لا توجد أمثلة كثيرة على الإنترنت، لذا أنصحك بحفظ هذا في مكان ما. لنقم بتحديث ملف .github/workflows/maven.yml:
# This workflow will build a Java project with Maven
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven

name: Java CI with Maven

on:
 push:
   branches: [ main ]
 pull_request:
   branches: [ main ]

jobs:
 build:
   runs-on: ubuntu-latest
   steps:
   - uses: actions/checkout@v2
   - name: Set up MySQL
     uses: mirromutth/mysql-action@v1.1
     with:
       mysql version: '5.7'
       mysql database: 'dev_jrtb_db'
       mysql root password: 'root'
       mysql user: 'dev_jrtb_db_user'
       mysql password: 'dev_jrtb_db_password'
   - name: Set up JDK 1.11
     uses: actions/setup-java@v1
     with:
       java-version: 1.11
   - name: Build with Maven
     run: mvn -B package --file pom.xml
يوجد الآن كتلة إعداد MySQL جديدة . نضيف فيه MySQL إلى عملية CI الخاصة بنا، ونحدد في نفس الوقت المتغيرات التي نحتاجها. الآن أضفنا كل ما أردناه. المرحلة الأخيرة هي دفع التغييرات والتأكد من أن البناء سوف يمر ويصبح أخضر.

تحديث الوثائق

لنقم بتحديث إصدار المشروع من 0.3.0-SNAPSHOT إلى 0.4.0-SNAPSHOT في pom.xml ونضيفه أيضًا إلى RELEASE_NOTES:
## 0.4.0-SNAPSHOT

*   JRTB-1: added repository layer.
بعد كل هذا، نقوم بإنشاء طلب الالتزام والدفع والسحب. والأهم من ذلك أن بنياننا أخضر!"مشروع جافا من الألف إلى الياء": إضافة كل ما يتعلق بقاعدة البيانات.  الجزء 2 - 3

روابط مفيدة:

يمكن رؤية جميع التغييرات هنا في طلب السحب الذي تم إنشاؤه . شكرا للجميع على القراءة.

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

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