MyBatis Junior Actual 2: Additions, Deletions, and Reviews

Welcome to my GitHub

https://github.com/zq2599/blog_demos
Content: A summary of all original articles, including Java, Docker, Kubernetes, DevOPS, etc.

This is the second article in the MyBatis Beginner Actual Warfare series. We know how to integrate MyBatis in SpringBoot from the previous article. We will practice basic skills together: adding, deleting, and checking.

This Overview

The following are the exercises to be practiced in this article:

  1. Check for additions and deletions of forms
  2. Bulk Add
  3. Joint Table Query

The full text consists of the following parts:

  1. New Project
  2. Add Startup Class
  3. Add swagger's configuration class, and the project includes swagger for later validation on the browser
  4. Add Profile
  5. Add Entity Class
  6. Add mapper profile
  7. Add mapper interface
  8. Add service, call mapper interface
  9. Add controller to call service service service
  10. Write unit test cases
  11. Verification

Source Download

  1. If you don't want to encode, you can download all the sources in GitHub, with the address and link information shown in the table below ( https://github.com/zq2599/blog_demos):
Name link Remarks
Project Home https://github.com/zq2599/blog_demos The project's home page on GitHub
git repository address (https) https://github.com/zq2599/blog_demos.git Warehouse address for the project source code, https protocol
git warehouse address (ssh) git@github.com:zq2599/blog_demos.git Warehouse address for the project source code, ssh protocol
  1. There are several folders in this git project. The application of this chapter is in the mybatis folder, as shown in the red box below:

Development

  1. The database and table structure used in the actual operation of this article and the previous one One of MyBatis's Beginning Actuals: Spring Boot Integration As like as two peas;
  2. Previous One of MyBatis's Beginning Actuals: Spring Boot Integration A new parent project mybatis has been created, and this article continues to add a new subproject in this project, named curd, with the entire subproject file structure as follows:
  3. Modify the POM of the parent project mybatis. Xml, add two dependency nodes under the dependency management node, as shown below, in order to unify the versions of the dependency libraries:
<!-- swagger-ui -->
<dependency>
  <groupId>io.springfox</groupId>
  <artifactId>springfox-swagger-ui</artifactId>
  <version>2.5.0</version>
</dependency>

<dependency>
  <groupId>com.google.code.gson</groupId>
   <artifactId>gson</artifactId>
  <version>2.8.6</version>
</dependency>
  1. Named curd subproject, its pom. The XML is as follows:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.bolingcavalry</groupId>
        <artifactId>mybatis</artifactId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <groupId>com.bolingcavalry</groupId>
    <artifactId>curd</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>curd</name>
    <description>Demo project for Mybatis CURD in Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
        </dependency>
        <!-- swagger-ui -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  1. Add a startup class, and note that the MapperScan comment specifies the package path for the mapper interface code:
package com.bolingcavalry.curd;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.bolingcavalry.curd.mapper")
public class CurdApplication {

    public static void main(String[] args) {
        SpringApplication.run(CurdApplication.class, args);
    }
}
  1. swagger is used in this battle, which makes it easy to send requests to controller s through the browser. Here are the configuration classes:
package com.bolingcavalry.curd;

import springfox.documentation.service.Contact;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Tag;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .tags(new Tag("UserController", "User Services"), new Tag("LogController", "Logging Service"))
                .select()
                // Current Package Path
                .apis(RequestHandlerSelectors.basePackage("com.bolingcavalry.curd.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    //Build the details function for the api document, noting which of the comments here refers to
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                //Page Title
                .title("MyBatis CURD operation")
                //Creator
                .contact(new Contact("Programmer Xin Chen", "https://github.com/zq2599/blog_demos", "zq2599@gmail.com"))
                //version number
                .version("1.0")
                //describe
                .description("API describe")
                .build();
    }
}
  1. application.yml is as follows:
server:
  port: 8080

spring:
  # data source
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://192.168.50.43:3306/mybatis?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
    driver-class-name: com.mysql.cj.jdbc.Driver

# mybatis configuration
mybatis:
  # Location of profile
  config-location: classpath:mybatis-config.xml
  # Mapping file location
  mapper-locations: classpath:mappers/*Mapper.xml

# Log Configuration
logging:
  level:
    root: INFO
    com:
      bolingcavalry:
        curd:
          mapper: debug
  1. Increase the entity class User of the user table. Java, with swagger's comments, for easy display on the swagger page:
package com.bolingcavalry.curd.entity;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;

@ApiModel(description = "User Entity Class")
public class User {

    @ApiModelProperty(value = "user ID")
    private Integer id;

    @ApiModelProperty(value = "User name", required = true)
    private String name;

    @ApiModelProperty(value = "User Address", required = false)
    private Integer age;

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
    
    // get and set methods are omitted, please complete them yourself
}
  1. Add the entity class Log of the log table. Java, with swagger's comments, for easy display on the swagger page:
package com.bolingcavalry.curd.entity;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;

import java.sql.Date;

/**
 * @Description: Entity Class
 * @author: willzhao E-mail: zq2599@gmail.com
 * @date: 2020/8/4 8:24
 */
