JavaRush /Java Blog /Random EN /We add everything related to the database. (Part 2) - "Ja...

We add everything related to the database. (Part 2) - "Java project from A to Z"

Published in the Random EN group
Hi all. Let me remind you: in the first part we added Flyway. Let's continue.

Adding a database to docker-compose.yml

The next stage is setting up work with the database in the main docker-compose.yml. Let's add the database to the docker-compose file:
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'
I also added this line to our application:
depends_on:
 - jrtb-db
This means that we wait for the database to start before starting the application. Next, you can notice the addition of two more variables that we need to work with the database:
${BOT_DB_USERNAME}
${BOT_DB_PASSWORD}
We will get them in docker-compose in the same way as for the telegram bot - through environment variables. I did this so that we have only one place where we set the values ​​of the database user name and its password. We pass them to the docker image of our application and to the docker container of our database. Next we need to update the Dockerfile to teach our SpringBoot to accept variables for the database.
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"]
Now we add database variables to the Dockerfile:
ENV BOT_DB_USERNAME=jrtb_db_user
ENV BOT_DB_PASSWORD=jrtb_db_password
The variable values ​​will be different. The ones we will pass into the Dockerfile, however, require default values, so I entered some. We expand the last line with two elements, with the help of which we will pass the DB username and password to the application launch:
"-Dspring.datasource.password=${BOT_DB_PASSWORD}", "-Dbot.username=${BOT_NAME}"
The last line in the Dockerfile (which starts with ENTRYPOINT) must be without wrapping. If you make a transfer, this code will not work. The last step is to update the start.sh file to pass variables to the database.
#!/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
We already know how to add environment variables before running docker-compose. To do this, you just need to execute export var_name=var_value.. Therefore, we add only two lines:
export BOT_DB_USERNAME='prod_jrtb_db_user'
export BOT_DB_PASSWORD='Pap9L9VVUkNYj99GCUCC3mJkb'
This is where we set the database username and password. Of course, it would be possible to pass these variables when running the bash script, as we do for the bot’s name and token. But it seems to me that this is unnecessary. To actually access the database, you need to know the IP of the server on which the database will be deployed, and be on the list of allowed IP addresses for the request. As for me, this is already enough. The foundation has been laid: now you can do things that are more understandable for a developer - write code. Before that, we were doing what DevOps engineers do - setting up the environment.

Adding a Repository layer

Typically an application has three layers:
  1. Controllers are the entry points into the application.
  2. Services are where business logic works. We already partially have this: SendMessageService is an explicit representative of business logic.
  3. Repositories are a place to work with a database. In our case, this is a telegram bot.
Now we will add the third layer - repositories. Here we will use a project from the Spring ecosystem - Spring Data. You can read about what it is in this article on Habré . We need to know and understand several points:
  1. We will not have to work with JDBC: we will work directly with higher abstractions. That is, store POJOs that correspond to tables in the database. We will call such classes entity , as they are officially called in the Java Persistence API (this is a common set of interfaces for working with a database through an ORM, that is, an abstraction over working with JDBC). We will have an entity class that we will save in the database, and they will be written to exactly the table that we need. We will receive the same objects when searching in the database.
  2. Spring Data offers to use their set of interfaces: JpaRepository , CrudRepository , etc... There are other interfaces: a full list can be found here . The beauty is that you can use their methods without implementing them(!). Moreover, there is a certain template using which you can write new methods in the interface, and they will be implemented automatically.
  3. Spring simplifies our development as much as it can. To do this, we need to create our own interface and inherit from those described above. And so that Spring knows that it needs to use this interface, add the Repository annotation.
  4. If we need to write a method for working with a database that does not exist, then this is also not a problem - we will write it. I'll show you what and how to do there.
