JavaRush /Курси /All lectures for UA purposes /Проект на тему: SQL, JDBC та Hibernate

Проект на тему: SQL, JDBC та Hibernate

All lectures for UA purposes
Рівень 1 , Лекція 514
Відкрита

Сьогодні будемо робити фінальний проєкт по четвертому модулю JRU. Що це буде? Спробуємо працювати з різними технологіями: MySQL, Hibernate, Redis, Docker. Тепер предметніше.

Завдання: у нас є реляційна база даних MySQL зі схемою (країна-місто, мова по країні). І є частий запит міста, яке йде з затримкою. Ми придумали рішення – винести всі дані, на які часто робиться запит, до Redis (in memory storage типу ключ-значення).

І потрібні нам не всі дані, а визначений набір полів. Проєкт буде у форматі туторіалу. Тобто, тут будемо порушувати проблему й одразу її вирішувати.

Отже, почнемо з того, який софт нам буде потрібен:

  1. IDEA Ultimate (у кого закінчився ключ — пишіть у Slack куратору групи)
  2. Workbench (або будь-який інший клієнт для MySQL)
  3. Docker
  4. redis-insight — опціонально

Наш план дій:

  1. Налаштувати докер. Для кожної ОС є особливості, тому радимо пошукати відповідь в мережі на питання типу "how to install docker on windows"). Перевірити, що все працює.
  2. Запустити MySQL сервер як докер-контейнер.
  3. Розгорнути дамп.
  4. Створити проект в IDEA, додати залежності maven.
  5. Зробити шар domain.
  6. Написати метод отримання всіх даних з MySQL.
  7. Написати метод трансформації даних (в Redis писатимемо лише ті дані, на які часто робиться запит).
  8. Запустити Redis сервер як докер-контейнер.
  9. Записати дані в Redis.
  10. Опціонально: встановити redis-insight, подивитися на дані, які зберігаються в Redis.
  11. Написати метод отримання даних з Redis.
  12. Написати метод отримання даних з MySQL.
  13. Порівняти швидкість отримання одних і тих самих даних з MySQL та Redis.

Налаштування Docker

Докер — це відкрита платформа для розробки, доставки та експлуатації застосунків. Ми його використовуватимемо для того, щоб не встановлювати і налаштовувати Redis на локальній машині, а використовувати вже готовий image. Докладніше про докер можна подивитися тут.

Аби переконатися, що докер у тебе встановлений і налаштований, виконай команду: docker -v

Якщо все ОК, ти побачиш версію докера

Розгорнути дамп

Для розгортання дампа потрібно з Workbench створити новий конекшн до БД, де вказати параметри. Я використовував стандартний порт (3306), не змінював ім'я користувача (root за замовчуванням) і вказав пароль для рутового користувача (root).

У Workbench-і робимо Data Import/Restore і обираємо Import from Self Contained File. Як файл вкажи куди ти скачав дамп. Схему заздалегідь створювати не потрібно — її створення входить до дамп-файлу. Після успішного імпорту, у тебе буде схема world із трьома таблицями:

  1. city — це таблиця міст.
  2. country — таблиця країн.
  3. country_language — таблиця, де вказується, який відсоток населення в країні говорить певною мовою.

Оскільки під час запуску контейнера ми використовували вольюм, після зупинки та навіть видалення контейнера mysql і повторного виконання команди запуску (docker run --name mysql -d -p 3306:3306 -e ) повторно дамп розгортати не потрібно – він вже розгорнутий у вольюмі.

Створити проєкт в IDEA, додати залежності maven

Як в IDEA створити проєкт ти вже давно знаєш – це найлегший пункт у сьогоднішньому проєкті.

Додаємо до pom-файлу залежності:


<dependencies>
   <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.30</version>
   </dependency>

   <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-core-jakarta</artifactId>
      <version>5.6.14.Final</version>
   </dependency>

   <dependency>
      <groupId>p6spy</groupId>
      <artifactId>p6spy</artifactId>
      <version>3.9.1</version>
   </dependency>

   <dependency>
      <groupId>io.lettuce</groupId>
      <artifactId>lettuce-core</artifactId>
      <version>6.2.2.RELEASE</version>
   </dependency>

   <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.14.0</version>
   </dependency>
</dependencies>

Перші три залежності тобі вже давно знайомі.