@ApiModel(description = "Log Entity Class")
public class Log {
    @ApiModelProperty(value = "Journal ID")
    private Integer id;

    @ApiModelProperty(value = "user ID")
    private Integer userId;

    @ApiModelProperty(value = "Log Content")
    private String action;

    @ApiModelProperty(value = "Creation Time")
    private Date createTime;

    @Override
    public String toString() {
        return "Log{" +
                "id=" + id +
                ", userId=" + userId +
                ", action='" + action + '\'' +
                ", createTime=" + createTime +
                '}';
    }
    // get and set methods are omitted, please complete them yourself
}

  1. Prepare a bean named LogExtend for the results of the join table query. Java, inherited from Log.java, which has only a userName field of its own, queries the name field of the user table for the associated table:
package com.bolingcavalry.curd.entity;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;

@ApiModel(description = "Log Entity Class(Fields with user tables)")
public class LogExtend extends Log {


    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    @ApiModelProperty(value = "User name")
    private String userName;

    @Override
    public String toString() {
        return "LogExtend{" +
                "id=" + getId() +
                ", userId=" + getUserId() +
                ", userName='" + getUserName() + '\'' +
                ", action='" + getAction() + '\'' +
                ", createTime=" + getCreateTime() +
                '}';
    }
}

  1. Increasing the mapper mapping file of the user table shows some very simple sql. Note that the new nodes are added in batches, where foreach syntax is used, and SQL can be generated dynamically through collections:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bolingcavalry.curd.mapper.UserMapper">

    <select id="sel" parameterType="int" resultType="user">
        select * from user where id = #{id}
    </select>

    <!--New Single Record-->
    <insert id="insertWithFields" useGeneratedKeys="true" keyProperty="id">
        insert into user (id, name, age) values (#{id}, #{name}, #{age})
    </insert>

    <!--Bulk Add-->
    <insert id="insertBatch" useGeneratedKeys="true" keyProperty="id">
        insert into user (id, name, age)
        values
        <foreach collection="users" item="user" separator=",">
            (#{user.id}, #{user.name}, #{user.age})
        </foreach>
    </insert>

    <!--Find by name-->
    <select id="findByName" parameterType="String" resultType="user">
        select id, name, age from user where name like concat('%', #{name}, '%')
    </select>

    <!--Delete specified data-->
    <delete id="delete">
        delete from user where id= #{id}
    </delete>

    <!--Delete all data-->
    <delete id="clearAll">
        delete from user
    </delete>

    <!--To update-->
    <update id="update">
        update user set name = #{name}, age = #{age} where id = #{id}
    </update>

    <!--Get Total-->
    <select id="totalCount" resultType="java.lang.Integer">
        select count(*) from user
    </select>
</mapper>
  1. Increase the mapper mapping file for the log table, as shown below, and pay attention to the join table operation selExtend, which results in the logExtendResultMap:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bolingcavalry.curd.mapper.LogMapper">

    <resultMap id="logExtendResultMap" type="logExtend">
        <id property="id" column="id"/>
        <result column="user_id" jdbcType="INTEGER" property="userId"/>
        <result column="action" jdbcType="VARCHAR" property="action"/>
        <result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
        <result column="user_name" jdbcType="TIMESTAMP" property="userName"/>
    </resultMap>

    <!--New Single Record-->
    <insert id="insertWithFields" useGeneratedKeys="true" keyProperty="id" parameterType="log">
        insert into log (id, user_id, action, create_time) values (#{id}, #{userId}, #{action}, #{createTime})
    </insert>

    <select id="selExtend" parameterType="int" resultMap="logExtendResultMap">
        select l.id as id,
               l.user_id as user_id,
               l.action as action,
               l.create_time as create_time,
               u.name as user_name
        from log as l
        left join user as u
        on l.user_id = u.id
        where l.id = #{id}
    </select>

</mapper>
  1. Add the mapper interface class UserMapper to the user table. Java, corresponding to the id of the sql node in the mapping file:
package com.bolingcavalry.curd.mapper;

import com.bolingcavalry.curd.entity.LogExtend;
import com.bolingcavalry.curd.entity.User;
import org.springframework.stereotype.Repository;
import java.util.List;

@Repository
public interface UserMapper {

    User sel(int id);

    int insertWithFields(User user);

    int insertBatch(List<User> users);

    int clearAll();

    List<User> findByName(String name);

    int update(User user);

    int delete(int id);

    int totalCount();

    LogExtend selExtend(int id);
}

  1. Add the mapper interface class LogMapper to the log table. Java, corresponding to the id of the sql node in the mapping file:
package com.bolingcavalry.curd.mapper;

import com.bolingcavalry.curd.entity.Log;
import com.bolingcavalry.curd.entity.LogExtend;
import org.springframework.stereotype.Repository;

@Repository
public interface LogMapper {
    Log sel(int id);

    LogExtend selExtend(int id);

    int insertWithFields(Log log);
}

  1. After the mapper interface is finished, it is the service layer, which writes the service of the user table first, as shown below, it is clear that all calls to the mapper interface are made:
package com.bolingcavalry.curd.service;

import com.bolingcavalry.curd.entity.User;
import com.bolingcavalry.curd.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class UserService {

    @Autowired
    UserMapper userMapper;

    public User sel(int id) {
        return userMapper.sel(id);
    }

    public User insertWithFields(User user) {
        userMapper.insertWithFields(user);
        return user;
    }

    public List<User> insertBatch(List<User> users) {
        userMapper.insertBatch(users);
        return users;
    }

    public int clearAll() {
       return userMapper.clearAll();
    }

    public List<User> findByName(String name) {
        return userMapper.findByName(name);
    }

    public int update(User user) {
        return userMapper.update(user);
    }

    public int delete(int id) {
        return userMapper.delete(id);
    }

    public int totalCount() {
        return userMapper.totalCount();
    }
}
  1. There are also log table service s:
package com.bolingcavalry.curd.service;

import com.bolingcavalry.curd.entity.Log;
import com.bolingcavalry.curd.entity.LogExtend;
import com.bolingcavalry.curd.entity.User;
import com.bolingcavalry.curd.mapper.LogMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class LogService {
    @Autowired
    LogMapper logMapper;

    public Log sel(int id){
        return logMapper.sel(id);
    }

    public LogExtend selExtend(int id) {
        return logMapper.selExtend(id);
    }

    public Log insertWithFields(Log log) {
        logMapper.insertWithFields(log);
        return log;
    }

}
  1. Finally, there is the controller layer, which is slightly more complex (with more notes) than the previous one due to the use of swagger:
package com.bolingcavalry.curd.controller;

import com.bolingcavalry.curd.entity.User;
import com.bolingcavalry.curd.service.UserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("/user")
@Api(tags = {"UserController"})
public class UserController {

    @Autowired
    private UserService userService;


    @ApiOperation(value = "Newly added user Record", notes="Newly added user Record")
    @RequestMapping(value = "/insertwithfields",method = RequestMethod.PUT)
    public User create(@RequestBody User user) {
        return userService.insertWithFields(user);
    }

    @ApiOperation(value = "Bulk Add user Record", notes="Bulk Add user Record")
    @RequestMapping(value = "/insertbatch", method = RequestMethod.PUT)
    public List<User> insertBatch(@RequestBody List<User> users) {
        return userService.insertBatch(users);
    }

    @ApiOperation(value = "Delete Specified ID Of user Record", notes="Delete Specified ID Of user Record")
    @ApiImplicitParam(name = "id", value = "user ID", paramType = "path", required = true, dataType = "Integer")
    @RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
    public int delete(@PathVariable int id){
        return userService.delete(id);
    }

    @ApiOperation(value = "delete user Table All Data", notes="delete user Table All Data")
    @RequestMapping(value = "/clearall", method = RequestMethod.DELETE)
    public int clearAll(){
        return userService.clearAll();
    }

    @ApiOperation(value = "according to ID modify user Record", notes="according to ID modify user Record")
    @RequestMapping(value = "/update", method = RequestMethod.POST)
    public int update(@RequestBody User user){
        return userService.update(user);
    }

    @ApiOperation(value = "Fuzzy Find All By Name user Record", notes="Fuzzy Find All By Name user Record")
    @ApiImplicitParam(name = "name", value = "User name", paramType = "path", required = true, dataType = "String")
    @RequestMapping(value = "/findbyname/{name}", method = RequestMethod.GET)
    public List<User> findByName(@PathVariable("name") String name){
        return userService.findByName(name);
    }

    @ApiOperation(value = "according to ID lookup user Record", notes="according to ID lookup user Record")
    @ApiImplicitParam(name = "id", value = "user ID", paramType = "path", required = true, dataType = "Integer")
    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public User GetUser(@PathVariable int id){
        return userService.sel(id);
    }

    @ApiOperation(value = "Get Total", notes="Get Total")
    @RequestMapping(value = "/totalcount", method = RequestMethod.GET)
    public int totalcount(){
        return userService.totalCount();
    }
}

  1. The controller s for log are as follows:
package com.bolingcavalry.curd.controller;

import com.bolingcavalry.curd.entity.Log;
import com.bolingcavalry.curd.entity.LogExtend;
import com.bolingcavalry.curd.service.LogService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/log")
@Api(tags = {"LogController"})
public class LogController {
    @Autowired
    private LogService logService;

    @ApiOperation(value = "according to ID Find Log Records", notes="according to ID Find Log Records")
    @ApiImplicitParam(name = "id", value = "Journal ID", paramType = "path", required = true, dataType = "Integer")
    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public LogExtend logExtend(@PathVariable int id){
        return logService.selExtend(id);
    }

    @ApiOperation(value = "New Logging", notes="New Logging")
    @RequestMapping(value = "/insertwithfields",method = RequestMethod.PUT)
    public Log create(@RequestBody Log log) {
        return logService.insertWithFields(log);
    }
}
  1. Finally, there is a code for unit testing. Let's try self-testing with junit. As shown below, there are three controller s tested: add, find, and delete. Note the usage of MockMvc, the usage of jsonPath method, and the order of execution is controlled by the Order comment (make sure to add the TestMethodOrder comment, otherwise the Order comment will not work):
package com.bolingcavalry.curd.controller;

import com.bolingcavalry.curd.entity.User;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonParser;
import org.junit.Ignore;
import org.junit.jupiter.api.*;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import java.util.List;
import java.util.UUID;

import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class UserControllerTest {

    @Autowired
    private MockMvc mvc;

    // The name field of the user table, where UUID is used as the user name to ensure that the records added and deleted during the test are the same
    static String testName;

    @BeforeAll
    static void init() {
        testName = UUID.randomUUID().toString().replaceAll("-","");;
    }

    @Test
    @Order(1)
    void insertWithFields() throws Exception {
        String jsonStr = "{\"name\": \"" + testName + "\", \"age\": 10}";

        mvc.perform(
                MockMvcRequestBuilders.put("/user/insertwithfields")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(jsonStr)
                        .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name", is(testName)))
                .andDo(print())
                .andReturn()
                .getResponse()
                .getContentAsString();
    }

    @Test
    @Order(2)
    void findByName() throws Exception {
        mvc.perform(MockMvcRequestBuilders.get("/user/findbyname/"+ testName).accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$", hasSize(1)))
                .andDo(print());
    }


    @Test
    @Order(3)
    void delete() throws Exception {
        // Find records by name first
        String responseString = mvc.perform(MockMvcRequestBuilders.get("/user/findbyname/"+ testName).accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$", hasSize(1)))
                .andDo(print())
                .andReturn()
                .getResponse()
                .getContentAsString();

        // Deserialize to get an array
        JsonArray jsonArray = JsonParser.parseString(responseString).getAsJsonArray();

        // Deserialize to get a user instance
        User user = new Gson().fromJson(jsonArray.get(0), User.class);

        // Perform deletion
        mvc.perform(MockMvcRequestBuilders.delete("/user/"+ user.getId()).accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(content().string(equalTo("1")))
                .andDo(print());
    }
}
  • At the end of the encoding, verify the above functions;

Unit test validation

  1. IDEA opens UserController Test. Java, click on the icon in the red box below to start unit testing:
  2. The IDEA will give you the result after the unit test is completed. As shown below, the detailed test process data can be viewed on the right side of the red box:
  • This is a small number of unit test cases, and swagger is used to validate each interface.

swagger validate web interface

  1. Start the CurdApplication class as follows:

  2. Browser access: http://localhost:8080/swagger-ui.html to open the swagger page, as shown below:

  3. Try the new interface first, as shown below:

  4. Click Try it out in red box 3 above! After the button, the response information is as follows, you can see that the operation was successful:

  5. Due to space limitation, the tests of other interfaces are not listed one by one. Please verify by yourself.

  • So far, the actual battle between basic addition and deletion checking and simple join table operation of MyBatis has been completed, and we will continue to explore the basic operation of MyBatis.

Welcome to my public number: programmer Xin Chen

Tags: Java Mybatis Spring Boot

Posted by Ekate on Sat, 21 May 2022 20:05:01 +0300