In this article, we will work with adding along the entire path of TelegramUser and show this part as an example. We will expand the rest on other tasks. That is, when we execute the /start command, we will write active = true to the database of our user. This will mean that the user is using a bot. If the user is already in the database, we will update the field active = true. When executing the /stop command, we will not delete the user, but will only update the active field to false, so that if the user wants to use the bot again, he can start it and pick up where he left off. And so that when testing we can see that something is happening, we will create a /stat command: it will display the number of active users. We create a repository package next to the bot, command, service packages. In this package we create another one - entity . In the entity package we create the TelegramUser class:
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;
}
Here you can see that we have all the annotations from the javax.persistence package. These are common annotations that are used for all ORM implementations. By default, Spring Data Jpa uses Hibernate, although other implementations can be used. Here is a list of annotations we use:
  • Entity - indicates that this is an entity for working with the database;
  • Table - here we define the name of the table;
  • Id - the annotation says which field will be the Primary Key in the table;
  • Column - determine the name of the field from the table.
Next, we create an interface for working with the database. Typically, the names of such interfaces are written using the template - EntiryNameRepository. We will have a TelegramuserRepository:
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();
}
Here you can see how I added the findAllByActiveTrue() method , which I do not implement anywhere. But that won't stop him from working. Spring Data will understand that it needs to get all records from the tg_user table whose active field = true . We add a service for working with the TelegramUser entity (we use dependency inversion from SOLID in the context that services of other entities cannot directly communicate with the repository of another entity - only through the service of that entity). We create a service TelegramUserService in the package, which for now will have several methods: save the user, get the user by his ID and display a list of active users. First we create the TelegramUserService interface:
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);
}
And, in fact, the implementation of 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);
   }
}
Here it should be noted that we use dependency injection (introduce a class instance) of the TelegramuserRepository object using the Autowired annotation , and on the constructor. You can do this for a variable, but this is the approach the Spring Framework team recommends to us.

Adding statistics for the bot

Next you need to update the /start and /stop commands. When the /start command is used, you need to save the new user in the database and set it to active = true. And when there is /stop, update the user data: set active = false. Let's fix the StartCommand class :
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);
   }
}
Here we also pass the TelegramuserService object to the constructor, with which we will save the new user. Further, using the delights of Optional in Java, the following logic works: if we have a user in the database, we simply make him active, if not, we create a new active one. StopCommand:
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);
               });
   }
}
We pass TelegramServiceTest to StopCommand in the same way. The additional logic is this: if we have a user with such a chat ID, we deactivate it, that is, we set active = false. How can you see this with your own eyes? Let's create a new command /stat, which will display the bot's statistics. At this stage, these will be simple statistics available to all users. In the future, we will limit it and make access only for administrators. There will be one entry in the statistics: the number of active bot users. To do this, add the value STAT("/stat") to CommandName. Next, create the StatCommand class:
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));
   }
}
Everything is simple here: we get a list of all active users using the retrieveAllActiveUsers method and get the size of the collection. We also now need to update the ascending classes: CommandContainer and JavarushTelegramBot so that they learn to transfer the new service we need. CommandContainer:
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);
   }

}
Here we added a new command to the map and passed it through the TelegramUserService constructor. But in the bot itself, only the constructor will change:
@Autowired
public JavarushTelegramBot(TelegramUserService telegramUserService) {
   this.commandContainer = new CommandContainer(new SendBotMessageServiceImpl(this), telegramUserService);
}
Now we pass TelegramUserService as an argument, adding the Autowired annotation. This means that we will receive it from the Application Context. We will also update the HelpCommand class so that a new statistics command appears in the description.

Manual testing

Let's launch the database from docker-compose-test.yml and the main method in the JavarushTelegramBotApplication class. Next we write a set of commands:
  • /stat - we expect that if the database is empty, there will be zero people using this bot;
  • /start - start the bot;
  • /stat - now we expect that the bot will be used by 1 person;
  • /stop - stop the bot;
  • /stat - we expect that again there will be 0 people using it.
"Java project from A to Z": Adding everything related to the database.  Part 2 - 2If the result is the same for you, we can say that the functionality worked correctly and the bot is working properly. If something goes wrong, it doesn’t matter: we restart the main method in debug mode and clearly go through the entire path to find what the error was.