lettuce-core — це один із доступних Java клієнтів для роботи з Redis.

jackson-databind — залежність для використання ObjectMapper (для перетворення даних для їх зберігання в Redis (ключ-значення типу String)).

Так само до папки ресурсів (src/main/resources) додай spy.properties для перегляду запитів з параметрами, які виконує Hibernate. Вміст файлу:

driverlist=com.mysql.cj.jdbc.Driver
dateformat=yyyy-MM-dd hh:mm:ss a
appender=com.p6spy.engine.spy.appender.StdoutLogger
logMessageFormat=com.p6spy.engine.spy.appender.MultiLineFormat

Зробити шар domain

Створи пакет com.javarush.domain

Мені зручно під час мапінгу таблиць на ентіті користуватися структурою таблиці в IDEA, тому додамо підключення БД до IDEA.

Ентіті я пропоную створювати в такому порядку:

  • Country
  • City
  • CountryLanguage

Бажано щоб мапінг ти виконав самостійно.

Код класу Country:


package com.javarush.domain;

import jakarta.persistence.*;

import java.math.BigDecimal;
import java.util.Set;

@Entity
@Table(schema = "world", name = "country")
public class Country {
    @Id
    @Column(name = "id")
    private Integer id;

    private String code;

    @Column(name = "code_2")
    private String alternativeCode;

    private String name;

    @Column(name = "continent")
    @Enumerated(EnumType.ORDINAL)
    private Continent continent;

    private String region;

    @Column(name = "surface_area")
    private BigDecimal surfaceArea;

    @Column(name = "indep_year")
    private Short independenceYear;

    private Integer population;

    @Column(name = "life_expectancy")
    private BigDecimal lifeExpectancy;

    @Column(name = "gnp")
    private BigDecimal GNP;

    @Column(name = "gnpo_id")
    private BigDecimal GNPOId;

    @Column(name = "local_name")
    private String localName;

    @Column(name = "government_form")
    private String governmentForm;

    @Column(name = "head_of_state")
    private String headOfState;

    @OneToOne
    @JoinColumn(name = "capital")
    private City city;

    @OneToMany(fetch = FetchType.EAGER)
    @JoinColumn(name = "country_id")
    private Set<CountryLanguage> languages;


    //Getters and Setters omitted

}

У коді є 3 цікаві моменти.

Перший — це enum Continent, який у БД зберігається як original значення. У структурі таблиці country в коментарі до поля continent можна подивитися, яке числове значення відповідає якому континенту.


package com.javarush.domain;

public enum Continent {
    ASIA,
    EUROPE,
    NORTH_AMERICA,
    AFRICA,
    OCEANIA,
    ANTARCTICA,
    SOUTH_AMERICA
}

Другий момент — це сет сутностей CountryLanguage. Тут зв'язок @OneToMany, якого не було у другому проєкті цього модуля. За замовчуванням Hibernate не витягуватиме значення цього сету під час запиту ентіті країни. Але оскільки нам потрібно віднімати всі значення з реляційної бази даних для кешування, тут додано параметр FetchType.EAGER.

Третє — поле city. Зв'язок @OneToOne — тут начебто все звично і зрозуміло. Але якщо поглянути на структуру foreign key у БД, побачимо, що у країни (country) є зв'язок на столицю (city), а у міста (city) — на країну (country). А це — циклічна залежність.

Поки що з цим нічого робити не будемо, але, коли дійдемо до пункту «Написати метод отримання всіх даних з MySQL», подивимося, які запити виконує Hibernate, на їх кількість, і згадаємо про цей пункт.


package com.javarush.domain;

import jakarta.persistence.*;

@Entity
@Table(schema = "world", name = "city")
public class City {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "country_id")
    private Country country;

    private String district;

    private Integer population;


    //Getters and Setters omitted

}

Код класу CountryLanguage:


package com.javarush.domain;

import jakarta.persistence.*;
import org.hibernate.annotations.Type;

import java.math.BigDecimal;

@Entity
@Table(schema = "world", name = "country_language")
public class CountryLanguage {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Integer id;

    @ManyToOne
    @JoinColumn(name = "country_id")
    private Country country;

    private String language;

    @Column(name = "is_official", columnDefinition = "BIT")
    @Type(type = "org.hibernate.type.NumericBooleanType")
    private Boolean isOfficial;

