大家好。讓我提醒您:在第一部分中我們新增了 Flyway。我們繼續吧。
將資料庫加入 docker-compose.yml
下一階段是在主 docker-compose.yml 中設定資料庫工作。讓我們將資料庫新增到 docker-compose 檔案: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 中獲取它們 - 透過環境變數。我這樣做是為了我們只有一個地方可以設定資料庫使用者名稱及其密碼的值。我們將它們傳遞給應用程式的 docker 映像和資料庫的 docker 容器。接下來,我們需要更新 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 之前新增環境變數。為此,你只需要執行export var_name=var_value.。因此,我們只增加兩行:
export BOT_DB_USERNAME='prod_jrtb_db_user'
export BOT_DB_PASSWORD='Pap9L9VVUkNYj99GCUCC3mJkb'
這是我們設定資料庫使用者名稱和密碼的地方。當然,在運行 bash 腳本時可以傳遞這些變量,就像我們對機器人的名稱和令牌所做的那樣。但在我看來,這是沒有必要的。要實際存取資料庫,您需要知道將部署資料庫的伺服器的 IP,並且位於請求允許的 IP 位址清單中。對我來說,這已經足夠了。基礎已經奠定:現在您可以做開發人員更容易理解的事情 - 編寫程式碼。在此之前,我們正在做 DevOps 工程師所做的事情——設定環境。
新增儲存庫層
通常,應用程式具有三層:- 控制器是應用程式的入口點。
- 服務是業務邏輯發揮作用的地方。我們已經部分了解了這一點:SendMessageService 是業務邏輯的明確代表。
- 儲存庫是使用資料庫的地方。在我們的例子中,這是一個電報機器人。
- 我們不必使用 JDBC:我們將直接使用更高的抽象。即儲存與資料庫中的表格對應的POJO。我們將此類類別稱為「實體」 ,因為它們在Java 持久性 API中被正式稱為「實體」(這是透過 ORM 操作資料庫的一組通用接口,即對 JDBC 操作的抽象)。我們將有一個實體類,將其保存在資料庫中,它們將準確地寫入我們需要的表中。在資料庫中搜尋時,我們將收到相同的物件。
- Spring Data 提供使用它們的介面集:JpaRepository、CrudRepository等...還有其他介面:可以在此處找到完整清單。美妙之處在於您可以使用它們的方法而不需要實現它們(!)。此外,還有一個特定的模板,您可以使用它在介面中編寫新方法,並且它們將自動實現。
- Spring 盡可能地簡化了我們的開發。為此,我們需要建立自己的介面並繼承上述介面。為了讓 Spring 知道它需要使用這個接口,請加入 Repository 註解。
- 如果我們需要編寫一個方法來處理不存在的資料庫,那麼這也不是問題 - 我們會編寫它。我將向您展示在那裡做什麼以及如何做。
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,但也可以使用其他實作。以下是我們使用的註釋清單:
- Entity - 表示這是一個用於操作資料庫的實體;
- Table-這裡我們定義表格的名稱;
- Id - 註解顯示哪個欄位將成為表中的主鍵;
- 列- 確定表格中欄位的名稱。
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 表中取得active 欄位 = true的所有記錄。我們新增了一個與 TelegramUser 實體一起使用的服務(在其他實體的服務無法直接與另一個實體的儲存庫通訊的情況下,我們使用 SOLID 的依賴關係反轉 - 只能透過該實體的服務)。我們在套件中建立一個服務 TelegramUserService,它現在有幾種方法:保存使用者、透過使用者 ID 取得使用者並顯示活動使用者清單。首先我們建立 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);
}
}
這裡要注意的是,我們使用Autowired註解對 TelegramuserRepository 物件進行依賴注入(引入類別實例) ,並在建構函式上進行依賴注入。您可以對變數執行此操作,但這是 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。額外的邏輯是這樣的:如果我們有一個具有這樣聊天ID的用戶,我們將其停用,即我們設定active = false。你怎麼能親眼看到這一點?讓我們建立一個新命令 /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));
}
}
這裡一切都很簡單:我們使用retrieveAllActiveUsers方法 來取得所有活動使用者的列表,並取得集合的大小。我們現在還需要更新上升的類別: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 類別中的 main 方法啟動資料庫。接下來我們寫一組命令:- /stat - 我們預期如果資料庫為空,則使用此機器人的人數將為零;
- /start - 啟動機器人;
- /stat - 現在我們預計該機器人將由 1 人使用;
- /stop - 停止機器人;
- /stat - 我們預計將再次有 0 人使用它。
我們編寫和更新測試
由於我們更改了建構函數,因此我們還需要更新測試類別。在AbstractCommandTest類別中,我們需要再增加一個欄位 - TelegramUserService類,三個命令都需要它:protected TelegramUserService telegramUserService = Mockito.mock(TelegramUserService.class);
接下來,我們更新CommandContainer 中的init()方法:
@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 公開運行。為了使測試在整個應用程式啟動時通過,我們必須使用電報機器人上的有效數據來運行它們:即透過其名稱和令牌......因此,我們有兩個選擇:
- 因此,將機器人的名稱和令牌公開,並希望一切順利,沒有人會使用它並幹擾我們。
- 想出另一種辦法。
@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 測試的結果(如您所見,整合測試的名稱會有所不同 - 我們添加 IT,而不是測試):
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());
}
}
我們編寫了測試,但問題出現了:在 GitHub 上啟動 CI 流程會發生什麼?它不會有資料庫。現在實際上只有一個紅色版本。為此,我們有 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
現在有一個新的Set up MySQL區塊。在其中,我們將 MySQL 加入到 CI 流程中,同時定義我們需要的變數。現在我們已經添加了我們想要的一切。最後一個階段是推動變更並確保建置能夠通過並變為綠色。
更新文件
讓我們在 pom.xml 中將專案版本從 0.3.0-SNAPSHOT 更新為 0.4.0-SNAPSHOT,並新增至 RELEASE_NOTES:## 0.4.0-SNAPSHOT
* JRTB-1: added repository layer.
所有這些之後,我們建立一個提交、推送和拉取請求。最重要的是,我們的建造是綠色的!
有用的連結:
- 我們的電報機器人的儲存庫
- 拉取請求以及文章中描述的所有更改
- SpringBoot + Flyway文章
- 來自 DockerHub 的MySQL 映像
- 中:如何使用 Docker Compose 建立 MySql 實例
- Habr:Spring Data Jpa
- 我的電報頻道
GO TO FULL VERSION