We write and update tests

Since we changed the constructors, we will need to update the test classes as well. In the AbstractCommandTest class, we need to add one more field - the TelegramUserService class , which is needed for three commands:
protected TelegramUserService telegramUserService = Mockito.mock(TelegramUserService.class);
Next, let’s update the init() method in CommandContainer :
@BeforeEach
public void init() {
   SendBotMessageService sendBotMessageService = Mockito.mock(SendBotMessageService.class);
   TelegramUserService telegramUserService = Mockito.mock(TelegramUserService.class);
   commandContainer = new CommandContainer(sendBotMessageService, telegramUserService);
}
In StartCommand you need to update the getCommand() method:
@Override
Command getCommand() {
   return new StartCommand(sendBotMessageService, telegramUserService);
}
Also in StopCommand:
@Override
Command getCommand() {
   return new StopCommand(sendBotMessageService, telegramUserService);
}
Next, let's look at the new tests. Let's create a typical test for 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);
   }
}
This is simple. Now let's talk about how we will test working with the database. All we did before were unit tests. An integration test tests integration between multiple parts of an application. For example, applications and databases. Here everything will be more complicated, because for testing we need a deployed database. Therefore, when we run our tests locally, we must have the database running from docker-compose-test.yml. To run this test, you need to run the entire SpringBoot application. The test class has a SpringBootTest annotation that will start the application. But this approach will not work for us, because when the application is launched, the telegram bot will also launch. But there is a contradiction here. Tests will be run both locally on our machine and publicly via GitHub Actions. In order for the tests to pass with the launch of the entire application, we must run them with valid data on the telegram bot: that is, by its name and token... Therefore, we have two options:
  1. So make the name and token of the bot public and hope that everything will be fine, no one will use it and interfere with us.
  2. Come up with another way.
I chose the second option. SpringBoot testing has the DataJpaTest annotation , which was created so that when testing a database, we use only the classes we need and leave others alone. But this suits us, because the telegram bot will not launch at all. This means that there is no need to pass it a valid name and token!))) We will get a test in which we will check that the methods that Spring Data implements for us work as we expect. It is important to note here that we use the @ActiveProfiles("test") annotation to specify the use of the test profile. And this is exactly what we need so that we can count the correct properties for our database. It would be nice to have a database prepared before running our tests. There is such an approach for this matter: add a Sql annotation to the test and pass it a collection of script names that need to be run before starting the test:
@Sql(scripts = {"/sql/clearDbs.sql", "/sql/telegram_users.sql"})
For us, they will be located along the path ./src/test/resources/ + the path specified in the annotation. Here's what they look like:
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);
This is what our TelegramUserRepositoryIT test will look like as a result (as you can see, the name for integration testing will be different - we add IT, not Test):
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());
   }
}
We wrote the tests, but the question arises: what will happen with the launch of our CI process on GitHub? It won't have a database. For now there will really just be a red build. To do this, we have GitHub actions, in which we can configure the launch of our build. Before running the tests, you need to add a database launch with the necessary settings. As it turns out, there aren’t many examples on the Internet, so I advise you to save this somewhere. Let's update the .github/workflows/maven.yml file:
# 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
Now there is a new Set up MySQL block . In it we add MySQL to our CI process, simultaneously defining the variables we need. Now we have added everything we wanted. The last stage is to push the changes and see that the build will pass and be green.

Updating the documentation

Let's update the project version from 0.3.0-SNAPSHOT to 0.4.0-SNAPSHOT in pom.xml and also add to RELEASE_NOTES:
## 0.4.0-SNAPSHOT

*   JRTB-1: added repository layer.
After all this, we create a commit, push and pull request. And most importantly, our build is green!"Java project from A to Z": Adding everything related to the database.  Part 2 - 3

Useful links:

All changes can be seen here in the created pull request . Thanks everyone for reading.

A list of all materials in the series is at the beginning of this article.

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