    private BigDecimal percentage;


    //Getters and Setters omitted
}

Написати метод отримання всіх даних з MySQL

У класі Main оголосимо поля:


private final SessionFactory sessionFactory;
private final RedisClient redisClient;

private final ObjectMapper mapper;

private final CityDAO cityDAO;
private final CountryDAO countryDAO;

та ініціализуємо їх у конструкторі класу Main:


public Main() {
    sessionFactory = prepareRelationalDb();
    cityDAO = new CityDAO(sessionFactory);
    countryDAO = new CountryDAO(sessionFactory);

    redisClient = prepareRedisClient();
    mapper = new ObjectMapper();
}

Як бачиш, не вистачає методів та класів — давай хх напишемо.

Оголоси пакет com.javarush.dao та додай до нього 2 класи:


package com.javarush.dao;

import com.javarush.domain.Country;
import org.hibernate.SessionFactory;
import org.hibernate.query.Query;

import java.util.List;

public class CountryDAO {
    private final SessionFactory sessionFactory;

    public CountryDAO(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public List<Country> getAll() {
        Query<Country> query = sessionFactory.getCurrentSession().createQuery("select c from Country c", Country.class);
        return query.list();
    }
}

package com.javarush.dao;

import com.javarush.domain.City;
import org.hibernate.SessionFactory;
import org.hibernate.query.Query;

import java.util.List;

public class CityDAO {
    private final SessionFactory sessionFactory;

    public CityDAO(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public List<City> getItems(int offset, int limit) {
        Query<City> query = sessionFactory.getCurrentSession().createQuery("select c from City c", City.class);
        query.setFirstResult(offset);
        query.setMaxResults(limit);
        return query.list();
    }

    public int getTotalCount() {
        Query<Long> query = sessionFactory.getCurrentSession().createQuery("select count(c) from City c", Long.class);
        return Math.toIntExact(query.uniqueResult());
    }
}

Тепер можна в Main заімпортити ці 2 класи. Ще не вистачає двох методів:


private SessionFactory prepareRelationalDb() {
    final SessionFactory sessionFactory;
    Properties properties = new Properties();
    properties.put(Environment.DIALECT, "org.hibernate.dialect.MySQL8Dialect");
    properties.put(Environment.DRIVER, "com.p6spy.engine.spy.P6SpyDriver");
    properties.put(Environment.URL, "jdbc:p6spy:mysql://localhost:3306/world");
    properties.put(Environment.USER, "root");
    properties.put(Environment.PASS, "root");
    properties.put(Environment.CURRENT_SESSION_CONTEXT_CLASS, "thread");
    properties.put(Environment.HBM2DDL_AUTO, "validate");
    properties.put(Environment.STATEMENT_BATCH_SIZE, "100");

    sessionFactory = new Configuration()
            .addAnnotatedClass(City.class)
            .addAnnotatedClass(Country.class)
            .addAnnotatedClass(CountryLanguage.class)
            .addProperties(properties)
            .buildSessionFactory();
    return sessionFactory;
}

До редіса ми ще не дійшли, тому імплементація ініціалізації клієнта редіса поки залишається заглушкою:


private void shutdown() {
    if (nonNull(sessionFactory)) {
        sessionFactory.close();
    }
    if (nonNull(redisClient)) {
        redisClient.shutdown();
    }
}

Нарешті, можна написати метод, в якому ми витягнемо всі міста:


    private List<City> fetchData(Main main) {
        try (Session session = main.sessionFactory.getCurrentSession()) {
            List<City> allCities = new ArrayList<>();
            session.beginTransaction();

            int totalCount = main.cityDAO.getTotalCount();
            int step = 500;
            for (int i = 0; i < totalCount; i += step) {
                allCities.addAll(main.cityDAO.getItems(i, step));
            }
            session.getTransaction().commit();
            return allCities;
        }
    }

Особливість реалізації така, що міста ми отримуємо по 500 штук. Це потрібно, бо існують обмеження на обсяг даних, що передаються. Так, в нашому випадку вони нас не стосуються, оскільки в нашій БД загалом 4079 міст. Але в продакшн застосунках, коли потрібно отримати багато даних, таким прийомом користуються часто.

І реалізація методу main:


public static void main(String[] args) {
    Main main = new Main();
    List<City> allCities = main.fetchData(main);
    main.shutdown();
}

Тепер можна вперше запустити нашу програму в дебазі і подивитися, як вона працює (або не працює – так, це часто трапляється).

Міста дістаються. До кожного міста дістається країна, якщо її ще не вичитали з БД раніше, для іншого міста. Давай умовно порахуємо, скільки запитів Hibernate надішле до БД:

  • 1 запит потрібен, щоб дізнатися загальну кількість міст (потрібно для циклу ітерування по 500 міст, щоб знати коли зупинитися).
  • 4079 / 500 = 9 запитів (список міст).
  • Для кожного з міст дістається країна, якщо її не зчитали раніше. Оскільки у БД 239 країн, це нам дасть 239 запитів.

Разом 249 запитів. І це ще ми вказали, що разом із країною потрібно одразу отримувати сет мов, інакше була б взагалі складна ситуація. Але це все одно багато, тому давай трохи підтюнимо поведінку. Почнемо з роздумів: що робити, куди тікати? А якщо серйозно — чому так багато запитів. Якщо поглянути на лог запитів, бачимо, що кожна країна запитується окремо, тому перше просте рішення — давай зробимо запит на всі країни разом, тому що ми наперед знаємо, що в цій транзакції вони нам будуть потрібні.

У методі fetchData() одразу після початку транзакції додай рядок:

List<Country> countries = main.countryDAO.getAll();

Рахуємо запити:

  • 1 — отримати всі країни
  • 239 — запит щодо кожної країни її столиці
  • 1 — запит кількості міст
  • 9 — запит списків міст

Разом виходить 250. Ідея гарна, але не спрацювало. Проблема в тому, що в країні зв'язок із столицею (city) @OneToOne. А такий зв'язок за замовчуванням завантажується одразу (FetchType.EAGER). Давай поставимо FetchType.LAZY, оскільки потім у цій же транзакції ми все одно завантажимо всі міста.


@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "capital")
private City city;

Окремо на столиці припинили йти запити, але кількість запитів не змінилася. Тепер по кожній країні робиться окремий запит на список CountryLanguage. Тобто прогрес є, і ми рухаємось у правильному напрямку. Якщо пам'ятаєш, у лекціях пропонувалося рішення «join fetch», щоб завдяки додаванню додаткового джоїну до запиту робити запит на сутність із залежними даними за один раз. У CountryDAO перепиши HQL запит у методі getAll() на:

"select c from Country c join fetch c.languages"

Запуск. Дивимося лог, рахуємо запити:

  • 1 — усі країни з мовами
  • 1 — кількість міст
  • 9 — списки міст.

Разом 11 — у нас усе вийшло)) Якщо ти не лише прочитав весь цей текст, а й спробував запускати після кожного кроку тюнінгу застосунки, у тебе була можливість навіть візуально відзначити прискорення роботи всього додатку в кілька разів.

Написати метод трансформації даних

Створимо пакет com.javarush.redis, до якого додамо 2 класи: CityCountry (дані щодо міста та країни, в якій це місто розташоване) та Language (дані з мови). Сюди винесено всі поля, які «за завданням» запитуються часто в «запиті, що гальмує».


package com.javarush.redis;

import com.javarush.domain.Continent;

import java.math.BigDecimal;
import java.util.Set;

public class CityCountry {
    private Integer id;

    private String name;

    private String district;

    private Integer population;

    private String countryCode;

    private String alternativeCountryCode;

    private String countryName;

    private Continent continent;

    private String countryRegion;

    private BigDecimal countrySurfaceArea;

    private Integer countryPopulation;

    private Set<Language> languages;

    //Getters and Setters omitted
}

package com.javarush.redis;

import java.math.BigDecimal;

public class Language {
    private String language;
    private Boolean isOfficial;
    private BigDecimal percentage;

    //Getters and Setters omitted
}

У методі main після отримання всіх міст додай рядок

List<CityCountry>> preparedData = main.transformData(allCities);

Та реалізуй цей метод:


private List<CityCountry> transformData(List<City> cities) {
    return cities.stream().map(city -> {
        CityCountry res = new CityCountry();
        res.setId(city.getId());
        res.setName(city.getName());
        res.setPopulation(city.getPopulation());
        res.setDistrict(city.getDistrict());

        Country country = city.getCountry();
        res.setAlternativeCountryCode(country.getAlternativeCode());
        res.setContinent(country.getContinent());
        res.setCountryCode(country.getCode());
        res.setCountryName(country.getName());
        res.setCountryPopulation(country.getPopulation());
        res.setCountryRegion(country.getRegion());
        res.setCountrySurfaceArea(country.getSurfaceArea());
        Set<CountryLanguage> countryLanguages = country.getLanguages();
        Set<Language> languages = countryLanguages.stream().map(cl -> {
            Language language = new Language();
            language.setLanguage(cl.getLanguage());
            language.setOfficial(cl.getOfficial());
            language.setPercentage(cl.getPercentage());
            return language;
        }).collect(Collectors.toSet());
        res.setLanguages(languages);

        return res;
    }).collect(Collectors.toList());
}

Думаю, цей метод пояснень не потребує: просто створюємо сутність CityCountry та заповнюємо даними з City, Country, CountryLanguage.

Запустити Redis сервер як докер-контейнер

Тут є 2 варіанти. Якщо ти робитимеш опціональний крок "встановити redis-insight, подивитися на дані, які зберігаються в Redis", тоді команда для тебе:

docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest

Якщо вирішив пропустити цей крок, тоді достатньо:

docker run -d --name redis -p 6379:6379 redis:latest 

Різниця в тому, що в першому варіанті прокидається порт 8001 на локальну машину, до якого можна підключитися зовнішнім клієнтом, щоб подивитися, що зберігається всередині. Імена я звик давати значущі, тому redis-stack або redis.

Після запуску можна переглянути список запущених контейнерів. Для цього виконай команду:

docker container ls

І побачиш щось приблизно таке:

Якщо потрібно знайти якусь команду, можна або в терміналі подивитися хелп (docker help) або загуглити «how to …» (наприклад, docker how to remove running container).

Також у конструкторі Main ми викликали ініціалізацію редіс-клієнта, але сам метод не реалізували. Додай реалізацію:


private RedisClient prepareRedisClient() {
    RedisClient redisClient = RedisClient.create(RedisURI.create("localhost", 6379));
    try (StatefulRedisConnection<String, String> connection = redisClient.connect()) {
        System.out.println("\nConnected to Redis\n");
    }
    return redisClient;
}

sout доданий у навчальних цілях, щоб у лозі запуску бачити, що всі ОК, і конекшн через редіс-клієнт пройшов без помилок.

Записати дані в Redis

До методу main додай виклик

main.pushToRedis(preparedData); 

З такою реалізацією методу:


private void pushToRedis(List<CityCountry> data) {
    try (StatefulRedisConnection<String, String> connection = redisClient.connect()) {
        RedisStringCommands<String, String> sync = connection.sync();
        for (CityCountry cityCountry : data) {
            try {
                sync.set(String.valueOf(cityCountry.getId()), mapper.writeValueAsString(cityCountry));
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }

    }
}

Тут відкривається синхронне з'єднання з редіс-клієнтом, і послідовно кожен об'єкт типу CityCountry пишеться до редісу. Оскільки редіс — це сховище формату ключ-значення типу String, ключ (id міста) перетворюється на рядок. А значення — теж на рядок, але з використанням ObjectMapper у JSON форматі.

Залишилося запустити і перевірити, що немає помилок у лозі. Все спрацювало.

Встановити redis-insight, подивитися на дані, які зберігаються в Redis (опціонально)

Завантажуємо за посиланням redis-insight та встановлюємо його. Після запуску у мене одразу показує наш інстанс редісу в докер-контейнері:

Якщо зайти, побачимо список всіх ключів:

І можна зайти на будь-який ключ, щоб подивитися, які дані за ним зберігаються:

Написати метод отримання даних з Redis

Для перевірки використовуємо такий тест: отримаємо 10 записів CityCountry. Кожен — окремим запитом, але в одному конекшні.

Дані з редіс можна отримати через нашого редіс-клієнта. Для цього напишемо метод, що приймає список ID, які потрібно отримати.


private void testRedisData(List<Integer> ids) {
    try (StatefulRedisConnection<String, String> connection = redisClient.connect()) {
        RedisStringCommands<String, String> sync = connection.sync();
        for (Integer id : ids) {
            String value = sync.get(String.valueOf(id));
            try {
                mapper.readValue(value, CityCountry.class);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }
    }
}

Реалізація, думаю, інтуїтивно зрозуміла: відкриваємо синхронне з'єднання, і для кожного id отримуємо JSON String, який перетворюємо на потрібний нам об'єкт типу CityCountry.

Написати метод отримання даних з MySQL

У класі CityDAO додай метод getById(Integer id), в якому отримаємо місто разом з країною:


public City getById(Integer id) {
    Query<City> query = sessionFactory.getCurrentSession().createQuery("select c from City c join fetch c.country where c.id = :ID", City.class);
    query.setParameter("ID", id);
    return query.getSingleResult();
}

За аналогією з попереднім пунктом, додамо до класу Main аналогічний метод для MySQL:


private void testMysqlData(List<Integer> ids) {
    try (Session session = sessionFactory.getCurrentSession()) {
        session.beginTransaction();
        for (Integer id : ids) {
            City city = cityDAO.getById(id);
            Set<CountryLanguage> languages = city.getCountry().getLanguages();
        }
        session.getTransaction().commit();
    }
}

З особливостей, щоб точно отримати повний об'єкт (без проксі-заглушок), явно зробимо запит у країни на список мов.

Порівняти швидкість отримання одних і тих же даних з MySQL та Redis

Тут одразу наведу код методу main, і результат, який виходить на моєму локальному комп'ютері.


public static void main(String[] args) {
    Main main = new Main();
    List<City> allCities = main.fetchData(main);
    List<CityCountry> preparedData = main.transformData(allCities);
    main.pushToRedis(preparedData);

    //закриємо поточну сесію, щоб точно зробити запит до БД, а не витянути дані з кеша
    main.sessionFactory.getCurrentSession().close();

    //обираємо 10 випадкових id міст
    //оскільки ми не робили обробку невалідних ситуацій, використовуй id, які існують БД
    List<Integer> ids = List.of(3, 2545, 123, 4, 189, 89, 3458, 1189, 10, 102);

    long startRedis = System.currentTimeMillis();
    main.testRedisData(ids);
    long stopRedis = System.currentTimeMillis();

    long startMysql = System.currentTimeMillis();
    main.testMysqlData(ids);
    long stopMysql = System.currentTimeMillis();

    System.out.printf("%s:\t%d ms\n", "Redis", (stopRedis - startRedis));
    System.out.printf("%s:\t%d ms\n", "MySQL", (stopMysql - startMysql));

    main.shutdown();
}

Під час тестування є особливість — дані з MySQL лише читаються, так що його можна не перезапускати між запусками нашої програми. А до Redis пишуться.

Хоча у разі спроби додати дублікат за таким же ключем дані просто оновляться, я б рекомендував між запусками програми в терміналі виконувати команди зупинки контейнера docker stop redis-stack та видалення контейнера docker rm redis-stack. Після цього заново піднімати контейнер з редісом docker run -d --name redis-stack -p 6379:6379 -p 8001:8001 redis/redis-stack:latest, і лише після цього виконувати нашу програму.

Ось мої результати тестування:

У підсумку ми домоглися підвищення продуктивності відповіді на «гальмуючий частий» запит у півтора рази. До того ж, з урахуванням, що в тестуванні ми використовували не найшвидшу десеріалізацію через ObjectMapper. Якщо її замінити на GSON, швидше за все, можна "виграти" ще трохи часу.

Коментарі (2)
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ
Павло Лєбєдєв Рівень 111 Expert
17 березня 2024
Багато часу в мене пішло, щоб з'ясувати такий момент стосовно запуску редіса з докера: якщо створювати контейнер командою docker run -d --name redis -p 6379:6379 redis:latest - буде доступ до редіса, бо "під капотом" буде вказано Host IP. Якщо ж створювати контейнер чи в докері чи через IDEA і не вказувати Host IP вручну - доступу до редіса в мене не було. Host IP можна вказувати localhost чи 0.0.0.0
Павло Лєбєдєв Рівень 111 Expert
29 лютого 2024
Дійшов до слів: "Тепер можна вперше запустити нашу програму в дебазі" Але справді запустити неможна - не реалізовані 2 методи. Це мене не збентежило, чуйка підказала, що далі ці методи будуть реалізовані (так і є), але прикольно - лектор пише, що можна, а насправді не можна